Skip to content

Commit 4e43d4e

Browse files
committed
fix: ratelimit headers
1 parent 82da8e2 commit 4e43d4e

File tree

1 file changed

+44
-35
lines changed

1 file changed

+44
-35
lines changed

apps/api/src/lib/rate-limit.ts

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,12 @@ export interface RateLimitResult {
2626
reset: Date;
2727
}
2828

29-
let redisClient: Redis;
29+
const redis = Redis.fromEnv();
3030

3131
const rateLimiterCache = new Map<string, Ratelimit>();
3232
const ephemeralCache = new Map<string, number>();
3333
const inMemoryLimits = new Map<string, { count: number; resetTime: number }>();
3434

35-
// Plan-based rate limiters matching pricing tiers
36-
const redis = Redis.fromEnv();
37-
3835
export 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-
8062
function 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

138147
function 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

Comments
 (0)