Initial Commit
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
# rate-limit-middleware
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides framework-agnostic middleware factory helpers that wrap `createRateLimiter` for easy integration into HTTP servers, API gateways, and custom request pipelines. All middleware functions accept `req: unknown` and return a `Promise<RateLimitResult>`, keeping them decoupled from any specific framework (Hono, Express, Fastify, Bun's native server, etc.).
|
||||
|
||||
The three exported helpers cover the most common patterns: a simple checker function, IP-based limiting, and route-specific per-path limits. For Hono-specific wiring, consumers can use a thin adapter that reads `result.allowed` and returns a `429` response.
|
||||
|
||||
## Features
|
||||
|
||||
- `createRateLimitChecker` — returns a `(context) => Promise<RateLimitResult>` wrapper
|
||||
- `getRateLimitResult` — low-level direct storage+algorithm call
|
||||
- `createIpRateLimiter` — IP-key extraction with configurable `keyExtractor`, `skip`, `onLimited`
|
||||
- `createRouteRateLimiter` — per-route config map with route-prefix matching
|
||||
- All factories accept an optional `skip` predicate to bypass limiting for health probes
|
||||
- `onLimited` callback for telemetry, logging, or 429-response generation
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createRateLimitChecker } from "@suaveplan/rate-limiter";
|
||||
|
||||
const check = createRateLimitChecker({
|
||||
defaultConfig: { limit: 100, window: 60_000 },
|
||||
});
|
||||
|
||||
const result = await check({ key: "user:42" });
|
||||
if (!result.allowed) {
|
||||
return new Response("Too Many Requests", { status: 429 });
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createIpRateLimiter, createRouteRateLimiter, MemoryStorage } from "@suaveplan/rate-limiter";
|
||||
|
||||
const storage = new MemoryStorage({ maxSize: 10_000 });
|
||||
|
||||
// IP-based limiter that skips health checks
|
||||
const ipLimiter = createIpRateLimiter(
|
||||
{ limit: 200, window: 60_000 },
|
||||
storage,
|
||||
{
|
||||
keyExtractor: (req) => (req as Request).headers.get("x-forwarded-for") ?? "unknown",
|
||||
skip: (req) => (req as Request).url.endsWith("/health"),
|
||||
onLimited: (result) => console.warn("IP limited", result),
|
||||
}
|
||||
);
|
||||
|
||||
// Route-specific limits
|
||||
const routeLimiter = createRouteRateLimiter(
|
||||
{
|
||||
"/api/auth/login": { limit: 5, window: 60_000 },
|
||||
"/api/search": { limit: 50, window: 10_000 },
|
||||
},
|
||||
storage
|
||||
);
|
||||
|
||||
const result = await ipLimiter(request);
|
||||
if (!result.allowed) {
|
||||
return new Response("Too Many Requests", { status: 429 });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createRateLimitChecker(options)`
|
||||
|
||||
`(options: RateLimiterOptions) => (context: RateLimitContext) => Promise<RateLimitResult>`
|
||||
|
||||
### `getRateLimitResult(context, storage, config)`
|
||||
|
||||
`(context: RateLimitContext, storage: RateLimitStorage, config: RateLimitConfig) => Promise<RateLimitResult>`
|
||||
|
||||
Low-level helper — creates a fresh `SlidingWindow` per call, restoring persisted state from storage.
|
||||
|
||||
### `createIpRateLimiter(config, storage?, middlewareOptions?)`
|
||||
|
||||
Returns `(req: unknown) => Promise<RateLimitResult>`.
|
||||
|
||||
### `createRouteRateLimiter(routes, storage?, middlewareOptions?)`
|
||||
|
||||
Returns `(req: unknown) => Promise<RateLimitResult>`. Matches route prefixes in insertion order.
|
||||
|
||||
### `RateLimiterMiddlewareOptions`
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `keyExtractor?` | `(req: unknown) => string` | Extract key from request |
|
||||
| `onLimited?` | `(result: RateLimitResult) => void` | Called on every check (use to send 429) |
|
||||
| `skip?` | `(req: unknown) => boolean` | Skip limiting for matching requests |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`createIpRateLimiter` and `createRouteRateLimiter` each construct one `createRateLimiter` instance per route/IP config at factory time, sharing a single storage adapter. Route matching in `createRouteRateLimiter` uses `key.startsWith(route) || key.includes(route)` — for production use with complex routing rules, consumers should provide a custom `keyExtractor` that returns the matched route prefix.
|
||||
Reference in New Issue
Block a user