From b25481d917cb982e871312015017c6f6da6d4b99 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Mon, 25 May 2026 14:01:41 +0300 Subject: [PATCH 1/3] refactor(ack-pay): extract timestamp schemas --- packages/ack-pay/src/schemas/valibot.ts | 12 ++++++------ packages/ack-pay/src/schemas/zod/v3.ts | 9 +++++---- packages/ack-pay/src/schemas/zod/v4.ts | 9 +++++---- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index 06a58f1..6255aff 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -4,6 +4,11 @@ import * as v from "valibot" const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema]) +const timestampSchema = v.pipe( + v.union([v.date(), v.string()]), + v.transform((input) => new Date(input).toISOString()), +) + export const paymentOptionSchema = v.object({ id: v.string(), amount: v.union([v.pipe(v.number(), v.integer(), v.gtValue(0)), v.string()]), @@ -19,12 +24,7 @@ export const paymentRequestSchema = v.object({ id: v.string(), description: v.optional(v.string()), serviceCallback: v.optional(v.pipe(v.string(), v.url())), - expiresAt: v.optional( - v.pipe( - v.union([v.date(), v.string()]), - v.transform((input) => new Date(input).toISOString()), - ), - ), + expiresAt: v.optional(timestampSchema), paymentOptions: v.pipe( v.tupleWithRest([paymentOptionSchema], paymentOptionSchema), v.nonEmpty(), diff --git a/packages/ack-pay/src/schemas/zod/v3.ts b/packages/ack-pay/src/schemas/zod/v3.ts index 2e3ee83..9ecf57f 100644 --- a/packages/ack-pay/src/schemas/zod/v3.ts +++ b/packages/ack-pay/src/schemas/zod/v3.ts @@ -4,6 +4,10 @@ import { z } from "zod/v3" const urlOrDidUri = z.union([z.string().url(), didUriSchema]) +const timestampSchema = z + .union([z.date(), z.string()]) + .transform((val) => new Date(val).toISOString()) + export const paymentOptionSchema = z.object({ id: z.string(), amount: z.union([z.number().int().positive(), z.string()]), @@ -19,10 +23,7 @@ export const paymentRequestSchema = z.object({ id: z.string(), description: z.string().optional(), serviceCallback: z.string().url().optional(), - expiresAt: z - .union([z.date(), z.string()]) - .transform((val) => new Date(val).toISOString()) - .optional(), + expiresAt: timestampSchema.optional(), paymentOptions: z.array(paymentOptionSchema).nonempty(), }) diff --git a/packages/ack-pay/src/schemas/zod/v4.ts b/packages/ack-pay/src/schemas/zod/v4.ts index 55c19f0..69e07ae 100644 --- a/packages/ack-pay/src/schemas/zod/v4.ts +++ b/packages/ack-pay/src/schemas/zod/v4.ts @@ -4,6 +4,10 @@ import * as z from "zod/v4" const urlOrDidUri = z.union([z.url(), didUriSchema]) +const timestampSchema = z + .union([z.date(), z.string()]) + .transform((val) => new Date(val).toISOString()) + export const paymentOptionSchema = z.object({ id: z.string(), amount: z.union([z.number().int().positive(), z.string()]), @@ -19,10 +23,7 @@ export const paymentRequestSchema = z.object({ id: z.string(), description: z.string().optional(), serviceCallback: z.url().optional(), - expiresAt: z - .union([z.date(), z.string()]) - .transform((val) => new Date(val).toISOString()) - .optional(), + expiresAt: timestampSchema.optional(), paymentOptions: z.array(paymentOptionSchema).nonempty(), }) From 224c426979215130440461c2ade3f4f9b47ee190 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Tue, 26 May 2026 12:55:43 +0300 Subject: [PATCH 2/3] fix(ack-pay): validate timestamp inputs --- packages/ack-pay/src/schemas.test.ts | 32 +++++++++++++++++++++++++ packages/ack-pay/src/schemas/valibot.ts | 1 + packages/ack-pay/src/schemas/zod/v3.ts | 13 +++++++++- packages/ack-pay/src/schemas/zod/v4.ts | 14 ++++++++++- 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 packages/ack-pay/src/schemas.test.ts diff --git a/packages/ack-pay/src/schemas.test.ts b/packages/ack-pay/src/schemas.test.ts new file mode 100644 index 0000000..e5c1a75 --- /dev/null +++ b/packages/ack-pay/src/schemas.test.ts @@ -0,0 +1,32 @@ +import * as v from "valibot" +import { describe, expect, it } from "vitest" + +import { paymentRequestSchema as valibotPaymentRequestSchema } from "./schemas/valibot" +import { paymentRequestSchema as zodV3PaymentRequestSchema } from "./schemas/zod/v3" +import { paymentRequestSchema as zodV4PaymentRequestSchema } from "./schemas/zod/v4" + +const paymentRequest = { + id: "test-payment-request-id", + paymentOptions: [ + { + id: "test-payment-option-id", + amount: 10, + decimals: 2, + currency: "USD", + recipient: "sol:123", + }, + ], +} + +describe("paymentRequestSchema", () => { + it("reports invalid expiresAt strings as schema errors", () => { + const input = { + ...paymentRequest, + expiresAt: "invalid-date", + } + + expect(v.safeParse(valibotPaymentRequestSchema, input).success).toBe(false) + expect(zodV3PaymentRequestSchema.safeParse(input).success).toBe(false) + expect(zodV4PaymentRequestSchema.safeParse(input).success).toBe(false) + }) +}) diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index 6255aff..d97ed94 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -6,6 +6,7 @@ const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema]) const timestampSchema = v.pipe( v.union([v.date(), v.string()]), + v.check((input) => !Number.isNaN(new Date(input).getTime()), "Invalid date"), v.transform((input) => new Date(input).toISOString()), ) diff --git a/packages/ack-pay/src/schemas/zod/v3.ts b/packages/ack-pay/src/schemas/zod/v3.ts index 9ecf57f..36acd31 100644 --- a/packages/ack-pay/src/schemas/zod/v3.ts +++ b/packages/ack-pay/src/schemas/zod/v3.ts @@ -6,7 +6,18 @@ const urlOrDidUri = z.union([z.string().url(), didUriSchema]) const timestampSchema = z .union([z.date(), z.string()]) - .transform((val) => new Date(val).toISOString()) + .transform((val, ctx) => { + const date = new Date(val) + if (Number.isNaN(date.getTime())) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid date", + }) + return z.NEVER + } + + return date.toISOString() + }) export const paymentOptionSchema = z.object({ id: z.string(), diff --git a/packages/ack-pay/src/schemas/zod/v4.ts b/packages/ack-pay/src/schemas/zod/v4.ts index 69e07ae..bdd0374 100644 --- a/packages/ack-pay/src/schemas/zod/v4.ts +++ b/packages/ack-pay/src/schemas/zod/v4.ts @@ -6,7 +6,19 @@ const urlOrDidUri = z.union([z.url(), didUriSchema]) const timestampSchema = z .union([z.date(), z.string()]) - .transform((val) => new Date(val).toISOString()) + .transform((val, ctx) => { + const date = new Date(val) + if (Number.isNaN(date.getTime())) { + ctx.addIssue({ + code: "custom", + message: "Invalid date", + input: val, + }) + return z.NEVER + } + + return date.toISOString() + }) export const paymentOptionSchema = z.object({ id: z.string(), From 6b8b4211126a6dc92d1564dea60e188cdd896664 Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Thu, 4 Jun 2026 22:13:41 +0300 Subject: [PATCH 3/3] test(ack-pay): assert timestamp schemas normalize valid inputs Add a happy-path parity test feeding a Date object and the equivalent ISO string, asserting all three schemas (valibot, zod v3, zod v4) accept them and normalize to the same ISO string. The .toISOString() transform was only covered indirectly via create-signed-payment-request.test.ts; this guards the three implementations against drift. Signed-off-by: EfeDurmaz16 --- packages/ack-pay/src/schemas.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/ack-pay/src/schemas.test.ts b/packages/ack-pay/src/schemas.test.ts index e5c1a75..8d931c0 100644 --- a/packages/ack-pay/src/schemas.test.ts +++ b/packages/ack-pay/src/schemas.test.ts @@ -29,4 +29,23 @@ describe("paymentRequestSchema", () => { expect(zodV3PaymentRequestSchema.safeParse(input).success).toBe(false) expect(zodV4PaymentRequestSchema.safeParse(input).success).toBe(false) }) + + it("normalizes valid expiresAt inputs to an ISO string", () => { + const expected = "2024-12-31T23:59:59.000Z" + for (const expiresAt of [ + new Date("2024-12-31T23:59:59Z"), + "2024-12-31T23:59:59Z", + ]) { + const input = { ...paymentRequest, expiresAt } + + const valibot = v.safeParse(valibotPaymentRequestSchema, input) + expect(valibot.success && valibot.output.expiresAt).toBe(expected) + + const v3 = zodV3PaymentRequestSchema.safeParse(input) + expect(v3.success && v3.data.expiresAt).toBe(expected) + + const v4 = zodV4PaymentRequestSchema.safeParse(input) + expect(v4.success && v4.data.expiresAt).toBe(expected) + } + }) })