Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.4 KiB
@suaveplan/api-rate-limit — per-route / per-principal / per-tenant / per-IP token-bucket middleware for the @suaveplan/api plugin system. Implements the RequestRateLimiter contract consumed by @suaveplan/api-host's request pipeline and provides a Hono-shaped Middleware that returns 429 + Retry-After on denial. Backed by @suaveplan/rate-limiter's TokenBucket. In-memory store ships by default; the RateLimitStoreLike interface keeps Redis/distributed adapters pluggable.
Per-route / per-principal / per-tenant / per-IP token-bucket rate-limit
middleware for @suaveplan/api. Implements the
RequestRateLimiter contract consumed by @suaveplan/api-host's
per-request pipeline (design doc §6 step 4a) and ships a richer
check-based middleware surface for adapter integrations that want the
429 response shape pre-computed.
Status: Stage-8 keystone (0.1.0). In-memory store ships by default;
the RateLimitStoreLike interface keeps a future Redis adapter
pluggable without changing call sites.
Documentation tree
- error-codes — Defines the stable
RATELIMIT.*error-code namespace for the@suaveplan/api-rate-limitpackage and the `RateLimitErr…- error-codes — Defines the stable
RATELIMIT.*error-code namespace for the@suaveplan/api-rate-limitpackage and the `RateLimitErr…
- error-codes — Defines the stable
- instruments — Owns the rate-limit OTel instrument registry. Today the package emits exactly one counter — `api.rate_limit.exceeded_to…
- instruments — Owns the rate-limit OTel instrument registry. Today the package emits exactly one counter — `api.rate_limit.exceeded_to…
- middleware — The package's centerpiece.
createRateLimitMiddlewarebuilds a per-request gate around the token-bucket store; the gat…- middleware — The package's centerpiece.
createRateLimitMiddlewarebuilds a per-request gate around the token-bucket store; the gat…
- middleware — The package's centerpiece.
- selector — Resolves the rate-limit bucket key for a per-request frame, given the route-declared (or host-defaulted) `RateLimitKeyS…
- selector — Resolves the rate-limit bucket key for a per-request frame, given the route-declared (or host-defaulted) `RateLimitKeyS…
- memory-store — Provides the default in-memory implementation of
RateLimitStoreLike. Backed byTokenBucketfrom `@suaveplan/rate-li…- memory-store — Provides the default in-memory implementation of
RateLimitStoreLike. Backed byTokenBucketfrom `@suaveplan/rate-li…
- memory-store — Provides the default in-memory implementation of
- store-like — Declares the
RateLimitStoreLikeinterface — the storage abstraction behind the per-route token-bucket middleware. Kee…- store-like — Declares the
RateLimitStoreLikeinterface — the storage abstraction behind the per-route token-bucket middleware. Kee…
- store-like — Declares the
Why this package exists
The host pipeline's RequestRateLimiter.consume(key, weight) slot is
synchronous and intentionally minimal. Plugins want richer policy —
{ rpm, burst, key } per route, plus a way for the host to declare
global defaults in host.json. This package owns both ends: the
token-bucket store, the key-selector resolution, the OTel counter
emission, and the projection back onto the host's narrow consume
shape.
Modules
| Subpath | Surface |
|---|---|
@suaveplan/api-rate-limit |
Aggregate barrel — every public symbol. |
@suaveplan/api-rate-limit/middleware |
createRateLimitMiddleware, RateLimitRouteOption, RateLimitDecision, HostRateLimitDefaults, RateLimitRouteOptionSchema. |
@suaveplan/api-rate-limit/selector |
RateLimitKeySelector, resolveRateLimitKey, RateLimitContext. |
@suaveplan/api-rate-limit/store |
RateLimitStoreLike, MemoryRateLimitStore, createMemoryRateLimitStore, RateLimitBudget, RateLimitTakeResult. |
@suaveplan/api-rate-limit/instruments |
createRateLimitInstruments, RATE_LIMIT_COUNTER_NAMES. |
@suaveplan/api-rate-limit/error-codes |
RATELIMIT_CODES, RateLimitError, RateLimitCode. |
Quick start
import { createRateLimitMiddleware } from "@suaveplan/api-rate-limit";
const mw = createRateLimitMiddleware({
hostDefaults: {
perTenant: { rpm: 600, burst: 30 },
perIp: { rpm: 60, burst: 10 },
},
});
const decision = mw.check({
plugin: "billing",
route: "POST /invoices",
option: { rpm: 60, burst: 5, key: "principal" },
ctx: requestFrame,
});
if (!decision.allowed) {
return new Response(null, { status: 429, headers: decision.headers });
}
Wiring into @suaveplan/api-host
import { createRateLimitMiddleware } from "@suaveplan/api-rate-limit";
const mw = createRateLimitMiddleware({ /* opts */ });
const host = createHost({
rateLimit: mw.asRequestRateLimiter({
plugin: "billing",
route: "POST /invoices",
option: { rpm: 60, burst: 5 },
keyKind: "principal",
}),
// ...
});
Acceptance
Pre-archive gate:
bunx --bun turbo lint typecheck test:coverage --filter=@suaveplan/api-rate-limit
100% statements/branches/functions/lines (Istanbul).