Skip to content

Commit 5488a9a

Browse files
authored
Merge pull request #14 from dcplatforms/feat/zero-trust-standardization-and-mpp-verification-2365180924913388382
Standardize Zero Trust Validation and Enhance MPP Verification
2 parents 5b9c8ca + 3b361f9 commit 5488a9a

3 files changed

Lines changed: 138 additions & 6 deletions

File tree

scripts/ocp-cli.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,17 @@ program.command('x402:settle')
166166

167167
// Validate budget
168168
if (decodedMandate.max_budget && amountNum > decodedMandate.max_budget.value) {
169-
console.error(`Fiduciary Validation Failed: Amount ${amountNum} exceeds mandate budget of ${decodedMandate.max_budget.value} ${decodedMandate.max_budget.currency}`);
169+
console.error(`Zero Trust Validation Failed: Amount ${amountNum} exceeds mandate budget of ${decodedMandate.max_budget.value} ${decodedMandate.max_budget.currency}`);
170170
return;
171171
}
172172

173173
// Validate expiration
174174
if (decodedMandate.exp < Math.floor(Date.now() / 1000)) {
175-
console.error('Fiduciary Validation Failed: Mandate has expired');
175+
console.error('Zero Trust Validation Failed: Mandate has expired');
176176
return;
177177
}
178178
} catch (error) {
179-
console.error(`Fiduciary Validation Failed: ${error.message}`);
179+
console.error(`Zero Trust Validation Failed: ${error.message}`);
180180
return;
181181
}
182182

src/middleware/mpp.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* and retries the request with the payment header.
77
*/
88

