Initial Commit
This commit is contained in:
100
packages/api-stack/api/api-tenant/src/rate-limit/rate-limit.md
Normal file
100
packages/api-stack/api/api-tenant/src/rate-limit/rate-limit.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# rate-limit
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `rate-limit` module ships `tenantRateLimit(options)` — a Hono
|
||||
middleware that throttles requests per resolved tenant. The store is
|
||||
pluggable via the `RateLimitStore` interface; the default
|
||||
`MemoryRateLimitStore` is bounded by `maxEntries` (default 10 000)
|
||||
with LRU eviction on overflow so a high-cardinality tenant set
|
||||
cannot leak memory.
|
||||
|
||||
Tenant-less requests pass through untouched — the gate only activates
|
||||
once `createTenantMiddleware` has populated `c.get("tenant")`.
|
||||
Exceeding the window returns a 429 with `Retry-After` plus the
|
||||
standard `X-RateLimit-*` triple. The body carries the
|
||||
`TENANT.RATE_LIMITED` code so consumers can branch on the typed
|
||||
identifier rather than the HTTP status alone.
|
||||
|
||||
## Features
|
||||
|
||||
- Pluggable backing store via `RateLimitStore` interface (`get`,
|
||||
`set`, `delete`). Adapters for Redis or SQL plug in via the same
|
||||
shape.
|
||||
- Default `MemoryRateLimitStore` — bounded by `maxEntries`, LRU
|
||||
eviction, in-process only.
|
||||
- Sliding-fixed window — counter resets at `now + windowMs`; the
|
||||
next request after `resetAt` starts a fresh window.
|
||||
- 429 response carries `Retry-After`, `X-RateLimit-{Limit,Remaining,
|
||||
Reset}`, and a JSON body with `code: "TENANT.RATE_LIMITED"`.
|
||||
- Injectable `clock` — `FakeClock` interop, deterministic tests.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { tenantRateLimit } from "@suaveplan/api-tenant";
|
||||
|
||||
app.use(
|
||||
"*",
|
||||
tenantRateLimit({ maxRequests: 60, windowMs: 60_000 })
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Plugging in a Redis-backed store and a custom key prefix:
|
||||
|
||||
```ts
|
||||
import {
|
||||
type RateLimitRecord,
|
||||
type RateLimitStore,
|
||||
tenantRateLimit,
|
||||
} from "@suaveplan/api-tenant";
|
||||
|
||||
const redisStore: RateLimitStore = {
|
||||
async get(key) {
|
||||
const raw = await redis.get(key);
|
||||
return raw ? (JSON.parse(raw) as RateLimitRecord) : undefined;
|
||||
},
|
||||
async set(key, value) {
|
||||
await redis.set(key, JSON.stringify(value), "PX", value.resetAt);
|
||||
},
|
||||
async delete(key) {
|
||||
await redis.del(key);
|
||||
},
|
||||
};
|
||||
|
||||
app.use(
|
||||
"*",
|
||||
tenantRateLimit({
|
||||
maxRequests: 1000,
|
||||
windowMs: 60_000,
|
||||
store: redisStore,
|
||||
keyPrefix: "ratelimit:v1",
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `tenantRateLimit(options): MiddlewareHandler`.
|
||||
- `interface TenantRateLimitOptions` — `maxRequests`, `windowMs`,
|
||||
`store?`, `keyPrefix?`, `clock?`.
|
||||
- `interface RateLimitStore` — `get`, `set`, `delete`.
|
||||
- `interface RateLimitRecord` — `{ count, resetAt }`.
|
||||
- `class MemoryRateLimitStore` — public constructor `(maxEntries = 10_000)`,
|
||||
exposes `.size` for assertions.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`MemoryRateLimitStore.set` deletes-and-reinserts on overwrite to keep
|
||||
LRU ordering consistent — this guarantees the same key never inflates
|
||||
the apparent "newness" through repeated updates. The 429 response is
|
||||
returned (not thrown) so the rest of the middleware chain can short
|
||||
circuit cleanly; the `TenantError(TENANT_RATE_LIMITED)` instance is
|
||||
constructed for the message-formatting path and contributes the code
|
||||
to the conformance gate's "every code raised" check.
|
||||
Reference in New Issue
Block a user