@@ -180,9 +180,38 @@ const parseDateParts = (
180180 if ( ! Number . isFinite ( year ) || ! Number . isFinite ( monthIndex ) || ! Number . isFinite ( day ) ) {
181181 return undefined ;
182182 }
183+ // The regex only checks shape; Date.* silently rolls impossible calendar dates over
184+ // (e.g. 2026-02-30 -> 2026-03-02), so a typo'd day would return usage for the wrong day.
185+ // Reject parts that don't round-trip through a UTC probe (also catches the JS 2-digit-year remap).
186+ const probe = new Date ( Date . UTC ( year , monthIndex , day ) ) ;
187+ if (
188+ probe . getUTCFullYear ( ) !== year ||
189+ probe . getUTCMonth ( ) !== monthIndex ||
190+ probe . getUTCDate ( ) !== day
191+ ) {
192+ return undefined ;
193+ }
183194 return { year, monthIndex, day } ;
184195} ;
185196
197+ // usage.cost / sessions.usage accept optional startDate/endDate. parseDateParts returns
198+ // undefined for both absent and invalid input, so an explicitly supplied but unparseable
199+ // date (bad format or impossible calendar date like 2026-02-30) would otherwise silently
200+ // fall through to the default range and return a successful response for an unrelated range.
201+ // Return the offending field so handlers can reject it instead of querying the wrong window.
202+ const findInvalidExplicitDate = ( params : {
203+ startDate ?: unknown ;
204+ endDate ?: unknown ;
205+ } ) : "startDate" | "endDate" | undefined => {
206+ for ( const field of [ "startDate" , "endDate" ] as const ) {
207+ const raw = params [ field ] ;
208+ if ( typeof raw === "string" && raw . trim ( ) !== "" && parseDateParts ( raw ) === undefined ) {
209+ return field ;
210+ }
211+ }
212+ return undefined ;
213+ } ;
214+
186215/**
187216 * Parse a UTC offset string in the format UTC+H, UTC-H, UTC+HH, UTC-HH, UTC+H:MM, UTC-HH:MM.
188217 * Returns the UTC offset in minutes (east-positive), or undefined if invalid.
@@ -902,6 +931,7 @@ function mergeUsageCacheStatus(
902931// Exposed for unit tests (kept as a single export to avoid widening the public API surface).
903932export const testApi = {
904933 parseDateParts,
934+ findInvalidExplicitDate,
905935 parseUtcOffsetToMinutes,
906936 resolveDateInterpretation,
907937 parseDateToMs,
@@ -922,6 +952,21 @@ export const usageHandlers: GatewayRequestHandlers = {
922952 respond ( true , summary , undefined ) ;
923953 } ,
924954 "usage.cost" : async ( { respond, params, context } ) => {
955+ const invalidDate = findInvalidExplicitDate ( {
956+ startDate : params ?. startDate ,
957+ endDate : params ?. endDate ,
958+ } ) ;
959+ if ( invalidDate ) {
960+ respond (
961+ false ,
962+ undefined ,
963+ errorShape (
964+ ErrorCodes . INVALID_REQUEST ,
965+ `invalid ${ invalidDate } : expected a valid YYYY-MM-DD calendar date` ,
966+ ) ,
967+ ) ;
968+ return ;
969+ }
925970 const config = context . getRuntimeConfig ( ) ;
926971 const { startMs, endMs } = parseDateRange ( {
927972 startDate : params ?. startDate ,
@@ -956,6 +1001,18 @@ export const usageHandlers: GatewayRequestHandlers = {
9561001 }
9571002
9581003 const p = params ;
1004+ const invalidDate = findInvalidExplicitDate ( { startDate : p . startDate , endDate : p . endDate } ) ;
1005+ if ( invalidDate ) {
1006+ respond (
1007+ false ,
1008+ undefined ,
1009+ errorShape (
1010+ ErrorCodes . INVALID_REQUEST ,
1011+ `invalid ${ invalidDate } : expected a valid YYYY-MM-DD calendar date` ,
1012+ ) ,
1013+ ) ;
1014+ return ;
1015+ }
9591016 const config = context . getRuntimeConfig ( ) ;
9601017 const { startMs, endMs } = parseDateRange ( {
9611018 startDate : p . startDate ,
@@ -1335,7 +1392,8 @@ export const usageHandlers: GatewayRequestHandlers = {
13351392 }
13361393 const hiddenSession = hiddenSessions [ hiddenIndex ] ;
13371394 const merged = mergedEntries [ hiddenSession . entryIndex ] ;
1338- const usage = usageByEntryIndex [ hiddenSession . entryIndex ] ?? createEmptySessionCostSummary ( ) ;
1395+ const usage =
1396+ usageByEntryIndex [ hiddenSession . entryIndex ] ?? createEmptySessionCostSummary ( ) ;
13391397 usage . sessionId = merged . sessionId ;
13401398 usage . sessionFile = merged . sessionFile ;
13411399 mergeSessionUsageInto ( usage , summary ) ;
0 commit comments