11import { z } from "zod" ;
22import { base } from "../shared/os.ts" ;
33
4+ // Simple in-memory rate limiter: max 3 requests per IP per 10 minutes
5+ const rateLimitMap = new Map < string , number [ ] > ( ) ;
6+ const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000 ;
7+ const RATE_LIMIT_MAX = 3 ;
8+
9+ function isRateLimited ( ip : string ) : boolean {
10+ const now = Date . now ( ) ;
11+ const timestamps = rateLimitMap . get ( ip ) ?? [ ] ;
12+ const recent = timestamps . filter ( ( t ) => now - t < RATE_LIMIT_WINDOW_MS ) ;
13+
14+ if ( recent . length >= RATE_LIMIT_MAX ) {
15+ rateLimitMap . set ( ip , recent ) ;
16+ return true ;
17+ }
18+
19+ recent . push ( now ) ;
20+ rateLimitMap . set ( ip , recent ) ;
21+ return false ;
22+ }
23+
24+ // Periodic cleanup to prevent memory leak
25+ setInterval ( ( ) => {
26+ const now = Date . now ( ) ;
27+ for ( const [ ip , timestamps ] of rateLimitMap ) {
28+ const recent = timestamps . filter ( ( t ) => now - t < RATE_LIMIT_WINDOW_MS ) ;
29+ if ( recent . length === 0 ) rateLimitMap . delete ( ip ) ;
30+ else rateLimitMap . set ( ip , recent ) ;
31+ }
32+ } , 60 * 1000 ) ;
33+
434export const subscribe = base
535 . input (
636 z . object ( {
@@ -17,7 +47,15 @@ export const subscribe = base
1747 message : "Failed to subscribe" ,
1848 } ,
1949 } )
20- . handler ( async ( { input, errors } ) => {
50+ . handler ( async ( { input, context, errors } ) => {
51+ // Rate limit by IP
52+ const forwarded = ( context . headers as Headers ) . get ?.( "x-forwarded-for" ) ;
53+ const ip = forwarded ?. split ( "," ) [ 0 ] ?. trim ( ) || "unknown" ;
54+
55+ if ( isRateLimited ( ip ) ) {
56+ throw errors . RATE_LIMITED ( { data : { retryAfter : 600 } } ) ;
57+ }
58+
2159 const apiKey = process . env . RESEND_API_KEY ;
2260 const segmentId = process . env . RESEND_SEGMENT_ID ;
2361
0 commit comments