diff --git a/.changeset/payment-option-empty-fields.md b/.changeset/payment-option-empty-fields.md new file mode 100644 index 0000000..ca60103 --- /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/payment-option.test.ts b/packages/ack-pay/src/schemas/payment-option.test.ts index 166b449..cfe3105 100644 --- a/packages/ack-pay/src/schemas/payment-option.test.ts +++ b/packages/ack-pay/src/schemas/payment-option.test.ts @@ -35,3 +35,24 @@ describe("paymentOptionSchema amount", () => { }, ) }) + +describe.each([ + [ + "valibot", + (input: unknown) => v.safeParse(valibotPaymentOptionSchema, input).success, + ], + ["zod", (input: unknown) => zodPaymentOptionSchema.safeParse(input).success], +] as const)("%s paymentOptionSchema empty fields", (_, accepts) => { + it.each(["id", "currency", "recipient"] as const)( + "rejects a payment option with an empty %s", + (field) => { + expect( + accepts({ + ...paymentOption, + amount: 1, + [field]: "", + }), + ).toBe(false) + }, + ) +}) diff --git a/packages/ack-pay/src/schemas/schemas.test.ts b/packages/ack-pay/src/schemas/schemas.test.ts index 735b3ea..3359d83 100644 --- a/packages/ack-pay/src/schemas/schemas.test.ts +++ b/packages/ack-pay/src/schemas/schemas.test.ts @@ -17,6 +17,26 @@ const paymentRequest = { ], } +const validators = { + valibot: (input: unknown) => + v.safeParse(valibotPaymentRequestSchema, input).success, + zod: (input: unknown) => zodPaymentRequestSchema.safeParse(input).success, +} as const + +describe.each(Object.entries(validators))( + "%s paymentRequestSchema", + (_, accepts) => { + it("rejects a payment request with an empty id", () => { + expect( + accepts({ + ...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 75fc980..392b67f 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -5,6 +5,8 @@ import * as v from "valibot" const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema]) const positiveIntegerString = v.pipe(v.string(), v.regex(/^[1-9]\d*$/)) +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"), @@ -12,21 +14,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), diff --git a/packages/ack-pay/src/schemas/zod.ts b/packages/ack-pay/src/schemas/zod.ts index 05cfdca..6a53974 100644 --- a/packages/ack-pay/src/schemas/zod.ts +++ b/packages/ack-pay/src/schemas/zod.ts @@ -5,6 +5,8 @@ import * as z from "zod" const urlOrDidUri = z.union([z.url(), didUriSchema]) const positiveIntegerString = z.string().regex(/^[1-9]\d*$/) +const nonEmptyString = z.string().min(1) + const timestampSchema = z .union([z.date(), z.string()]) .transform((val, ctx) => { @@ -22,18 +24,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(),