diff --git a/.changeset/clean-amount-strings.md b/.changeset/clean-amount-strings.md new file mode 100644 index 00000000..86fe130d --- /dev/null +++ b/.changeset/clean-amount-strings.md @@ -0,0 +1,5 @@ +--- +"@agentcommercekit/ack-pay": patch +--- + +Reject malformed, fractional, zero, and negative string payment amounts while preserving positive integer strings for values larger than JavaScript's safe integer range. diff --git a/packages/ack-pay/src/schemas/payment-option.test.ts b/packages/ack-pay/src/schemas/payment-option.test.ts new file mode 100644 index 00000000..166b4496 --- /dev/null +++ b/packages/ack-pay/src/schemas/payment-option.test.ts @@ -0,0 +1,37 @@ +import * as v from "valibot" +import { describe, expect, it } from "vitest" + +import { paymentOptionSchema as valibotPaymentOptionSchema } from "./valibot" +import { paymentOptionSchema as zodPaymentOptionSchema } from "./zod" + +const paymentOption = { + id: "test-payment-option-id", + decimals: 2, + currency: "USD", + recipient: "did:example:recipient", +} + +function acceptsAmount(amount: number | string) { + const value = { ...paymentOption, amount } + + return { + valibot: v.safeParse(valibotPaymentOptionSchema, value).success, + zod: zodPaymentOptionSchema.safeParse(value).success, + } +} + +describe("paymentOptionSchema amount", () => { + it.each([1, 100, "1", "100", "9007199254740993"])( + "accepts positive integer amount %s", + (amount) => { + expect(acceptsAmount(amount)).toEqual({ valibot: true, zod: true }) + }, + ) + + it.each([0, -1, 1.5, "", "0", "-1", "1.5", "abc"])( + "rejects invalid amount %s", + (amount) => { + expect(acceptsAmount(amount)).toEqual({ valibot: false, zod: false }) + }, + ) +}) diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index 06a58f1e..fc04172e 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -3,10 +3,14 @@ import { jwtStringSchema } from "@agentcommercekit/jwt/schemas/valibot" 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*$/)) export const paymentOptionSchema = v.object({ id: v.string(), - amount: v.union([v.pipe(v.number(), v.integer(), v.gtValue(0)), v.string()]), + 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(), diff --git a/packages/ack-pay/src/schemas/zod.ts b/packages/ack-pay/src/schemas/zod.ts index 66ceffd9..40cec4e2 100644 --- a/packages/ack-pay/src/schemas/zod.ts +++ b/packages/ack-pay/src/schemas/zod.ts @@ -3,10 +3,11 @@ import { jwtStringSchema } from "@agentcommercekit/jwt/schemas/zod" import * as z from "zod" const urlOrDidUri = z.union([z.url(), didUriSchema]) +const positiveIntegerString = z.string().regex(/^[1-9]\d*$/) export const paymentOptionSchema = z.object({ id: z.string(), - amount: z.union([z.number().int().positive(), z.string()]), + amount: z.union([z.number().int().positive(), positiveIntegerString]), decimals: z.number().int().nonnegative(), currency: z.string(), recipient: z.string(),