Files
2026-05-27 23:29:53 +01:00

3.1 KiB

@suaveplan/api-rate-limit — per-tenant / per-principal / per-IP token-bucket middleware

Package: @suaveplan/api-rate-limit Status: PROPOSED Type: new-package Stage: 8 — see umbrella openspec/changes/api-plugin-foundation/

Why

Per design doc §6 step 4a, the per-request pipeline includes an optional rate-limit gate between tenant resolution and RBAC. Plugin routes opt in via rateLimit: { rpm, burst, key }. The host needs a deterministic, in-memory-first implementation that can be lifted onto a shared store (Redis) for multi-host fleets later without changing the contract.

What changes

  • ADDED: createRateLimitMiddleware({ store, clock }) — produces a Middleware that consumes the route's rateLimit option (rpm, burst, key selector). Implements a token-bucket per key. 429 on deny with Retry-After. Emits api.rate_limit.exceeded_total{plugin, route, key_kind}.
  • ADDED: RateLimitKeySelector — discriminated union: "tenant", "principal", "ip", { kind: "custom", select: (ctx) => string }. The selector is per route; default is "principal" when authenticated, else "ip".
  • ADDED: RateLimitStoreLike — abstract storage interface (take(key, weight) → { allowed, retryAfterMs, remaining }). Default in-memory implementation with TTL-evict ring buffer. Pluggable; an api-rate-limit-redis adapter can land in a later change.
  • ADDED: Default host budgets configurable in host.json — global per-tenant default, global per-IP default. Route-declared budgets override.
  • ADDED: Error codes (RATELIMIT_*): EXCEEDED, STORE_UNAVAILABLE.

Out of scope

  • Distributed/Redis backend — abstract interface present, adapter ships separately.
  • Sliding-window or leaky-bucket alternatives — token bucket only in v1.
  • Per-plugin global rate limiting (across all its routes) — out of scope, plugins can compose multiple route declarations.

Source mapping

Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.

Requirement Design doc section
Per-request rate-limit gate (optional) §6 step 4a
Per-tenant / per-principal / per-IP keys §6 step 4a
api.rate_limit.exceeded_total counter §12.3, §12.4
429 + Retry-After response §6

Verification approach

  • src/middleware/middleware.test.ts — token-bucket allow / deny matrices with FakeClock, including burst behaviour and refill correctness.
  • src/middleware/middleware.spec-conformance.test.ts — pins observability: api.rate_limit.exceeded_total increments only on the denial transition (not every subsequent denied call within the same window); Retry-After header matches the bucket's refill estimate.
  • src/store/memory-store.test.ts — TTL eviction, key isolation, concurrent take correctness.
  • src/selector/selector.test.ts — every key kind resolves correctly; missing principal falls back to IP.
  • tests/dep-graph.test.ts — runtime deps (api-types, error, logger, types Clock).

Migration impact

None — additive only. api-host wires this middleware when any route declares rateLimit or when host-level defaults are set.