@@ -38,6 +38,26 @@ type Config struct { |
| 38 | Storage StorageConfig `toml:"storage"` | 38 | Storage StorageConfig `toml:"storage"` |
| 39 | Auth AuthConfig `toml:"auth"` | 39 | Auth AuthConfig `toml:"auth"` |
| 40 | Notif NotifConfig `toml:"notif"` | 40 | Notif NotifConfig `toml:"notif"` |
| | 41 | + RateLimit RateLimitConfig `toml:"ratelimit"` |
| | 42 | +} |
| | 43 | + |
| | 44 | +// RateLimitConfig configures runtime rate-limit budgets for surfaces that |
| | 45 | +// don't carry a domain-specific limiter. The /api/v1/ JSON surface uses |
| | 46 | +// the API.* sub-block; future surfaces (search, git transports) get their |
| | 47 | +// own sub-blocks here as they're factored out of their handlers. |
| | 48 | +type RateLimitConfig struct { |
| | 49 | + API APIRateLimitConfig `toml:"api"` |
| | 50 | +} |
| | 51 | + |
| | 52 | +// APIRateLimitConfig sets the per-hour budgets for /api/v1/* requests. |
| | 53 | +// AuthedPerHour applies when the caller presents a valid PAT (keyed by |
| | 54 | +// token id). AnonPerHour applies to unauthenticated callers (keyed by |
| | 55 | +// remote IP). Defaults are GitHub-aligned: 5000/h authed, 60/h anon — |
| | 56 | +// operators tune via SHITHUB_RATELIMIT__API__AUTHED_PER_HOUR / |
| | 57 | +// SHITHUB_RATELIMIT__API__ANON_PER_HOUR. |
| | 58 | +type APIRateLimitConfig struct { |
| | 59 | + AuthedPerHour int `toml:"authed_per_hour"` |
| | 60 | + AnonPerHour int `toml:"anon_per_hour"` |
| 41 | } | 61 | } |
| 42 | | 62 | |
| 43 | // NotifConfig configures the S29 notification surface. UnsubscribeKeyB64 | 63 | // NotifConfig configures the S29 notification surface. UnsubscribeKeyB64 |
@@ -205,6 +225,12 @@ func Defaults() Config { |
| 205 | ForcePathStyle: true, | 225 | ForcePathStyle: true, |
| 206 | }, | 226 | }, |
| 207 | }, | 227 | }, |
| | 228 | + RateLimit: RateLimitConfig{ |
| | 229 | + API: APIRateLimitConfig{ |
| | 230 | + AuthedPerHour: 5000, |
| | 231 | + AnonPerHour: 60, |
| | 232 | + }, |
| | 233 | + }, |
| 208 | Auth: AuthConfig{ | 234 | Auth: AuthConfig{ |
| 209 | RequireEmailVerification: true, | 235 | RequireEmailVerification: true, |
| 210 | BaseURL: "http://127.0.0.1:8080", | 236 | BaseURL: "http://127.0.0.1:8080", |
@@ -321,6 +347,18 @@ func Validate(c *Config) error { |
| 321 | if c.Auth.EmailFrom == "" { | 347 | if c.Auth.EmailFrom == "" { |
| 322 | return errors.New("config: auth.email_from is required") | 348 | return errors.New("config: auth.email_from is required") |
| 323 | } | 349 | } |
| | 350 | + if c.RateLimit.API.AuthedPerHour < 0 { |
| | 351 | + return fmt.Errorf("config: ratelimit.api.authed_per_hour: must be >= 0, got %d", c.RateLimit.API.AuthedPerHour) |
| | 352 | + } |
| | 353 | + if c.RateLimit.API.AnonPerHour < 0 { |
| | 354 | + return fmt.Errorf("config: ratelimit.api.anon_per_hour: must be >= 0, got %d", c.RateLimit.API.AnonPerHour) |
| | 355 | + } |
| | 356 | + if c.RateLimit.API.AuthedPerHour == 0 { |
| | 357 | + c.RateLimit.API.AuthedPerHour = 5000 |
| | 358 | + } |
| | 359 | + if c.RateLimit.API.AnonPerHour == 0 { |
| | 360 | + c.RateLimit.API.AnonPerHour = 60 |
| | 361 | + } |
| 324 | return nil | 362 | return nil |
| 325 | } | 363 | } |
| 326 | | 364 | |