From c28c1e307dd06e84e061985cf679cc9203d17515 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Fri, 15 May 2026 13:04:24 +0300 Subject: [PATCH 1/2] fix(ack-pay): validate receipt payment option --- .changeset/validate-receipt-payment-option.md | 5 ++++ packages/ack-pay/src/errors.ts | 7 +++++ .../src/verify-payment-receipt.test.ts | 27 +++++++++++++++++-- .../ack-pay/src/verify-payment-receipt.ts | 13 +++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 .changeset/validate-receipt-payment-option.md diff --git a/.changeset/validate-receipt-payment-option.md b/.changeset/validate-receipt-payment-option.md new file mode 100644 index 0000000..744509a --- /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. 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, From 5a4cb46bed95edd0ceb3776d2d8fb32b7ba41f6a Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Sat, 16 May 2026 07:13:32 +0300 Subject: [PATCH 2/2] docs: mention receipt validation error Signed-off-by: EfeDurmaz16 --- .changeset/validate-receipt-payment-option.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/validate-receipt-payment-option.md b/.changeset/validate-receipt-payment-option.md index 744509a..5e42480 100644 --- a/.changeset/validate-receipt-payment-option.md +++ b/.changeset/validate-receipt-payment-option.md @@ -2,4 +2,4 @@ "@agentcommercekit/ack-pay": patch --- -Validate that a PaymentReceipt paymentOptionId matches an option from the verified Payment Request token. +Validate that a PaymentReceipt paymentOptionId matches an option from the verified Payment Request token. Exports `InvalidPaymentReceiptError` so callers can catch receipt validation failures explicitly.