From aeb39680ad608fa8d11fb5512551332819cbb8d8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 5 Sep 2026 21:35:32 +0000 Subject: [PATCH 1/2] fix(ack-pay): reject empty payment request and option fields Require non-empty strings for payment request id and payment option id/currency/recipient (plus receipt claim paymentOptionId) in both Valibot and Zod schemas. Empty identifiers were previously accepted despite other fields already validating more strictly. --- .../ack-pay-reject-empty-payment-fields.md | 7 +++ packages/ack-pay/src/schemas/schemas.test.ts | 54 ++++++++++++++++++- packages/ack-pay/src/schemas/valibot.ts | 11 ++-- packages/ack-pay/src/schemas/zod.ts | 11 ++-- 4 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 .changeset/ack-pay-reject-empty-payment-fields.md diff --git a/.changeset/ack-pay-reject-empty-payment-fields.md b/.changeset/ack-pay-reject-empty-payment-fields.md new file mode 100644 index 00000000..de772413 --- /dev/null +++ b/.changeset/ack-pay-reject-empty-payment-fields.md @@ -0,0 +1,7 @@ +--- +"@agentcommercekit/ack-pay": patch +--- + +Reject empty payment request and payment option identifiers. + +`paymentOptionSchema` accepted empty strings for `id`, `currency`, and `recipient`, and `paymentRequestSchema` / `paymentReceiptClaimSchema` accepted empty request and option ids. Require non-empty strings for these fields in both the Valibot and Zod schemas so HTTP 402 bodies and verified payment request tokens cannot carry blank identifiers. diff --git a/packages/ack-pay/src/schemas/schemas.test.ts b/packages/ack-pay/src/schemas/schemas.test.ts index 735b3ead..f97113f6 100644 --- a/packages/ack-pay/src/schemas/schemas.test.ts +++ b/packages/ack-pay/src/schemas/schemas.test.ts @@ -1,8 +1,14 @@ import * as v from "valibot" import { describe, expect, it } from "vitest" -import { paymentRequestSchema as valibotPaymentRequestSchema } from "./valibot" -import { paymentRequestSchema as zodPaymentRequestSchema } from "./zod" +import { + paymentOptionSchema as valibotPaymentOptionSchema, + paymentRequestSchema as valibotPaymentRequestSchema, +} from "./valibot" +import { + paymentOptionSchema as zodPaymentOptionSchema, + paymentRequestSchema as zodPaymentRequestSchema, +} from "./zod" const paymentRequest = { id: "test-payment-request-id", @@ -17,6 +23,8 @@ const paymentRequest = { ], } +const paymentOption = paymentRequest.paymentOptions[0] + describe("paymentRequestSchema", () => { it("rejects invalid expiresAt strings instead of throwing", () => { const input = { @@ -44,3 +52,45 @@ describe("paymentRequestSchema", () => { } }) }) + +describe.each([ + [ + "valibot", + { + paymentRequest: (input: unknown) => + v.safeParse(valibotPaymentRequestSchema, input).success, + paymentOption: (input: unknown) => + v.safeParse(valibotPaymentOptionSchema, input).success, + }, + ], + [ + "zod", + { + paymentRequest: (input: unknown) => + zodPaymentRequestSchema.safeParse(input).success, + paymentOption: (input: unknown) => + zodPaymentOptionSchema.safeParse(input).success, + }, + ], +] as const)("%s rejects empty required payment fields", (_name, schema) => { + it.each(["id", "currency", "recipient"] as const)( + "rejects a payment option with an empty %s", + (field) => { + expect( + schema.paymentOption({ + ...paymentOption, + [field]: "", + }), + ).toBe(false) + }, + ) + + it("rejects a payment request with an empty id", () => { + expect( + schema.paymentRequest({ + ...paymentRequest, + id: "", + }), + ).toBe(false) + }) +}) diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index 75fc9807..41c6a73a 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -3,6 +3,7 @@ import { jwtStringSchema } from "@agentcommercekit/jwt/schemas/valibot" import * as v from "valibot" const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema]) +const nonEmptyString = v.pipe(v.string(), v.minLength(1)) const positiveIntegerString = v.pipe(v.string(), v.regex(/^[1-9]\d*$/)) const timestampSchema = v.pipe( @@ -12,21 +13,21 @@ const timestampSchema = v.pipe( ) export const paymentOptionSchema = v.object({ - id: v.string(), + id: nonEmptyString, amount: v.union([ v.pipe(v.number(), v.integer(), v.gtValue(0)), positiveIntegerString, ]), decimals: v.pipe(v.number(), v.integer(), v.toMinValue(0)), - currency: v.string(), - recipient: v.string(), + currency: nonEmptyString, + recipient: nonEmptyString, network: v.optional(v.string()), paymentService: v.optional(urlOrDidUri), receiptService: v.optional(urlOrDidUri), }) export const paymentRequestSchema = v.object({ - id: v.string(), + id: nonEmptyString, description: v.optional(v.string()), serviceCallback: v.optional(v.pipe(v.string(), v.url())), expiresAt: v.optional(timestampSchema), @@ -38,6 +39,6 @@ export const paymentRequestSchema = v.object({ export const paymentReceiptClaimSchema = v.object({ paymentRequestToken: jwtStringSchema, - paymentOptionId: v.string(), + paymentOptionId: nonEmptyString, metadata: v.optional(v.record(v.string(), v.unknown())), }) diff --git a/packages/ack-pay/src/schemas/zod.ts b/packages/ack-pay/src/schemas/zod.ts index 05cfdca8..2e0f1458 100644 --- a/packages/ack-pay/src/schemas/zod.ts +++ b/packages/ack-pay/src/schemas/zod.ts @@ -3,6 +3,7 @@ import { jwtStringSchema } from "@agentcommercekit/jwt/schemas/zod" import * as z from "zod" const urlOrDidUri = z.union([z.url(), didUriSchema]) +const nonEmptyString = z.string().min(1) const positiveIntegerString = z.string().regex(/^[1-9]\d*$/) const timestampSchema = z @@ -22,18 +23,18 @@ const timestampSchema = z }) export const paymentOptionSchema = z.object({ - id: z.string(), + id: nonEmptyString, amount: z.union([z.number().int().positive(), positiveIntegerString]), decimals: z.number().int().nonnegative(), - currency: z.string(), - recipient: z.string(), + currency: nonEmptyString, + recipient: nonEmptyString, network: z.string().optional(), paymentService: urlOrDidUri.optional(), receiptService: urlOrDidUri.optional(), }) export const paymentRequestSchema = z.object({ - id: z.string(), + id: nonEmptyString, description: z.string().optional(), serviceCallback: z.url().optional(), expiresAt: timestampSchema.optional(), @@ -42,6 +43,6 @@ export const paymentRequestSchema = z.object({ export const paymentReceiptClaimSchema = z.object({ paymentRequestToken: jwtStringSchema, - paymentOptionId: z.string(), + paymentOptionId: nonEmptyString, metadata: z.record(z.string(), z.unknown()).optional(), }) From b19cd1ddb7d9c79e14551af8bbba91a314aefd86 Mon Sep 17 00:00:00 2001 From: KutluhanETH Date: Mon, 7 Sep 2026 00:01:18 +0000 Subject: [PATCH 2/2] test(ack-pay): cover empty paymentOptionId rejection Add Valibot and Zod adapters for paymentReceiptClaimSchema and assert empty paymentOptionId is rejected, per CodeRabbit review on #203. --- packages/ack-pay/src/schemas/schemas.test.ts | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/ack-pay/src/schemas/schemas.test.ts b/packages/ack-pay/src/schemas/schemas.test.ts index f97113f6..92c17c72 100644 --- a/packages/ack-pay/src/schemas/schemas.test.ts +++ b/packages/ack-pay/src/schemas/schemas.test.ts @@ -3,10 +3,12 @@ import { describe, expect, it } from "vitest" import { paymentOptionSchema as valibotPaymentOptionSchema, + paymentReceiptClaimSchema as valibotPaymentReceiptClaimSchema, paymentRequestSchema as valibotPaymentRequestSchema, } from "./valibot" import { paymentOptionSchema as zodPaymentOptionSchema, + paymentReceiptClaimSchema as zodPaymentReceiptClaimSchema, paymentRequestSchema as zodPaymentRequestSchema, } from "./zod" @@ -25,6 +27,14 @@ const paymentRequest = { const paymentOption = paymentRequest.paymentOptions[0] +const paymentRequestToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + +const paymentReceiptClaim = { + paymentRequestToken, + paymentOptionId: "test-payment-option-id", +} + describe("paymentRequestSchema", () => { it("rejects invalid expiresAt strings instead of throwing", () => { const input = { @@ -61,6 +71,8 @@ describe.each([ v.safeParse(valibotPaymentRequestSchema, input).success, paymentOption: (input: unknown) => v.safeParse(valibotPaymentOptionSchema, input).success, + paymentReceiptClaim: (input: unknown) => + v.safeParse(valibotPaymentReceiptClaimSchema, input).success, }, ], [ @@ -70,6 +82,8 @@ describe.each([ zodPaymentRequestSchema.safeParse(input).success, paymentOption: (input: unknown) => zodPaymentOptionSchema.safeParse(input).success, + paymentReceiptClaim: (input: unknown) => + zodPaymentReceiptClaimSchema.safeParse(input).success, }, ], ] as const)("%s rejects empty required payment fields", (_name, schema) => { @@ -93,4 +107,13 @@ describe.each([ }), ).toBe(false) }) + + it("rejects a payment receipt claim with an empty paymentOptionId", () => { + expect( + schema.paymentReceiptClaim({ + ...paymentReceiptClaim, + paymentOptionId: "", + }), + ).toBe(false) + }) })