Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/payment-option-decimals-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@agentcommercekit/ack-pay": minor
---

`paymentOptionSchema` now rejects a negative `decimals` instead of normalizing
it. The valibot schema used `toMinValue(0)`, which is a transformation that
clamps rather than a validation that rejects, so `decimals: -2` parsed
successfully as `decimals: 0`.

`decimals` scales `amount`, so clamping turned an invalid payment option into a
valid one asking for a different sum, and it did so on the verification path as
well: `verifyPaymentRequestToken` parses token payloads with this schema. The
zod schema already rejected the same input via `nonnegative()`, so the two
validators disagreed about which payment requests are well-formed.
13 changes: 13 additions & 0 deletions packages/ack-pay/src/payment-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ describe("isPaymentRequest", () => {
).toBe(false)
})

// `decimals` scales `amount`, so a negative value has to be rejected rather
// than normalized: reading it as 0 silently changes what the request asks for.
it("returns false if decimals is negative", () => {
expect(
isPaymentRequest({
...validPaymentRequest,
paymentOptions: [
{ ...validPaymentRequest.paymentOptions[0], decimals: -2 },
],
}),
).toBe(false)
})

it("returns false if given null", () => {
expect(isPaymentRequest(null)).toBe(false)
})
Expand Down
2 changes: 1 addition & 1 deletion packages/ack-pay/src/schemas/valibot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down