diff --git a/.changeset/validate-receipt-payment-option.md b/.changeset/validate-receipt-payment-option.md new file mode 100644 index 0000000..5e42480 --- /dev/null +++ b/.changeset/validate-receipt-payment-option.md @@ -0,0 +1,5 @@ +--- +"@agentcommercekit/ack-pay": patch +--- + +Validate that a PaymentReceipt paymentOptionId matches an option from the verified Payment Request token. Exports `InvalidPaymentReceiptError` so callers can catch receipt validation failures explicitly. diff --git a/packages/ack-pay/src/errors.ts b/packages/ack-pay/src/errors.ts index 3765649..b030349 100644 --- a/packages/ack-pay/src/errors.ts +++ b/packages/ack-pay/src/errors.ts @@ -4,3 +4,10 @@ export class InvalidPaymentRequestTokenError extends Error { this.name = "InvalidPaymentRequestTokenError" } } + +export class InvalidPaymentReceiptError extends Error { + constructor(message = "Invalid payment receipt") { + super(message) + this.name = "InvalidPaymentReceiptError" + } +} diff --git a/packages/ack-pay/src/verify-payment-receipt.test.ts b/packages/ack-pay/src/verify-payment-receipt.test.ts index 4705b55..53962e0 100644 --- a/packages/ack-pay/src/verify-payment-receipt.test.ts +++ b/packages/ack-pay/src/verify-payment-receipt.test.ts @@ -22,7 +22,10 @@ import { beforeEach, describe, expect, it } from "vitest" import { createPaymentReceipt } from "./create-payment-receipt" import { createSignedPaymentRequest } from "./create-signed-payment-request" -import { InvalidPaymentRequestTokenError } from "./errors" +import { + InvalidPaymentReceiptError, + InvalidPaymentRequestTokenError, +} from "./errors" import type { PaymentRequestInit } from "./payment-request" import { verifyPaymentReceipt } from "./verify-payment-receipt" @@ -31,11 +34,12 @@ describe("verifyPaymentReceipt()", () => { let unsignedReceipt: W3CCredential let signedReceipt: Verifiable let signedReceiptJwt: JwtString + let receiptIssuerKeypair: Awaited> let receiptIssuerDid: DidUri let paymentRequestIssuerDid: DidUri beforeEach(async () => { - const receiptIssuerKeypair = await generateKeypair("secp256k1") + receiptIssuerKeypair = await generateKeypair("secp256k1") receiptIssuerDid = createDidKeyUri(receiptIssuerKeypair) const paymentRequestIssuerKeypair = await generateKeypair("secp256k1") paymentRequestIssuerDid = createDidKeyUri(paymentRequestIssuerKeypair) @@ -144,6 +148,25 @@ describe("verifyPaymentReceipt()", () => { ).rejects.toThrow(InvalidPaymentRequestTokenError) }) + it("throws when the receipt payment option is not in the payment request", async () => { + const mismatchedReceipt = { + ...unsignedReceipt, + credentialSubject: { + ...unsignedReceipt.credentialSubject, + paymentOptionId: "missing-payment-option-id", + }, + } + + const mismatchedReceiptJwt = await signCredential(mismatchedReceipt, { + did: receiptIssuerDid, + signer: createJwtSigner(receiptIssuerKeypair), + }) + + await expect( + verifyPaymentReceipt(mismatchedReceiptJwt, { resolver }), + ).rejects.toThrow(InvalidPaymentReceiptError) + }) + it("validates trusted receipt issuers", async () => { const result = await verifyPaymentReceipt(signedReceiptJwt, { resolver, diff --git a/packages/ack-pay/src/verify-payment-receipt.ts b/packages/ack-pay/src/verify-payment-receipt.ts index 3359ed8..a5bf01e 100644 --- a/packages/ack-pay/src/verify-payment-receipt.ts +++ b/packages/ack-pay/src/verify-payment-receipt.ts @@ -10,6 +10,7 @@ import { type W3CCredential, } from "@agentcommercekit/vc" +import { InvalidPaymentReceiptError } from "./errors" import type { PaymentRequest } from "./payment-request" import { getReceiptClaimVerifier, @@ -116,6 +117,18 @@ export async function verifyPaymentReceipt( }, ) + const receiptPaymentOptionId = + parsedCredential.credentialSubject.paymentOptionId + const paymentOptionExists = paymentRequest.paymentOptions.some( + (paymentOption) => paymentOption.id === receiptPaymentOptionId, + ) + + if (!paymentOptionExists) { + throw new InvalidPaymentReceiptError( + "Receipt paymentOptionId does not match any payment option in the Payment Request token", + ) + } + return { receipt: parsedCredential, paymentRequestToken,