102 lines
5.4 KiB
Markdown
102 lines
5.4 KiB
Markdown
# @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.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable `RATELIMIT.*` error-code namespace for the `@suaveplan/api-rate-limit` package and the `RateLimitErr…
|
|
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable `RATELIMIT.*` error-code namespace for the `@suaveplan/api-rate-limit` package and the `RateLimitErr…
|
|
- [instruments](./src/instruments/instruments.md) — Owns the rate-limit OTel instrument registry. Today the package emits exactly one counter — `api.rate_limit.exceeded_to…
|
|
- [instruments](./src/instruments/instruments.md) — Owns the rate-limit OTel instrument registry. Today the package emits exactly one counter — `api.rate_limit.exceeded_to…
|
|
- [middleware](./src/middleware/middleware.md) — The package's centerpiece. `createRateLimitMiddleware` builds a per-request gate around the token-bucket store; the gat…
|
|
- [middleware](./src/middleware/middleware.md) — The package's centerpiece. `createRateLimitMiddleware` builds a per-request gate around the token-bucket store; the gat…
|
|
- [selector](./src/selector/selector.md) — Resolves the rate-limit bucket key for a per-request frame, given the route-declared (or host-defaulted) `RateLimitKeyS…
|
|
- [selector](./src/selector/selector.md) — Resolves the rate-limit bucket key for a per-request frame, given the route-declared (or host-defaulted) `RateLimitKeyS…
|
|
- [memory-store](./src/store/memory-store/memory-store.md) — Provides the default in-memory implementation of `RateLimitStoreLike`. Backed by `TokenBucket` from `@suaveplan/rate-li…
|
|
- [memory-store](./src/store/memory-store/memory-store.md) — Provides the default in-memory implementation of `RateLimitStoreLike`. Backed by `TokenBucket` from `@suaveplan/rate-li…
|
|
- [store-like](./src/store/store-like/store-like.md) — Declares the `RateLimitStoreLike` interface — the storage abstraction behind the per-route token-bucket middleware. Kee…
|
|
- [store-like](./src/store/store-like/store-like.md) — Declares the `RateLimitStoreLike` interface — the storage abstraction behind the per-route token-bucket middleware. Kee…
|
|
<!-- end:auto-docs -->
|
|
|
|
## 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
|
|
|
|
```ts
|
|
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`
|
|
|
|
```ts
|
|
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).
|
|
|