@@ -12,57 +12,61 @@ export type ApiResult =
1212const API_URL = "https://chatgpt.com/backend-api/wham/usage" ;
1313const TIMEOUT_MS = 10_000 ;
1414
15+ /** Check that value is a non-null record with all required string fields. */
16+ function hasStrings ( v : unknown , ...keys : string [ ] ) : boolean {
17+ if ( typeof v !== "object" || v === null ) return false ;
18+ const r = v as Record < string , unknown > ;
19+ return keys . every ( ( k ) => typeof r [ k ] === "string" ) ;
20+ }
21+
22+ /** Check that value is a non-null record with all required boolean fields. */
23+ function hasBooleans ( v : unknown , ...keys : string [ ] ) : boolean {
24+ if ( typeof v !== "object" || v === null ) return false ;
25+ const r = v as Record < string , unknown > ;
26+ return keys . every ( ( k ) => typeof r [ k ] === "boolean" ) ;
27+ }
28+
29+ /** Check that value is a non-null record with all required number fields. */
30+ function hasNumbers ( v : unknown , ...keys : string [ ] ) : boolean {
31+ if ( typeof v !== "object" || v === null ) return false ;
32+ const r = v as Record < string , unknown > ;
33+ return keys . every ( ( k ) => typeof r [ k ] === "number" ) ;
34+ }
35+
36+ /** Validate optional window object (nullable, but shape-checked if present). */
37+ function isValidWindow ( w : unknown ) : boolean {
38+ if ( w === null || w === undefined ) return true ;
39+ return hasNumbers (
40+ w ,
41+ "used_percent" ,
42+ "limit_window_seconds" ,
43+ "reset_after_seconds" ,
44+ "reset_at" ,
45+ ) ;
46+ }
47+
1548/**
1649 * Validates that parsed JSON data has the required QuotaResponse shape.
1750 * Checks both top-level and nested required fields for type safety.
1851 * `code_review_rate_limit` is OPTIONAL and not checked here.
1952 */
2053function validateResponse ( data : unknown ) : data is QuotaResponse {
21- if ( typeof data !== "object" || data === null ) return false ;
54+ if ( ! hasStrings ( data , "user_id" , "account_id" , "email" , "plan_type" ) )
55+ return false ;
2256 const d = data as Record < string , unknown > ;
2357
24- // Top-level required strings
25- if ( typeof d . user_id !== "string" ) return false ;
26- if ( typeof d . account_id !== "string" ) return false ;
27- if ( typeof d . email !== "string" ) return false ;
28- if ( typeof d . plan_type !== "string" ) return false ;
29-
30- // rate_limit — must be non-null object with boolean fields
31- if ( typeof d . rate_limit !== "object" || d . rate_limit === null ) return false ;
58+ // rate_limit — must be non-null object with boolean fields + windows
59+ if ( ! hasBooleans ( d . rate_limit , "allowed" , "limit_reached" ) ) return false ;
3260 const rl = d . rate_limit as Record < string , unknown > ;
33- if ( typeof rl . allowed !== "boolean" ) return false ;
34- if ( typeof rl . limit_reached !== "boolean" ) return false ;
35- // primary_window: required but nullable — validate shape if present
36- if ( rl . primary_window !== null && rl . primary_window !== undefined ) {
37- if ( typeof rl . primary_window !== "object" ) return false ;
38- const pw = rl . primary_window as Record < string , unknown > ;
39- if ( typeof pw . used_percent !== "number" ) return false ;
40- if ( typeof pw . limit_window_seconds !== "number" ) return false ;
41- if ( typeof pw . reset_after_seconds !== "number" ) return false ;
42- if ( typeof pw . reset_at !== "number" ) return false ;
43- }
44- // secondary_window: optional, validate shape if present
45- if ( rl . secondary_window !== null && rl . secondary_window !== undefined ) {
46- if ( typeof rl . secondary_window !== "object" ) return false ;
47- const sw = rl . secondary_window as Record < string , unknown > ;
48- if ( typeof sw . used_percent !== "number" ) return false ;
49- if ( typeof sw . limit_window_seconds !== "number" ) return false ;
50- if ( typeof sw . reset_after_seconds !== "number" ) return false ;
51- if ( typeof sw . reset_at !== "number" ) return false ;
52- }
61+ if ( ! isValidWindow ( rl . primary_window ) ) return false ;
62+ if ( ! isValidWindow ( rl . secondary_window ) ) return false ;
5363
5464 // credits — must be non-null object with required fields
55- if ( typeof d . credits !== "object" || d . credits === null ) return false ;
56- const cr = d . credits as Record < string , unknown > ;
57- if ( typeof cr . has_credits !== "boolean" ) return false ;
58- if ( typeof cr . unlimited !== "boolean" ) return false ;
59- if ( typeof cr . balance !== "string" ) return false ;
65+ if ( ! hasBooleans ( d . credits , "has_credits" , "unlimited" ) ) return false ;
66+ if ( ! hasStrings ( d . credits , "balance" ) ) return false ;
6067
6168 // spend_control — must be non-null object with boolean field
62- if ( typeof d . spend_control !== "object" || d . spend_control === null )
63- return false ;
64- const sc = d . spend_control as Record < string , unknown > ;
65- if ( typeof sc . reached !== "boolean" ) return false ;
69+ if ( ! hasBooleans ( d . spend_control , "reached" ) ) return false ;
6670
6771 return true ;
6872}
0 commit comments