1- import  {  createCache ,  DefaultStatefulContext ,  Namespace ,  Cache  as  UnkeyCache  }  from  "@unkey/cache" ; 
2- import  {  MemoryStore  }  from  "@unkey/cache/stores" ; 
3- import  {  Ratelimit  }  from  "@upstash/ratelimit" ; 
4- import  {  Request  as  ExpressRequest ,  Response  as  ExpressResponse ,  NextFunction  }  from  "express" ; 
5- import  {  RedisOptions  }  from  "ioredis" ; 
6- import  {  createHash  }  from  "node:crypto" ; 
7- import  {  z  }  from  "zod" ; 
81import  {  env  }  from  "~/env.server" ; 
92import  {  authenticateAuthorizationHeader  }  from  "./apiAuth.server" ; 
10- import  {  logger  }  from  "./logger.server" ; 
11- import  {  createRedisRateLimitClient ,  Duration ,  RateLimiter  }  from  "./rateLimiter.server" ; 
12- import  {  RedisCacheStore  }  from  "./unkey/redisCacheStore.server" ; 
13- 
14- const  DurationSchema  =  z . custom < Duration > ( ( value )  =>  { 
15-   if  ( typeof  value  !==  "string" )  { 
16-     throw  new  Error ( "Duration must be a string" ) ; 
17-   } 
18- 
19-   return  value  as  Duration ; 
20- } ) ; 
21- 
22- export  const  RateLimitFixedWindowConfig  =  z . object ( { 
23-   type : z . literal ( "fixedWindow" ) , 
24-   window : DurationSchema , 
25-   tokens : z . number ( ) , 
26- } ) ; 
27- 
28- export  type  RateLimitFixedWindowConfig  =  z . infer < typeof  RateLimitFixedWindowConfig > ; 
29- 
30- export  const  RateLimitSlidingWindowConfig  =  z . object ( { 
31-   type : z . literal ( "slidingWindow" ) , 
32-   window : DurationSchema , 
33-   tokens : z . number ( ) , 
34- } ) ; 
35- 
36- export  type  RateLimitSlidingWindowConfig  =  z . infer < typeof  RateLimitSlidingWindowConfig > ; 
37- 
38- export  const  RateLimitTokenBucketConfig  =  z . object ( { 
39-   type : z . literal ( "tokenBucket" ) , 
40-   refillRate : z . number ( ) , 
41-   interval : DurationSchema , 
42-   maxTokens : z . number ( ) , 
43- } ) ; 
44- 
45- export  type  RateLimitTokenBucketConfig  =  z . infer < typeof  RateLimitTokenBucketConfig > ; 
46- 
47- export  const  RateLimiterConfig  =  z . discriminatedUnion ( "type" ,  [ 
48-   RateLimitFixedWindowConfig , 
49-   RateLimitSlidingWindowConfig , 
50-   RateLimitTokenBucketConfig , 
51- ] ) ; 
52- 
53- export  type  RateLimiterConfig  =  z . infer < typeof  RateLimiterConfig > ; 
54- 
55- type  LimitConfigOverrideFunction  =  ( authorizationValue : string )  =>  Promise < unknown > ; 
56- 
57- type  Options  =  { 
58-   redis ?: RedisOptions ; 
59-   keyPrefix : string ; 
60-   pathMatchers : ( RegExp  |  string ) [ ] ; 
61-   pathWhiteList ?: ( RegExp  |  string ) [ ] ; 
62-   defaultLimiter : RateLimiterConfig ; 
63-   limiterConfigOverride ?: LimitConfigOverrideFunction ; 
64-   limiterCache ?: { 
65-     fresh : number ; 
66-     stale : number ; 
67-   } ; 
68-   log ?: { 
69-     requests ?: boolean ; 
70-     rejections ?: boolean ; 
71-     limiter ?: boolean ; 
72-   } ; 
73- } ; 
74- 
75- async  function  resolveLimitConfig ( 
76-   authorizationValue : string , 
77-   hashedAuthorizationValue : string , 
78-   defaultLimiter : RateLimiterConfig , 
79-   cache : UnkeyCache < {  limiter : RateLimiterConfig  } > , 
80-   logsEnabled : boolean , 
81-   limiterConfigOverride ?: LimitConfigOverrideFunction 
82- ) : Promise < RateLimiterConfig >  { 
83-   if  ( ! limiterConfigOverride )  { 
84-     return  defaultLimiter ; 
85-   } 
86- 
87-   if  ( logsEnabled )  { 
88-     logger . info ( "RateLimiter: checking for override" ,  { 
89-       authorizationValue : hashedAuthorizationValue , 
90-       defaultLimiter, 
91-     } ) ; 
92-   } 
93- 
94-   const  cacheResult  =  await  cache . limiter . swr ( hashedAuthorizationValue ,  async  ( key )  =>  { 
95-     const  override  =  await  limiterConfigOverride ( authorizationValue ) ; 
96- 
97-     if  ( ! override )  { 
98-       if  ( logsEnabled )  { 
99-         logger . info ( "RateLimiter: no override found" ,  { 
100-           authorizationValue, 
101-           defaultLimiter, 
102-         } ) ; 
103-       } 
104- 
105-       return  defaultLimiter ; 
106-     } 
107- 
108-     const  parsedOverride  =  RateLimiterConfig . safeParse ( override ) ; 
109- 
110-     if  ( ! parsedOverride . success )  { 
111-       logger . error ( "Error parsing rate limiter override" ,  { 
112-         override, 
113-         errors : parsedOverride . error . errors , 
114-       } ) ; 
115- 
116-       return  defaultLimiter ; 
117-     } 
118- 
119-     if  ( logsEnabled  &&  parsedOverride . data )  { 
120-       logger . info ( "RateLimiter: override found" ,  { 
121-         authorizationValue, 
122-         defaultLimiter, 
123-         override : parsedOverride . data , 
124-       } ) ; 
125-     } 
126- 
127-     return  parsedOverride . data ; 
128-   } ) ; 
129- 
130-   return  cacheResult . val  ??  defaultLimiter ; 
131- } 
132- 
133- //returns an Express middleware that rate limits using the Bearer token in the Authorization header 
134- export  function  authorizationRateLimitMiddleware ( { 
135-   redis, 
136-   keyPrefix, 
137-   defaultLimiter, 
138-   pathMatchers, 
139-   pathWhiteList =  [ ] , 
140-   log =  { 
141-     rejections : true , 
142-     requests : true , 
143-   } , 
144-   limiterCache, 
145-   limiterConfigOverride, 
146- } : Options )  { 
147-   const  ctx  =  new  DefaultStatefulContext ( ) ; 
148-   const  memory  =  new  MemoryStore ( {  persistentMap : new  Map ( )  } ) ; 
149-   const  redisCacheStore  =  new  RedisCacheStore ( { 
150-     connection : { 
151-       keyPrefix : `cache:${ keyPrefix }  , 
152-       ...redis , 
153-     } , 
154-   } ) ; 
155- 
156-   // This cache holds the rate limit configuration for each org, so we don't have to fetch it every request 
157-   const  cache  =  createCache ( { 
158-     limiter : new  Namespace < RateLimiterConfig > ( ctx ,  { 
159-       stores : [ memory ,  redisCacheStore ] , 
160-       fresh : limiterCache ?. fresh  ??  30_000 , 
161-       stale : limiterCache ?. stale  ??  60_000 , 
162-     } ) , 
163-   } ) ; 
164- 
165-   const  redisClient  =  createRedisRateLimitClient ( 
166-     redis  ??  { 
167-       port : env . REDIS_PORT , 
168-       host : env . REDIS_HOST , 
169-       username : env . REDIS_USERNAME , 
170-       password : env . REDIS_PASSWORD , 
171-       enableAutoPipelining : true , 
172-       ...( env . REDIS_TLS_DISABLED  ===  "true"  ? { }  : {  tls : { }  } ) , 
173-     } 
174-   ) ; 
175- 
176-   return  async  ( req : ExpressRequest ,  res : ExpressResponse ,  next : NextFunction )  =>  { 
177-     if  ( log . requests )  { 
178-       logger . info ( `RateLimiter (${ keyPrefix } ${ req . path }  ) ; 
179-     } 
180- 
181-     // allow OPTIONS requests 
182-     if  ( req . method . toUpperCase ( )  ===  "OPTIONS" )  { 
183-       return  next ( ) ; 
184-     } 
185- 
186-     //first check if any of the pathMatchers match the request path 
187-     const  path  =  req . path ; 
188-     if  ( 
189-       ! pathMatchers . some ( ( matcher )  => 
190-         matcher  instanceof  RegExp  ? matcher . test ( path )  : path  ===  matcher 
191-       ) 
192-     )  { 
193-       if  ( log . requests )  { 
194-         logger . info ( `RateLimiter (${ keyPrefix } ${ req . path }  ) ; 
195-       } 
196-       return  next ( ) ; 
197-     } 
198- 
199-     // Check if the path matches any of the whitelisted paths 
200-     if  ( 
201-       pathWhiteList . some ( ( matcher )  => 
202-         matcher  instanceof  RegExp  ? matcher . test ( path )  : path  ===  matcher 
203-       ) 
204-     )  { 
205-       if  ( log . requests )  { 
206-         logger . info ( `RateLimiter (${ keyPrefix } ${ req . path }  ) ; 
207-       } 
208-       return  next ( ) ; 
209-     } 
210- 
211-     if  ( log . requests )  { 
212-       logger . info ( `RateLimiter (${ keyPrefix } ${ req . path }  ) ; 
213-     } 
214- 
215-     const  authorizationValue  =  req . headers . authorization ; 
216-     if  ( ! authorizationValue )  { 
217-       if  ( log . requests )  { 
218-         logger . info ( `RateLimiter (${ keyPrefix }  ,  {  headers : req . headers ,  url : req . url  } ) ; 
219-       } 
220-       res . setHeader ( "Content-Type" ,  "application/problem+json" ) ; 
221-       return  res . status ( 401 ) . send ( 
222-         JSON . stringify ( 
223-           { 
224-             title : "Unauthorized" , 
225-             status : 401 , 
226-             type : "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401" , 
227-             detail : "No authorization header provided" , 
228-             error : "No authorization header provided" , 
229-           } , 
230-           null , 
231-           2 
232-         ) 
233-       ) ; 
234-     } 
235- 
236-     const  hash  =  createHash ( "sha256" ) ; 
237-     hash . update ( authorizationValue ) ; 
238-     const  hashedAuthorizationValue  =  hash . digest ( "hex" ) ; 
239- 
240-     const  limiterConfig  =  await  resolveLimitConfig ( 
241-       authorizationValue , 
242-       hashedAuthorizationValue , 
243-       defaultLimiter , 
244-       cache , 
245-       typeof  log . limiter  ===  "boolean"  ? log . limiter  : false , 
246-       limiterConfigOverride 
247-     ) ; 
248- 
249-     const  limiter  = 
250-       limiterConfig . type  ===  "fixedWindow" 
251-         ? Ratelimit . fixedWindow ( limiterConfig . tokens ,  limiterConfig . window ) 
252-         : limiterConfig . type  ===  "tokenBucket" 
253-         ? Ratelimit . tokenBucket ( 
254-             limiterConfig . refillRate , 
255-             limiterConfig . interval , 
256-             limiterConfig . maxTokens 
257-           ) 
258-         : Ratelimit . slidingWindow ( limiterConfig . tokens ,  limiterConfig . window ) ; 
259- 
260-     const  rateLimiter  =  new  RateLimiter ( { 
261-       redisClient, 
262-       keyPrefix, 
263-       limiter, 
264-       logSuccess : log . requests , 
265-       logFailure : log . rejections , 
266-     } ) ; 
267- 
268-     const  {  success,  limit,  reset,  remaining }  =  await  rateLimiter . limit ( hashedAuthorizationValue ) ; 
269- 
270-     const  $remaining  =  Math . max ( 0 ,  remaining ) ;  // remaining can be negative if the user has exceeded the limit, so clamp it to 0 
271- 
272-     res . set ( "x-ratelimit-limit" ,  limit . toString ( ) ) ; 
273-     res . set ( "x-ratelimit-remaining" ,  $remaining . toString ( ) ) ; 
274-     res . set ( "x-ratelimit-reset" ,  reset . toString ( ) ) ; 
275- 
276-     if  ( success )  { 
277-       return  next ( ) ; 
278-     } 
279- 
280-     res . setHeader ( "Content-Type" ,  "application/problem+json" ) ; 
281-     const  secondsUntilReset  =  Math . max ( 0 ,  ( reset  -  new  Date ( ) . getTime ( ) )  /  1000 ) ; 
282-     return  res . status ( 429 ) . send ( 
283-       JSON . stringify ( 
284-         { 
285-           title : "Rate Limit Exceeded" , 
286-           status : 429 , 
287-           type : "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429" , 
288-           detail : `Rate limit exceeded ${ $remaining } ${ limit } ${ secondsUntilReset }  , 
289-           reset, 
290-           limit, 
291-           remaining, 
292-           secondsUntilReset, 
293-           error : `Rate limit exceeded ${ $remaining } ${ limit } ${ secondsUntilReset }  , 
294-         } , 
295-         null , 
296-         2 
297-       ) 
298-     ) ; 
299-   } ; 
300- } 
3+ import  {  authorizationRateLimitMiddleware  }  from  "./authorizationRateLimitMiddleware.server" ; 
4+ import  {  Duration  }  from  "./rateLimiter.server" ; 
3015
3026export  const  apiRateLimiter  =  authorizationRateLimitMiddleware ( { 
3037  keyPrefix : "api" , 
@@ -312,16 +16,24 @@ export const apiRateLimiter = authorizationRateLimitMiddleware({
31216    stale : 60_000  *  20 ,  // Date is stale after 20 minutes 
31317  } , 
31418  limiterConfigOverride : async  ( authorizationValue )  =>  { 
315-     // TODO: we need to add an option to "allowJWT" auth and then handle this differently 
31619    const  authenticatedEnv  =  await  authenticateAuthorizationHeader ( authorizationValue ,  { 
31720      allowPublicKey : true , 
21+       allowJWT : true , 
31822    } ) ; 
31923
32024    if  ( ! authenticatedEnv )  { 
32125      return ; 
32226    } 
32327
324-     return  authenticatedEnv . environment . organization . apiRateLimiterConfig ; 
28+     if  ( authenticatedEnv . type  ===  "PUBLIC_JWT" )  { 
29+       return  { 
30+         type : "fixedWindow" , 
31+         window : env . API_RATE_LIMIT_JWT_WINDOW , 
32+         tokens : env . API_RATE_LIMIT_JWT_TOKENS , 
33+       } ; 
34+     }  else  { 
35+       return  authenticatedEnv . environment . organization . apiRateLimiterConfig ; 
36+     } 
32537  } , 
32638  pathMatchers : [ / ^ \/ a p i / ] , 
32739  // Allow /api/v1/tasks/:id/callback/:secret 
0 commit comments