package middleware import ( "net/http" "sync" "time" ) // RateLimit is a per-org sliding-window limiter. It exists as a stub // so handlers can compose it today; the production version reads // per-org rate limits from the api_keys table via deps.Base. // // TODO(bootnode-port): replace with the FastAPI rate-limit equivalent // (Redis token bucket via deps.MQ or in-process LRU). See // api/bootnode/api/deps.py:rate_limit. type RateLimit struct { mu sync.Mutex buckets map[string]*bucket limit int window time.Duration } type bucket struct { tokens int resetAt time.Time } // NewRateLimit creates a per-key limiter (limit requests / window). func NewRateLimit(limit int, window time.Duration) *RateLimit { return &RateLimit{ buckets: make(map[string]*bucket), limit: limit, window: window, } } // Wrap returns a chi middleware that keys off the org slug attached // to the request context. func (rl *RateLimit) Wrap(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { key := OrgID(r.Context()) if key == "" { key = r.RemoteAddr } if !rl.allow(key) { http.Error(w, "rate limited", http.StatusTooManyRequests) return } next.ServeHTTP(w, r) }) } func (rl *RateLimit) allow(key string) bool { rl.mu.Lock() defer rl.mu.Unlock() now := time.Now() b, ok := rl.buckets[key] if !ok || now.After(b.resetAt) { rl.buckets[key] = &bucket{tokens: rl.limit - 1, resetAt: now.Add(rl.window)} return true } if b.tokens <= 0 { return false } b.tokens-- return true }