@@ -26,15 +26,12 @@ export interface RateLimitResult {
2626 reset : Date ;
2727}
2828
29- let redisClient : Redis ;
29+ const redis = Redis . fromEnv ( ) ;
3030
3131const rateLimiterCache = new Map < string , Ratelimit > ( ) ;
3232const ephemeralCache = new Map < string , number > ( ) ;
3333const inMemoryLimits = new Map < string , { count : number ; resetTime : number } > ( ) ;
3434
35- // Plan-based rate limiters matching pricing tiers
36- const redis = Redis . fromEnv ( ) ;
37-
3835export const ratelimit = {
3936 free : new Ratelimit ( {
4037 redis,
@@ -62,21 +59,6 @@ export const ratelimit = {
6259 } ) ,
6360} ;
6461
65- function getRedisClient ( ) : Redis | null {
66- if (
67- ! redisClient &&
68- process . env . UPSTASH_REDIS_REST_URL &&
69- process . env . UPSTASH_REDIS_REST_TOKEN
70- ) {
71- try {
72- redisClient = redis ; // Use the same Redis instance
73- } catch {
74- // ignore
75- }
76- }
77- return redisClient || null ;
78- }
79-
8062function createRateLimiter (
8163 type : RateLimitType ,
8264 customConfig ?: { requests : number ; window : string }
@@ -89,14 +71,9 @@ function createRateLimiter(
8971 return cached ;
9072 }
9173
92- const redisInstance = getRedisClient ( ) ;
93- if ( ! redisInstance ) {
94- return null ;
95- }
96-
9774 try {
9875 const rateLimiter = new Ratelimit ( {
99- redis : redisInstance ,
76+ redis,
10077 limiter : Ratelimit . slidingWindow ( config . requests , config . window as '1 m' ) ,
10178 analytics : true ,
10279 prefix : `@databuddy/ratelimit:${ type } ` ,
@@ -121,26 +98,58 @@ function getRateLimitIdentifier(
12198 return `user:${ userId } ` ;
12299 }
123100
124- const apiKey =
125- request . headers . get ( 'x-api-key' ) ||
126- request . headers . get ( 'authorization' ) ?. replace ( 'Bearer ' , '' ) ;
101+ let apiKey = request . headers . get ( 'x-api-key' ) ;
102+ if ( ! apiKey ) {
103+ const auth = request . headers . get ( 'authorization' ) ;
104+ if ( auth ?. toLowerCase ( ) . startsWith ( 'bearer ' ) ) {
105+ apiKey = auth . slice ( 7 ) . trim ( ) ;
106+ }
107+ }
108+
127109 if ( apiKey ) {
128- return `apikey:${ apiKey . substring ( 0 , 8 ) } ` ;
110+ const keyHash = btoa ( apiKey ) . slice ( 0 , 12 ) ;
111+ return `apikey:${ keyHash } ` ;
129112 }
130113
131- const ip =
132- request . headers . get ( 'x-forwarded-for' ) ||
133- request . headers . get ( 'x-real-ip' ) ||
134- 'unknown' ;
135- return `ip:${ ip } ` ;
114+ let ip =
115+ request . headers . get ( 'cf-connecting-ip' ) || // Cloudflare real IP
116+ request . headers . get ( 'x-forwarded-for' ) || // Standard proxy chain
117+ request . headers . get ( 'x-real-ip' ) || // Nginx real IP
118+ request . headers . get ( 'x-client-ip' ) || // Alternative header
119+ 'direct' ;
120+
121+ if ( ip . includes ( ',' ) ) {
122+ ip = ip . split ( ',' ) [ 0 ] ?. trim ( ) || 'direct' ;
123+ }
124+
125+ const userAgent = request . headers . get ( 'user-agent' ) ;
126+ const agentHash = userAgent ? btoa ( userAgent ) . slice ( 0 , 6 ) : 'noua' ;
127+
128+ return `ip:${ ip } :${ agentHash } ` ;
129+ }
130+
131+ function parseWindowMs ( window : string ) : number {
132+ if ( window === '1 m' ) {
133+ return 60_000 ;
134+ }
135+ if ( window === '10 s' ) {
136+ return 10_000 ;
137+ }
138+ if ( window === '1 h' ) {
139+ return 3_600_000 ;
140+ }
141+ if ( window === '1 d' ) {
142+ return 86_400_000 ;
143+ }
144+ return 60_000 ; // default fallback
136145}
137146
138147function checkInMemoryRateLimit (
139148 identifier : string ,
140149 config : { requests : number ; window : string } ,
141150 rate = 1
142151) : RateLimitResult {
143- const windowMs = config . window === '1 m' ? 60_000 : 60_000 ;
152+ const windowMs = parseWindowMs ( config . window ) ;
144153 const now = Date . now ( ) ;
145154 const resetTime = Math . ceil ( now / windowMs ) * windowMs ;
146155 const key = `${ identifier } :${ resetTime } ` ;
0 commit comments