From 3b361f9039759122fea8e5fccdea214f8b34fcca Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:32:27 +0000 Subject: [PATCH] feat: standardize zero trust error messages and enhance mpp verification Co-authored-by: dcplatforms <10982057+dcplatforms@users.noreply.github.com> --- scripts/ocp-cli.js | 6 +- src/middleware/mpp.js | 8 ++- tests/unit/mpp.spec.js | 130 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 tests/unit/mpp.spec.js diff --git a/scripts/ocp-cli.js b/scripts/ocp-cli.js index 65b1ba7..0f43e7f 100644 --- a/scripts/ocp-cli.js +++ b/scripts/ocp-cli.js @@ -166,17 +166,17 @@ program.command('x402:settle') // Validate budget if (decodedMandate.max_budget && amountNum > decodedMandate.max_budget.value) { - console.error(`Fiduciary Validation Failed: Amount ${amountNum} exceeds mandate budget of ${decodedMandate.max_budget.value} ${decodedMandate.max_budget.currency}`); + console.error(`Zero Trust Validation Failed: Amount ${amountNum} exceeds mandate budget of ${decodedMandate.max_budget.value} ${decodedMandate.max_budget.currency}`); return; } // Validate expiration if (decodedMandate.exp < Math.floor(Date.now() / 1000)) { - console.error('Fiduciary Validation Failed: Mandate has expired'); + console.error('Zero Trust Validation Failed: Mandate has expired'); return; } } catch (error) { - console.error(`Fiduciary Validation Failed: ${error.message}`); + console.error(`Zero Trust Validation Failed: ${error.message}`); return; } diff --git a/src/middleware/mpp.js b/src/middleware/mpp.js index 225eae6..b4f4d4c 100644 --- a/src/middleware/mpp.js +++ b/src/middleware/mpp.js @@ -6,6 +6,8 @@ * and retries the request with the payment header. */ +const logger = require('../utils/logger'); + class MPP402Handler { constructor(agentService, mandateService) { this.agentService = agentService; @@ -40,7 +42,7 @@ class MPP402Handler { * @private */ async _handle402Response(agent, response, intentMandateToken, requestFn) { - console.log(`MPP: Handling 402 Payment Required for agent ${agent.name}`); + logger.info(`MPP: Handling 402 Payment Required for agent ${agent.name}`); // 1. Extract payment requirement details from headers or body // MPP standard uses headers like 'X-MPP-Amount' and 'X-MPP-Merchant-DID' @@ -62,7 +64,7 @@ class MPP402Handler { } // 3. Generate a Cart Mandate for the specific 402 request - console.log(`MPP: Issuing autonomous cart mandate for amount ${amount}`); + logger.info(`MPP: Issuing autonomous cart mandate for amount ${amount}`); const cartMandateToken = await this.mandateService.issueCartMandate({ intentMandate: intentMandateToken, cartItems, @@ -71,7 +73,7 @@ class MPP402Handler { }); // 4. Retry the request with the Payment Mandate header - console.log(`MPP: Retrying request with Cart Mandate...`); + logger.info(`MPP: Retrying request with Cart Mandate...`); return await requestFn({ headers: { 'X-OCP-Cart-Mandate': cartMandateToken, diff --git a/tests/unit/mpp.spec.js b/tests/unit/mpp.spec.js new file mode 100644 index 0000000..6841bad --- /dev/null +++ b/tests/unit/mpp.spec.js @@ -0,0 +1,130 @@ +const MPP402Handler = require('../../src/middleware/mpp'); +const MandateService = require('../../src/services/mandate'); + +describe('MPP402Handler', () => { + let mppHandler; + let mockAgentService; + let mockMandateService; + const signingKey = 'test-secret'; + + beforeEach(() => { + mockAgentService = {}; + mockMandateService = new MandateService({ signingKey }); + mppHandler = new MPP402Handler(mockAgentService, mockMandateService); + }); + + describe('executeAutonomousRequest', () => { + it('should return response if status is not 402', async () => { + const mockResponse = { status: 200, data: 'success' }; + const requestFn = jest.fn().mockResolvedValue(mockResponse); + const agent = { name: 'TestAgent' }; + + const result = await mppHandler.executeAutonomousRequest(agent, requestFn, 'some-token'); + + expect(result).toBe(mockResponse); + expect(requestFn).toHaveBeenCalledTimes(1); + }); + + it('should handle 402, issue cart mandate and retry', async () => { + const agent = { id: 'agent_123', name: 'TestAgent' }; + const intentMandate = await mockMandateService.issueIntentMandate({ + userDid: 'did:key:user', + agentDid: 'did:key:agent', + maxBudget: 1000 + }); + + const response402 = { + status: 402, + headers: { + 'x-mpp-amount': '500', + 'x-mpp-currency': 'USD', + 'x-mpp-merchant-did': 'did:key:merchant' + }, + data: { + cart_items: [{ item: 'API', quantity: 1 }] + } + }; + + const finalResponse = { status: 200, data: 'paid' }; + + // First call returns 402, second call returns 200 + const requestFn = jest.fn() + .mockResolvedValueOnce(response402) + .mockResolvedValueOnce(finalResponse); + + const result = await mppHandler.executeAutonomousRequest(agent, requestFn, intentMandate); + + expect(result).toBe(finalResponse); + expect(requestFn).toHaveBeenCalledTimes(2); + + // Verify retry call had the cart mandate header + const secondCallArgs = requestFn.mock.calls[1][0]; + expect(secondCallArgs.headers['X-OCP-Cart-Mandate']).toBeDefined(); + expect(secondCallArgs.headers['Authorization']).toBe(`Bearer ${agent.id}`); + }); + + it('should throw error if payment amount exceeds intent budget', async () => { + const agent = { name: 'TestAgent' }; + const intentMandate = await mockMandateService.issueIntentMandate({ + userDid: 'did:key:user', + agentDid: 'did:key:agent', + maxBudget: 100 + }); + + const response402 = { + status: 402, + headers: { + 'x-mpp-amount': '500', + 'x-mpp-merchant-did': 'did:key:merchant' + } + }; + + const requestFn = jest.fn().mockResolvedValue(response402); + + await expect(mppHandler.executeAutonomousRequest(agent, requestFn, intentMandate)) + .rejects.toThrow('MPP: Payment amount 500 exceeds intent mandate budget of 100'); + }); + + it('should throw error if 402 response is missing requirements', async () => { + const agent = { name: 'TestAgent' }; + const intentMandate = 'some-token'; + const response402 = { + status: 402, + headers: {} // Missing amount and merchant + }; + + const requestFn = jest.fn().mockResolvedValue(response402); + + await expect(mppHandler.executeAutonomousRequest(agent, requestFn, intentMandate)) + .rejects.toThrow('Incomplete payment requirements in 402 response'); + }); + + it('should handle 402 thrown as an error (e.g. from axios)', async () => { + const agent = { id: 'agent_123', name: 'TestAgent' }; + const intentMandate = await mockMandateService.issueIntentMandate({ + userDid: 'did:key:user', + agentDid: 'did:key:agent', + maxBudget: 1000 + }); + + const error402 = new Error('Payment Required'); + error402.response = { + status: 402, + headers: { + 'x-mpp-amount': '500', + 'x-mpp-merchant-did': 'did:key:merchant' + } + }; + + const finalResponse = { status: 200, data: 'paid' }; + const requestFn = jest.fn() + .mockRejectedValueOnce(error402) + .mockResolvedValueOnce(finalResponse); + + const result = await mppHandler.executeAutonomousRequest(agent, requestFn, intentMandate); + + expect(result).toBe(finalResponse); + expect(requestFn).toHaveBeenCalledTimes(2); + }); + }); +});