@@ -9,6 +9,131 @@ const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
99const OPENROUTER_FETCH_TIMEOUT_MS = 10_000 ;
1010const TRUST_PROXY_MODE = process . env . TRUST_PROXY ?. trim ( ) . toLowerCase ( ) ;
1111
12+ < < < << << HEAD
13+ /**
14+ * Basic IPv4/IPv6 format validation.
15+ * Rejects obviously spoofed or malformed values used in x-forwarded-for.
16+ */
17+ const IPV4_REGEX = / ^ ( \d { 1 , 3 } \. ) { 3 } \d { 1 , 3 } $ / ;
18+ const IPV6_REGEX = / ^ [ 0 - 9 a - f A - F : ] + $ / ;
19+
20+ function isValidIpFormat ( ip : string ) : boolean {
21+ return IPV4_REGEX . test ( ip ) || IPV6_REGEX . test ( ip ) ;
22+ }
23+
24+ if ( TRUST_PROXY_MODE === "true" ) {
25+ console . warn (
26+ "[Models API] TRUST_PROXY=true uses x-forwarded-for for rate limiting. " +
27+ "This is INSECURE unless the app is behind a trusted reverse proxy that overwrites x-forwarded-for. " +
28+ "Prefer TRUST_PROXY=cloudflare or TRUST_PROXY=vercel for production deployments." ,
29+ ) ;
30+ }
31+
32+ if ( ! TRUST_PROXY_MODE ) {
33+ console . warn ( "[Models API] TRUST_PROXY is unset; models endpoint will reject requests when IP is unavailable" ) ;
34+ }
35+
36+ if (
37+ TRUST_PROXY_MODE &&
38+ TRUST_PROXY_MODE !== "cloudflare" &&
39+ TRUST_PROXY_MODE !== "vercel" &&
40+ TRUST_PROXY_MODE !== "true"
41+ ) {
42+ console . warn ( "[Models API] Unrecognized TRUST_PROXY value; models endpoint will reject requests when IP is unavailable" ) ;
43+ }
44+
45+ const modelsIpRatelimit = upstashRedis
46+ ? new Ratelimit ( {
47+ redis : upstashRedis ,
48+ limiter : Ratelimit . slidingWindow ( 30 , "60 s" ) ,
49+ prefix : "ratelimit:models:ip" ,
50+ } )
51+ : null ;
52+
53+ async function fetchModelsFromOpenRouter ( ) : Promise < Response > {
54+ try {
55+ const response = await fetch ( OPENROUTER_MODELS_URL , {
56+ headers : {
57+ Accept : "application/json" ,
58+ } ,
59+ signal : AbortSignal . timeout ( OPENROUTER_FETCH_TIMEOUT_MS ) ,
60+ } ) ;
61+
62+ if ( ! response . ok ) {
63+ return json (
64+ { error : "Upstream service error" } ,
65+ { status : 502 } ,
66+ ) ;
67+ }
68+
69+ const payload = await response . text ( ) ;
70+
71+ if ( upstashRedis ) {
72+ try {
73+ await upstashRedis . set ( MODELS_CACHE_KEY , payload , {
74+ ex : MODELS_CACHE_TTL_SECONDS ,
75+ } ) ;
76+ } catch ( error ) {
77+ console . warn ( "[Models API] Failed to write cache:" , error ) ;
78+ }
79+ }
80+
81+ return new Response ( payload , {
82+ status : 200 ,
83+ headers : {
84+ "Content-Type" : "application/json" ,
85+ "Cache-Control" : "no-store" ,
86+ } ,
87+ } ) ;
88+ } catch ( error ) {
89+ console . warn ( "[Models API] OpenRouter fetch failed:" , error ) ;
90+ return json ( { error : "Upstream service unavailable" } , { status : 502 } ) ;
91+ }
92+ }
93+
94+ function getClientIp ( request : Request ) : string | null {
95+ if ( ! TRUST_PROXY_MODE ) {
96+ return null ;
97+ }
98+
99+ if ( TRUST_PROXY_MODE === "cloudflare" ) {
100+ const cfConnectingIp = request . headers . get ( "cf-connecting-ip" ) ?. trim ( ) ;
101+ if ( cfConnectingIp && isValidIpFormat ( cfConnectingIp ) ) return cfConnectingIp ;
102+ return null ;
103+ }
104+
105+ if ( TRUST_PROXY_MODE === "vercel" ) {
106+ const vercelForwardedFor = request . headers . get ( "x-vercel-forwarded-for" ) ?. trim ( ) ;
107+ if ( vercelForwardedFor ) {
108+ const first = vercelForwardedFor . split ( "," ) [ 0 ] ?. trim ( ) ;
109+ if ( first && isValidIpFormat ( first ) ) return first ;
110+ }
111+ return null ;
112+ }
113+
114+ if ( TRUST_PROXY_MODE === "true" ) {
115+ // Prefer platform-specific headers that are harder to spoof, as they
116+ // are typically set/overwritten by the edge proxy itself.
117+ const cfIp = request . headers . get ( "cf-connecting-ip" ) ?. trim ( ) ;
118+ if ( cfIp && isValidIpFormat ( cfIp ) ) return cfIp ;
119+
120+ const vercelIp = request . headers . get ( "x-vercel-forwarded-for" ) ?. trim ( ) ;
121+ if ( vercelIp ) {
122+ const first = vercelIp . split ( "," ) [ 0 ] ?. trim ( ) ;
123+ if ( first && isValidIpFormat ( first ) ) return first ;
124+ }
125+
126+ const realIp = request . headers . get ( "x-real-ip" ) ?. trim ( ) ;
127+ if ( realIp && isValidIpFormat ( realIp ) ) return realIp ;
128+
129+ // Fall back to x-forwarded-for only as last resort, with IP validation.
130+ // WARNING: This header is user-controlled unless the proxy overwrites it.
131+ const forwardedFor = request . headers . get ( "x-forwarded-for" ) ?. trim ( ) ;
132+ if ( forwardedFor ) {
133+ const first = forwardedFor . split ( "," ) [ 0 ] ?. trim ( ) ;
134+ if ( first && isValidIpFormat ( first ) ) return first ;
135+ | || || || 54e09 ce
136+ === = ===
12137<< < < < << HEAD
13138if ( TRUST_PROXY_MODE === "true" ) {
14139 console . warn ( "[Models API] TRUST_PROXY=true requires x-forwarded-for for rate limiting" ) ;
@@ -711,6 +836,7 @@ function getClientIp(request: Request): string | null {
711836 if ( first ) return first ;
712837> >>> >>> main
713838>>> > >>> main
839+ >>> > >>> main
714840>>> > >>> main
715841 }
716842 return null ;
0 commit comments