Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
# @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).