fix(measurements): reject impossible dayKey values (2026-02-30 etc)

The dayKey validator regex matched any YYYY-MM-DD string — including
impossible dates like 2026-02-30 or 2026-13-01. `new Date()` silently
overflows those values into the following month, so the drill-down
returned rows from a different calendar day than the user asked for.
No 422 was emitted; the response was syntactically well-formed and
pointed at the wrong day. The same helper feeds the admin drain
route, so a malformed CLI invocation had the same blast radius.

Add a refine that round-trips the parsed Date back to ISO and
rejects when the slice no longer matches the input.
This commit is contained in:
Marc-André Bombeck committed 2026-05-17 15:22:29 +02:00
1 parent 67ca526708
commit c2f436720f
2 files changed
+35

No files matched your search

@@ -284,4 +284,23 @@ describe("GET /api/measurements — schema rejections (W10 reconcile)", () => {
expect(prisma.measurement.findMany).not.toHaveBeenCalled();
});
// v1.4.37 W10 H2 — `2026-02-30` is a YYYY-MM-DD string but not a
// real calendar date. `new Date()` silently overflows it to March
// 2, so the drill-down would point at the wrong day. The validator
// refine must reject it at parse time.
it("rejects impossible calendar dates on dayKey (2026-02-30)", async () => {
const res = await GET(
getRequest("type=ACTIVITY_STEPS&dayKey=2026-02-30"),
);
expect(res.status).toBe(422);
expect(prisma.measurement.findMany).not.toHaveBeenCalled();
});
it("rejects impossible month on dayKey (2026-13-01)", async () => {
const res = await GET(
getRequest("type=ACTIVITY_STEPS&dayKey=2026-13-01"),
);
expect(res.status).toBe(422);
expect(prisma.measurement.findMany).not.toHaveBeenCalled();
});
});
+16
View File
@@ -299,6 +299,22 @@ export const listMeasurementsSchema = z
dayKey: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "dayKey must be YYYY-MM-DD")
// v1.4.37 W10 — a `YYYY-MM-DD` string can satisfy the regex while
// still being an impossible calendar date (`2026-02-30`,
// `2026-13-01`). `new Date("2026-02-30T00:00:00Z")` silently
// overflows to March 2, so the drill-down would return rows from
// a different day than the user asked for. The same helper feeds
// the admin drain route, so a malformed CLI invocation has the
// same blast radius. Reject the impossible shapes at the
// validator instead.
.refine(
(s) => {
const parsed = new Date(`${s}T00:00:00Z`);
if (Number.isNaN(parsed.getTime())) return false;
return s === parsed.toISOString().slice(0, 10);
},
"dayKey must be a real calendar date (YYYY-MM-DD)",
)
.optional(),
})
.refine(