diff --git a/.changeset/decimals-clamp-fix.md b/.changeset/decimals-clamp-fix.md new file mode 100644 index 00000000..071ae4d8 --- /dev/null +++ b/.changeset/decimals-clamp-fix.md @@ -0,0 +1,16 @@ +--- +"@agentcommercekit/ack-pay": patch +--- + +Fix `paymentOptionSchema`'s `decimals` field silently clamping negative +values instead of rejecting them (valibot schema) + +The valibot version of `paymentOptionSchema` used `v.toMinValue(0)` on the +`decimals` field, which is a **transform** that silently clamps a negative +number up to `0` rather than a validator that rejects it. This diverged from +the zod version of the same schema (`z.number().int().nonnegative()`), which +correctly rejects negative values. + +Switched to `v.minValue(0)`, valibot's validating counterpart, so a payment +option with a malformed negative `decimals` value is now rejected by both +schema implementations instead of being silently "fixed" by the valibot one. diff --git a/packages/ack-pay/src/schemas/decimals.test.ts b/packages/ack-pay/src/schemas/decimals.test.ts new file mode 100644 index 00000000..b17b206f --- /dev/null +++ b/packages/ack-pay/src/schemas/decimals.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest" +import * as v from "valibot" + +import { paymentOptionSchema as valibotPaymentOptionSchema } from "./valibot" +import { paymentOptionSchema as zodPaymentOptionSchema } from "./zod" + +function baseOption(decimals: number) { + return { + id: "opt-1", + amount: 100, + decimals, + currency: "USD", + recipient: "did:example:recipient", + } +} + +describe("paymentOptionSchema decimals", () => { + it("valibot rejects a negative decimals value instead of clamping it to 0", () => { + const result = v.safeParse(valibotPaymentOptionSchema, baseOption(-5)) + + expect(result.success).toBe(false) + }) + + it("zod rejects a negative decimals value", () => { + const result = zodPaymentOptionSchema.safeParse(baseOption(-5)) + + expect(result.success).toBe(false) + }) + + it("valibot and zod agree: zero and positive decimals are valid", () => { + for (const decimals of [0, 2, 18]) { + expect( + v.safeParse(valibotPaymentOptionSchema, baseOption(decimals)).success, + ).toBe(true) + expect( + zodPaymentOptionSchema.safeParse(baseOption(decimals)).success, + ).toBe(true) + } + }) +}) diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index 06a58f1e..dfb5bcc7 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -7,7 +7,7 @@ const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema]) export const paymentOptionSchema = v.object({ id: v.string(), amount: v.union([v.pipe(v.number(), v.integer(), v.gtValue(0)), v.string()]), - decimals: v.pipe(v.number(), v.integer(), v.toMinValue(0)), + decimals: v.pipe(v.number(), v.integer(), v.minValue(0)), currency: v.string(), recipient: v.string(), network: v.optional(v.string()),