Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/verify_ucp_flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class DBAdapter {
async createWallet(data) { return Wallet.create(data); }
async createTransaction(data) { return Transaction.create(data); }
async updateTransaction(id, data) { return Transaction.findByIdAndUpdate(id, data, { new: true }); }
async findAgentById(id) { return Agent.findById(id); }

async updateWalletBalance(walletId, amount) {
return Wallet.findByIdAndUpdate(walletId, { $inc: { balance: amount } }, { new: true });
Expand Down
51 changes: 9 additions & 42 deletions src/services/tokenization.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,53 +331,20 @@ class TokenizationService {
async signWithToken(tokenId, dataToSign, mandate, context = {}) {
// Zero Trust Validation: Verify mandate BEFORE entering try/catch simulation block
if (mandate) {
let decodedMandate;
try {
decodedMandate = await this.mandateService.verifyMandate(mandate);
// Normalize context for MandateService (merchant -> recipient)
const validationContext = {
...context,
recipient: context.recipient || context.merchant,
};
await this.mandateService.verifyMandate(mandate, validationContext);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merchant error says Recipient

Medium Severity

The mandateService.verifyMandate function now reports unauthorized counterparties as "Recipient ... not authorized by mandate". This change in error message phrasing from "Merchant" breaks existing tests and expectations.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f223810. Configure here.

} catch (error) {
if (error.message?.includes("jwt expired")) {
throw new Error("Zero Trust Validation Failed: Mandate has expired");
}
if (error.message?.includes("Zero Trust Validation Failed:")) {
throw error;
}
throw new Error(`Zero Trust Validation Failed: ${error.message || error}`);
}

// Validate budget if context amount is provided
if (context.amount) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expired mandate message changed

Medium Severity

The signWithToken refactor alters mandate validation error messages. Expired mandates now return generic JWT errors instead of "Mandate has expired", and unauthorized counterparty errors refer to "Recipient" instead of "Merchant". This breaks existing error message contracts and tests.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f223810. Configure here.

// Check Intent Mandate budget
if (
decodedMandate.max_budget &&
context.amount > decodedMandate.max_budget.value
) {
throw new Error(
`Zero Trust Validation Failed: Amount ${context.amount} exceeds mandate budget of ${decodedMandate.max_budget.value}`,
);
}
// Check Cart Mandate total price
if (
decodedMandate.total_price &&
context.amount !== decodedMandate.total_price
) {
throw new Error(
`Zero Trust Validation Failed: Amount ${context.amount} does not match cart mandate total of ${decodedMandate.total_price}`,
);
}
}

// Validate merchant if context merchant is provided
if (context.merchant && decodedMandate.allowed_merchants?.length > 0) {
if (!decodedMandate.allowed_merchants.includes(context.merchant)) {
throw new Error(
`Zero Trust Validation Failed: Merchant ${context.merchant} not authorized by mandate`,
);
}
}

// Validate expiration
if (decodedMandate.exp < Math.floor(Date.now() / 1000)) {
throw new Error("Zero Trust Validation Failed: Mandate has expired");
throw new Error(
`Zero Trust Validation Failed: ${error.message || error}`,
);
}
} else if (this.strictMandateMode) {
throw new Error(
Expand Down
87 changes: 87 additions & 0 deletions tests/unit/a2a_zerotrust.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const MandateService = require('../../src/services/mandate');
const A2AService = require('../../src/services/a2aService');

describe('A2AService Zero Trust Validation', () => {
let a2aService;
let mandateService;
let mockWalletService;
let mockDb;

beforeEach(() => {
mockWalletService = {
transfer: jest.fn().mockResolvedValue({ transferId: 'tx_123' })
};
mockDb = {
findAgentById: jest.fn().mockImplementation((id) => ({
id,
name: `Agent ${id}`,
status: 'active',
walletId: `wallet_${id}`,
config: { limits: { perTransaction: 1000 } }
}))
};
mandateService = new MandateService({ signingKey: 'test-secret' });
a2aService = new A2AService(mockWalletService, mockDb, {
mandateConfig: { signingKey: 'test-secret' },
strictMandateMode: true
});
});

it('should fail if mandate is missing in strict mode', async () => {
await expect(a2aService.executeTransfer({
fromAgentId: 'agent1',
toAgentId: 'agent2',
amount: 100
})).rejects.toThrow('Zero Trust Validation Failed: Mandate required for A2A transfer in strict mode');
});

it('should fail if mandate budget is exceeded', async () => {
const mandate = await mandateService.issueIntentMandate({
userDid: 'did:user:1',
agentDid: 'did:agent:1',
maxBudget: 50
});

await expect(a2aService.executeTransfer({
fromAgentId: 'agent1',
toAgentId: 'agent2',
amount: 100,
mandate
})).rejects.toThrow('Zero Trust Validation Failed: Amount 100 exceeds mandate budget of 50');
});

it('should fail if recipient is not authorized by mandate', async () => {
const mandate = await mandateService.issueIntentMandate({
userDid: 'did:user:1',
agentDid: 'did:agent:1',
maxBudget: 200,
allowedMerchants: ['agent2']
});

await expect(a2aService.executeTransfer({
fromAgentId: 'agent1',
toAgentId: 'agent3',
amount: 100,
mandate
})).rejects.toThrow('Zero Trust Validation Failed: Merchant agent3 not authorized by mandate');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A2A tests lack implementation

High Severity

The A2AService.executeTransfer method does not pass amount and recipient to verifyMandate. This prevents enforcement of new zero-trust budget and allowed-counterparty validations, allowing mandated transfers to bypass fiduciary checks.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f223810. Configure here.

});

it('should succeed with valid mandate', async () => {
const mandate = await mandateService.issueIntentMandate({
userDid: 'did:user:1',
agentDid: 'did:agent:1',
maxBudget: 200,
allowedMerchants: ['agent2']
});

const result = await a2aService.executeTransfer({
fromAgentId: 'agent1',
toAgentId: 'agent2',
amount: 100,
mandate
});

expect(result.success).toBe(true);
expect(mockWalletService.transfer).toHaveBeenCalled();
});
});
Loading