9+
const logger = require('../utils/logger');
10+
911
class MPP402Handler {
1012
constructor(agentService, mandateService) {
1113
this.agentService = agentService;
@@ -40,7 +42,7 @@ class MPP402Handler {
4042
* @private
4143
*/
4244
async _handle402Response(agent, response, intentMandateToken, requestFn) {
43-
console.log(`MPP: Handling 402 Payment Required for agent ${agent.name}`);
45+
logger.info(`MPP: Handling 402 Payment Required for agent ${agent.name}`);
4446

4547
// 1. Extract payment requirement details from headers or body
4648
// MPP standard uses headers like 'X-MPP-Amount' and 'X-MPP-Merchant-DID'
@@ -62,7 +64,7 @@ class MPP402Handler {
6264
}
6365

6466
// 3. Generate a Cart Mandate for the specific 402 request
65-
console.log(`MPP: Issuing autonomous cart mandate for amount ${amount}`);
67+
logger.info(`MPP: Issuing autonomous cart mandate for amount ${amount}`);
6668
const cartMandateToken = await this.mandateService.issueCartMandate({
6769
intentMandate: intentMandateToken,
6870
cartItems,
@@ -71,7 +73,7 @@ class MPP402Handler {
7173
});
7274

7375
// 4. Retry the request with the Payment Mandate header
74-
console.log(`MPP: Retrying request with Cart Mandate...`);
76+
logger.info(`MPP: Retrying request with Cart Mandate...`);
7577
return await requestFn({
7678
headers: {
7779
'X-OCP-Cart-Mandate': cartMandateToken,

tests/unit/mpp.spec.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
const MPP402Handler = require('../../src/middleware/mpp');
2+
const MandateService = require('../../src/services/mandate');
3+
4+
describe('MPP402Handler', () => {
5+
let mppHandler;
6+
let mockAgentService;
7+
let mockMandateService;
8+
const signingKey = 'test-secret';
9+
10+
beforeEach(() => {
11+
mockAgentService = {};
12+
mockMandateService = new MandateService({ signingKey });
13+
mppHandler = new MPP402Handler(mockAgentService, mockMandateService);
14+
});
15+
16+
describe('executeAutonomousRequest', () => {
17+
it('should return response if status is not 402', async () => {
18+
const mockResponse = { status: 200, data: 'success' };
19+
const requestFn = jest.fn().mockResolvedValue(mockResponse);
20+
const agent = { name: 'TestAgent' };
21+
22+
const result = await mppHandler.executeAutonomousRequest(agent, requestFn, 'some-token');
23+
24+
expect(result).toBe(mockResponse);
25+
expect(requestFn).toHaveBeenCalledTimes(1);
26+
});
27+
28+
it('should handle 402, issue cart mandate and retry', async () => {
29+
const agent = { id: 'agent_123', name: 'TestAgent' };
30+
const intentMandate = await mockMandateService.issueIntentMandate({
31+
userDid: 'did:key:user',
32+
agentDid: 'did:key:agent',
33+
maxBudget: 1000
34+
});
35+
36+
const response402 = {
37+
status: 402,
38+
headers: {
39+
'x-mpp-amount': '500',
40+
'x-mpp-currency': 'USD',
41+
'x-mpp-merchant-did': 'did:key:merchant'
42+
},
43+
data: {
44+
cart_items: [{ item: 'API', quantity: 1 }]
45+
}
46+
};
47+
48+
const finalResponse = { status: 200, data: 'paid' };
49+
50+
// First call returns 402, second call returns 200
51+
const requestFn = jest.fn()
52+
.mockResolvedValueOnce(response402)
53+
.mockResolvedValueOnce(finalResponse);
54+
55+
const result = await mppHandler.executeAutonomousRequest(agent, requestFn, intentMandate);
56+
57+
expect(result).toBe(finalResponse);
58+
expect(requestFn).toHaveBeenCalledTimes(2);
59+
60+
// Verify retry call had the cart mandate header
61+
const secondCallArgs = requestFn.mock.calls[1][0];
62+
expect(secondCallArgs.headers['X-OCP-Cart-Mandate']).toBeDefined();
63+
expect(secondCallArgs.headers['Authorization']).toBe(`Bearer ${agent.id}`);
64+
});
65+
66+
it('should throw error if payment amount exceeds intent budget', async () => {
67+
const agent = { name: 'TestAgent' };
68+
const intentMandate = await mockMandateService.issueIntentMandate({
69+
userDid: 'did:key:user',
70+
agentDid: 'did:key:agent',
71+
maxBudget: 100
72+
});
73+
74+
const response402 = {
75+
status: 402,
76+
headers: {
77+
'x-mpp-amount': '500',
78+
'x-mpp-merchant-did': 'did:key:merchant'
79+
}
80+
};
81+
82+
const requestFn = jest.fn().mockResolvedValue(response402);
83+
84+
await expect(mppHandler.executeAutonomousRequest(agent, requestFn, intentMandate))
85+
.rejects.toThrow('MPP: Payment amount 500 exceeds intent mandate budget of 100');
86+
});
87+
88+
it('should throw error if 402 response is missing requirements', async () => {
89+
const agent = { name: 'TestAgent' };
90+
const intentMandate = 'some-token';
91+
const response402 = {
92+
status: 402,
93+
headers: {} // Missing amount and merchant
94+
};
95+
96+
const requestFn = jest.fn().mockResolvedValue(response402);
97+
98+
await expect(mppHandler.executeAutonomousRequest(agent, requestFn, intentMandate))
99+
.rejects.toThrow('Incomplete payment requirements in 402 response');
100+
});
101+
102+
it('should handle 402 thrown as an error (e.g. from axios)', async () => {
103+
const agent = { id: 'agent_123', name: 'TestAgent' };
104+
const intentMandate = await mockMandateService.issueIntentMandate({
105+
userDid: 'did:key:user',
106+
agentDid: 'did:key:agent',
107+
maxBudget: 1000
108+
});
109+
110+
const error402 = new Error('Payment Required');
111+
error402.response = {
112+
status: 402,
113+
headers: {
114+
'x-mpp-amount': '500',
115+
'x-mpp-merchant-did': 'did:key:merchant'
116+
}
117+
};
118+
119+
const finalResponse = { status: 200, data: 'paid' };
120+
const requestFn = jest.fn()
121+
.mockRejectedValueOnce(error402)
122+
.mockResolvedValueOnce(finalResponse);
123+
124+
const result = await mppHandler.executeAutonomousRequest(agent, requestFn, intentMandate);
125+
126+
expect(result).toBe(finalResponse);
127+
expect(requestFn).toHaveBeenCalledTimes(2);
128+
});
129+
});
130+
});

0 commit comments

Comments
 (0)