From a30cfaa4c319e5780649d38393faae05af72b380 Mon Sep 17 00:00:00 2001 From: abhay-codes07 Date: Mon, 13 Jul 2026 01:43:31 +0530 Subject: [PATCH] fix(validation): reject non-positive page/limit in pagination query schemas Both pagination query schemas validated only the upper bound, so malformed pagination flowed straight through the API's source-of-truth validation into the query layer instead of failing with a clean 400: - ListMemoriesQuerySchema guards its string branch with /^\d+$/ (which still admits "0"), but the z.number() branch accepted any number: { page: -1, limit: -5 } and fractional values like 2.5 parsed successfully; only limit > 1100 was rejected - DocumentsWithMemoriesQuerySchema (the input schema for POST /documents/documents) had bare z.number() for both page and limit with no bounds at all Require page and limit to be positive integers in both schemas. The 1100 limit cap and all defaults are unchanged, and no new upper bound is introduced for the documents schema since existing web flows legitimately request large pages. Covered with bun tests: defaults, numeric and numeric-string acceptance, the 1100 cap, and rejection of zero/negative/fractional values on both schemas. --- packages/validation/api.test.ts | 74 ++++++++++++++++++++++++++++++++- packages/validation/api.ts | 10 ++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/packages/validation/api.test.ts b/packages/validation/api.test.ts index 05c18934c..e186af88f 100644 --- a/packages/validation/api.test.ts +++ b/packages/validation/api.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from "bun:test" import { readFileSync } from "node:fs" -import { SearchRequestSchema, Searchv4RequestSchema } from "./api" +import { + DocumentsWithMemoriesQuerySchema, + ListMemoriesQuerySchema, + SearchRequestSchema, + Searchv4RequestSchema, +} from "./api" describe("search threshold schemas", () => { it("do not contain redundant number transforms or unreachable range guards", () => { @@ -80,3 +85,70 @@ describe("search threshold schemas", () => { ).toBe(false) }) }) + +describe("pagination query schemas", () => { + it("preserve page/limit defaults", () => { + const listed = ListMemoriesQuerySchema.parse({}) + expect(listed.page).toBe(1) + expect(listed.limit).toBe(10) + + const docs = DocumentsWithMemoriesQuerySchema.parse({}) + expect(docs.page).toBe(1) + expect(docs.limit).toBe(10) + }) + + it.each([ + 1, 50, 1100, + ])("ListMemoriesQuerySchema accepts numeric limit %p", (limit) => { + expect(ListMemoriesQuerySchema.parse({ limit }).limit).toBe(limit) + }) + + it("ListMemoriesQuerySchema accepts numeric string page/limit", () => { + const parsed = ListMemoriesQuerySchema.parse({ page: "3", limit: "25" }) + expect(parsed.page).toBe(3) + expect(parsed.limit).toBe(25) + }) + + it.each([ + 0, -5, 2.5, + ])("ListMemoriesQuerySchema rejects non-positive or fractional numeric limit %p", (limit) => { + expect(ListMemoriesQuerySchema.safeParse({ limit }).success).toBe(false) + }) + + it.each([ + 0, -1, 1.5, + ])("ListMemoriesQuerySchema rejects non-positive or fractional numeric page %p", (page) => { + expect(ListMemoriesQuerySchema.safeParse({ page }).success).toBe(false) + }) + + it("ListMemoriesQuerySchema still caps limit at 1100", () => { + expect(ListMemoriesQuerySchema.safeParse({ limit: 1101 }).success).toBe( + false, + ) + }) + + it.each([ + 0, -1, 2.5, + ])("DocumentsWithMemoriesQuerySchema rejects invalid page %p", (page) => { + expect(DocumentsWithMemoriesQuerySchema.safeParse({ page }).success).toBe( + false, + ) + }) + + it.each([ + 0, -10, 2.5, + ])("DocumentsWithMemoriesQuerySchema rejects invalid limit %p", (limit) => { + expect(DocumentsWithMemoriesQuerySchema.safeParse({ limit }).success).toBe( + false, + ) + }) + + it("DocumentsWithMemoriesQuerySchema accepts a normal request", () => { + const parsed = DocumentsWithMemoriesQuerySchema.parse({ + page: 2, + limit: 50, + }) + expect(parsed.page).toBe(2) + expect(parsed.limit).toBe(50) + }) +}) diff --git a/packages/validation/api.ts b/packages/validation/api.ts index 8579ca408..730e0c7fd 100644 --- a/packages/validation/api.ts +++ b/packages/validation/api.ts @@ -275,6 +275,9 @@ export const ListMemoriesQuerySchema = z .regex(/^\d+$/) .or(z.number()) .transform(Number) + .refine((value) => Number.isInteger(value) && value >= 1, { + message: "Limit must be a positive integer", + }) .refine((value) => value <= 1100, { message: "Limit cannot be greater than 1100", }) @@ -292,6 +295,9 @@ export const ListMemoriesQuerySchema = z .regex(/^\d+$/) .or(z.number()) .transform(Number) + .refine((value) => Number.isInteger(value) && value >= 1, { + message: "Page must be a positive integer", + }) .default("1") .openapi({ description: "Page number to fetch", example: "1" }), sort: z @@ -1092,11 +1098,11 @@ export const DocumentsWithMemoriesResponseSchema = z export const DocumentsWithMemoriesQuerySchema = z .object({ - page: z.number().default(1).openapi({ + page: z.number().int().min(1).default(1).openapi({ description: "Page number to fetch", example: 1, }), - limit: z.number().default(10).openapi({ + limit: z.number().int().min(1).default(10).openapi({ description: "Number of items per page", example: 10, }),