From d5c2cabe1caa3ecf9f1b0de4dd0629fb3c288bdc Mon Sep 17 00:00:00 2001 From: crazywriter1 Date: Fri, 28 Aug 2026 21:21:52 +0300 Subject: [PATCH] fix(ack-pay): reject empty payment option identifiers paymentOptionSchema accepted empty strings for id, currency, and recipient, and paymentRequestSchema accepted an empty request id, even though other fields such as amount already reject invalid values. Require a non-empty string for these fields in both the valibot and zod schemas. --- .changeset/payment-option-empty-fields.md | 10 +++++ packages/ack-pay/src/schemas.test.ts | 50 ++++++++++++++++++++++- packages/ack-pay/src/schemas/valibot.ts | 10 +++-- packages/ack-pay/src/schemas/zod.ts | 10 +++-- 4 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 .changeset/payment-option-empty-fields.md diff --git a/.changeset/payment-option-empty-fields.md b/.changeset/payment-option-empty-fields.md new file mode 100644 index 00000000..ca60103f --- /dev/null +++ b/.changeset/payment-option-empty-fields.md @@ -0,0 +1,10 @@ +--- +"@agentcommercekit/ack-pay": patch +--- + +Reject empty payment request and payment option identifiers. + +`paymentOptionSchema` accepted empty strings for `id`, `currency`, and +`recipient`, and `paymentRequestSchema` accepted an empty request `id`, even +though other fields such as `amount` already reject invalid values. Require a +non-empty string for these fields in both the valibot and zod schemas. diff --git a/packages/ack-pay/src/schemas.test.ts b/packages/ack-pay/src/schemas.test.ts index 802b45c8..baf38667 100644 --- a/packages/ack-pay/src/schemas.test.ts +++ b/packages/ack-pay/src/schemas.test.ts @@ -1,8 +1,14 @@ import * as v from "valibot" import { describe, expect, it } from "vitest" -import { paymentRequestSchema as valibotPaymentRequestSchema } from "./schemas/valibot" -import { paymentRequestSchema as zodPaymentRequestSchema } from "./schemas/zod" +import { + paymentOptionSchema as valibotPaymentOptionSchema, + paymentRequestSchema as valibotPaymentRequestSchema, +} from "./schemas/valibot" +import { + paymentOptionSchema as zodPaymentOptionSchema, + paymentRequestSchema as zodPaymentRequestSchema, +} from "./schemas/zod" const paymentRequest = { id: "test-payment-request-id", @@ -17,6 +23,46 @@ const paymentRequest = { ], } +const paymentOption = paymentRequest.paymentOptions[0] + +const validators = { + 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 + +describe.each(Object.entries(validators))("%s payment schemas", (_, 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) + }) +}) + describe("paymentRequestSchema", () => { it("rejects invalid expiresAt strings instead of throwing", () => { const input = { diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index d97ed94d..e2c43c34 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -4,6 +4,8 @@ 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 timestampSchema = v.pipe( v.union([v.date(), v.string()]), v.check((input) => !Number.isNaN(new Date(input).getTime()), "Invalid date"), @@ -11,18 +13,18 @@ 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)), v.string()]), 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), diff --git a/packages/ack-pay/src/schemas/zod.ts b/packages/ack-pay/src/schemas/zod.ts index 0f676fcb..e6b77031 100644 --- a/packages/ack-pay/src/schemas/zod.ts +++ b/packages/ack-pay/src/schemas/zod.ts @@ -4,6 +4,8 @@ import * as z from "zod" const urlOrDidUri = z.union([z.url(), didUriSchema]) +const nonEmptyString = z.string().min(1) + const timestampSchema = z .union([z.date(), z.string()]) .transform((val, ctx) => { @@ -21,18 +23,18 @@ const timestampSchema = z }) export const paymentOptionSchema = z.object({ - id: z.string(), + id: nonEmptyString, amount: z.union([z.number().int().positive(), z.string()]), 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(),