Files
zip/middleware/ratelimit.go
T
zeekay c581e0af7b feat: zip canonical home — the ZAP-native web framework, consolidated
zip is shared protocol infra (the fiber-over-ZAP web framework used by every
Lux/Hanzo/Zoo Go binary — nodes, gateway, cloud, iam, kms, o11y, ai, …), so its
home is the ZAP protocol family (zap-proto/{go,http,zapd,zip}), NOT any one org's
brand namespace. This consolidates the rich framework (formerly hanzoai/zip v0.5.0
— Ctx/Handler/Errorf/Err*/HTTPError, typed handlers, middleware, runtime/extension
loading, openapi, validate, zaprpc) with the real ZAP transport (ListenZAP/
ListenHTTP/Serve over zap-proto/http) into ONE canonical module.

Module path github.com/hanzoai/zip -> github.com/zap-proto/zip. Code identical to
hanzoai/zip v0.5.0; promoted to v1.0.0 as the stable shared-infra framework. The
prior hanzoai/zip lineage is retired — one home, one import path, one way.

Verified: go build/vet/test ./... green; the ZAP round-trip integration test serves
/v1/health over the real ZAP transport (200 + handler JSON).
2026-07-01 23:08:35 -07:00

96 lines
1.9 KiB
Go

package middleware
import (
"sync"
"time"
"github.com/zap-proto/zip"
)
// RateLimitConfig configures the per-key token bucket.
type RateLimitConfig struct {
// Limit is the number of requests permitted per Window.
Limit int
// Window is the time window (e.g. 1*time.Minute).
Window time.Duration
// KeyFn extracts the bucket key from the request. Default: c.Org()
// if present, else c.Fiber().IP().
KeyFn func(c *zip.Ctx) string
}
// RateLimit returns a per-org (or per-IP fallback) token-bucket limiter.
// Buckets are kept in-memory; suitable for single-pod deployments.
// Multi-pod deployments should use a distributed limiter at the gateway.
func RateLimit(cfg RateLimitConfig) zip.Handler {
if cfg.Limit <= 0 {
cfg.Limit = 100
}
if cfg.Window <= 0 {
cfg.Window = time.Minute
}
if cfg.KeyFn == nil {
cfg.KeyFn = func(c *zip.Ctx) string {
if org := c.Org(); org != "" {
return org
}
return c.Fiber().IP()
}
}
type bucket struct {
count int
reset time.Time
}
var (
mu sync.Mutex
buckets = map[string]*bucket{}
)
return func(c *zip.Ctx) error {
key := cfg.KeyFn(c)
now := time.Now()
mu.Lock()
b, ok := buckets[key]
if !ok || now.After(b.reset) {
b = &bucket{reset: now.Add(cfg.Window)}
buckets[key] = b
}
b.count++
remaining := cfg.Limit - b.count
exhausted := b.count > cfg.Limit
mu.Unlock()
if exhausted {
c.SetHeader("X-RateLimit-Limit", itoa(cfg.Limit))
c.SetHeader("X-RateLimit-Remaining", "0")
return zip.Errorf(429, "rate limit exceeded")
}
c.SetHeader("X-RateLimit-Limit", itoa(cfg.Limit))
c.SetHeader("X-RateLimit-Remaining", itoa(remaining))
return c.Continue()
}
}
func itoa(n int) string {
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var b [20]byte
pos := len(b)
for n > 0 {
pos--
b[pos] = byte('0' + n%10)
n /= 10
}
if neg {
pos--
b[pos] = '-'
}
return string(b[pos:])
}