diff --git a/scripts/ocp-cli.js b/scripts/ocp-cli.js index 288e5f2..8571259 100644 --- a/scripts/ocp-cli.js +++ b/scripts/ocp-cli.js @@ -154,7 +154,7 @@ program.command('x402:settle') return; } } else if (process.env.STRICT_MANDATE_MODE === 'true') { - console.error(`Error: Mandate required for x402 settlement in STRICT_MANDATE_MODE.`); + console.error(`Zero Trust Validation Failed: Mandate required for x402 settlement in STRICT_MANDATE_MODE.`); return; } diff --git a/src/services/a2aService.js b/src/services/a2aService.js index 3c5c9d9..2b9125d 100644 --- a/src/services/a2aService.js +++ b/src/services/a2aService.js @@ -7,6 +7,7 @@ const MandateService = require("./mandate"); const logger = require("../utils/logger"); +const MandateService = require("./mandate"); class A2AService { constructor(walletService, db, config = {}) { @@ -67,10 +68,22 @@ class A2AService { ); } - // 2. Policy Checks (Sender) + // 2. Zero Trust Mandate Validation + if (mandate) { + await this.mandateService.verifyMandate(mandate, { + amount, + recipient: toAgentId, + }); + } else if (this.strictMandateMode) { + throw new Error( + "Zero Trust Validation Failed: Mandate required for A2A transfer in strict mode", + ); + } + + // 3. Policy Checks (Sender) await this._validateAgentPolicy(fromAgent, toAgentId, amount); - // 3. Execute Wallet Transfer + // 4. Execute Wallet Transfer const transferResult = await this.walletService.transfer({ fromWalletId: fromAgent.walletId, toWalletId: toAgent.walletId, @@ -136,6 +149,15 @@ class A2AService { */ _handleError(method, error) { logger.error(`A2AService.${method} error:`, error); + + // Normalize Zero Trust errors + if ( + error.message && + error.message.includes("Zero Trust Validation Failed:") + ) { + return error; + } + return error instanceof Error ? error : new Error(error); } } diff --git a/src/services/tokenization.js b/src/services/tokenization.js index 924f3f0..441e2f3 100644 --- a/src/services/tokenization.js +++ b/src/services/tokenization.js @@ -382,6 +382,14 @@ class TokenizationService { _handleError(method, error) { logger.error(`TokenizationService.${method} error:`, error); + // Normalize Zero Trust errors + if ( + error.message && + error.message.includes("Zero Trust Validation Failed:") + ) { + return error; + } + if (error.response) { const { status, data } = error.response; return new Error( @@ -393,7 +401,7 @@ class TokenizationService { return new Error("Tokenization service unavailable"); } - return error; + return error instanceof Error ? error : new Error(error); } } diff --git a/tests/unit/mandate_context.spec.js b/tests/unit/mandate_context.spec.js new file mode 100644 index 0000000..b4240a1 --- /dev/null +++ b/tests/unit/mandate_context.spec.js @@ -0,0 +1,65 @@ +const MandateService = require("../../src/services/mandate"); +const jwt = require("jsonwebtoken"); + +describe("MandateService - Context Validation", () => { + let mandateService; + const signingKey = "test-secret"; + + beforeEach(() => { + mandateService = new MandateService({ signingKey }); + }); + + it("should validate amount against intent mandate budget", async () => { + const mandate = await mandateService.issueIntentMandate({ + userDid: "did:key:user", + agentDid: "did:key:agent", + maxBudget: 100, + }); + + // Valid amount + await expect( + mandateService.verifyMandate(mandate, { amount: 50 }), + ).resolves.toBeDefined(); + + // Invalid amount + await expect( + mandateService.verifyMandate(mandate, { amount: 150 }), + ).rejects.toThrow( + "Zero Trust Validation Failed: Amount 150 exceeds mandate budget of 100", + ); + }); + + it("should validate recipient against allowed_merchants whitelist", async () => { + const mandate = await mandateService.issueIntentMandate({ + userDid: "did:key:user", + agentDid: "did:key:agent", + maxBudget: 100, + allowedMerchants: ["did:key:merchant-1"], + }); + + // Authorized merchant + await expect( + mandateService.verifyMandate(mandate, { recipient: "did:key:merchant-1" }), + ).resolves.toBeDefined(); + + // Unauthorized merchant + await expect( + mandateService.verifyMandate(mandate, { recipient: "did:key:merchant-2" }), + ).rejects.toThrow( + "Zero Trust Validation Failed: Merchant did:key:merchant-2 not authorized by mandate", + ); + }); + + it("should handle expired tokens with correct prefix", async () => { + const mandate = jwt.sign( + { + exp: Math.floor(Date.now() / 1000) - 60, + }, + signingKey, + ); + + await expect(mandateService.verifyMandate(mandate)).rejects.toThrow( + "Zero Trust Validation Failed: Mandate has expired", + ); + }); +});