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,372 @@
# OpenSpec Change: enterprise-rate-limiter
- **Change ID**: enterprise-rate-limiter
- **Package**: `@suaveplan/rate-limiter`
- **Category**: universal
- **Target path**: `universal/auth/rate-limiter`
- **Status**: PENDING
- **Date**: 2026-04-24
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/rate-limiter.md`
---
## Summary
Deliver `@suaveplan/rate-limiter` on the genesis branch from scratch at enterprise quality. The package provides tenant-specific rate limiting with multiple algorithms: `TokenBucket` (burst-friendly), `SlidingWindow` (smooth), and `FixedWindow` (simple); a `RateLimiter` orchestrator that applies rule-based routing to the right algorithm; pluggable `RateLimitStorage` adapters (`MemoryStorage`, adapter interface); multi-tenant multipliers; and framework-agnostic middleware helpers. The genesis rewrite removes Hono coupling from the core types, adds structured errors via `@suaveplan/error`, Zod schemas, injectable clock and RNG for determinism, OTel instrumentation, `AbortSignal` on async storage paths, property-based tests, and mitata benchmarks.
---
## Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build.
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/error` (`workspace:^`) | Base class for `RateLimiterError`; `defineCodes` for `RATE.*` error codes |
| `@suaveplan/types` (`workspace:^`) | `Clock` (injectable window computation), `Rng` (injectable jitter), `LoggerLike`, `EventBusLike` |
| `@suaveplan/events` (`workspace:^`) | `EventBus` for `rate:limit:exceeded` events |
| `@suaveplan/logger` (`workspace:^`) | Structured logging with trace correlation |
| `zod` (`catalog:forms`) | Zod schemas for all option types |
| `@opentelemetry/api` (`catalog:observability`) | Spans, counters, histograms |
| `tslib` (`catalog:build`) | TypeScript runtime helpers |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`), `@suaveplan/testing` (`workspace:^`).
---
## Preserved public API surface (from main branch)
### Core types
```ts
interface RateLimitResult {
readonly allowed: boolean
readonly remaining: number
readonly limit: number
readonly resetIn: number // ms until window resets
readonly retryAfter?: number // ms to wait (only when allowed === false)
readonly metadata?: Record<string, unknown>
}
interface RateLimitConfig {
readonly limit: number
readonly window: number // ms
readonly id?: string
readonly description?: string
}
interface RateLimitRule extends RateLimitConfig {
readonly priority?: number
readonly conditions?: RateLimitCondition[]
readonly bypass?: boolean
}
interface RateLimitCondition {
readonly field: string
readonly operator: "eq" | "neq" | "in" | "nin" | "matches"
readonly value: unknown
}
interface RateLimitContext {
readonly key: string
readonly tenantId?: string
readonly userId?: string
readonly ip?: string
readonly path?: string
readonly method?: string
readonly metadata?: Record<string, unknown>
}
interface RateLimiterOptions {
readonly defaultConfig: RateLimitConfig
readonly storage?: RateLimitStorage
readonly rules?: RateLimitRule[]
readonly enableTenantLimits?: boolean
readonly tenantMultipliers?: Record<string, number>
readonly keyPrefix?: string
readonly clock?: Clock
readonly bus?: EventBusLike
readonly logger?: LoggerLike
}
// State shapes (serialisable for persistence)
interface TokenBucketState {
tokens: number
lastRefill: number // epoch ms
}
interface SlidingWindowState {
requests: number[] // array of epoch ms timestamps
}
interface CounterState {
count: number
windowStart: number // epoch ms
}
interface RateLimitState {
key: string
algorithm: RateLimitAlgorithm
state: TokenBucketState | SlidingWindowState | CounterState
config: RateLimitConfig
updatedAt: number
}
type RateLimitAlgorithm = "token-bucket" | "sliding-window" | "fixed-window"
interface TenantRateLimitConfig extends RateLimitConfig {
tenantId: string
multiplier?: number
}
// Events
interface RateLimiterEvents {
'rate:limit:exceeded': RateLimitedPayload
'rate:limit:reset': RateLimitResetPayload
}
interface RateLimitedPayload {
key: string
tenantId?: string
remaining: number
resetIn: number
}
interface RateLimitResetPayload {
key: string
tenantId?: string
}
```
### Storage adapter
```ts
interface RateLimitStorage {
get(key: string): Promise<RateLimitState | null>
set(key: string, state: RateLimitState, ttl: number): Promise<void>
delete(key: string): Promise<void>
clear(): Promise<void>
}
interface MemoryStorageOptions {
maxSize?: number
clock?: Clock
}
class MemoryStorage implements RateLimitStorage {
constructor(options?: MemoryStorageOptions)
}
```
### Core algorithms
```ts
interface TokenBucketOptions {
capacity: number
refillRate: number // tokens per ms
initialTokens?: number
clock?: Clock
}
class TokenBucket {
constructor(options: TokenBucketOptions)
consume(tokens?: number): RateLimitResult
refill(tokens?: number): void
readonly available: number
readonly capacity: number
}
interface SlidingWindowOptions {
limit: number
windowMs: number
clock?: Clock
}
class SlidingWindow {
constructor(options: SlidingWindowOptions)
check(key: string): RateLimitResult
reset(key: string): void
}
```
### Middleware helpers
```ts
function createRateLimiter(options: RateLimiterOptions): RateLimiterInstance
function createIpRateLimiter(config: RateLimitConfig, storage?: RateLimitStorage): MiddlewareFn
function createRouteRateLimiter(routes: Record<string, RateLimitConfig>, storage?: RateLimitStorage): MiddlewareFn
function createRateLimitChecker(options: RateLimiterOptions): (context: RateLimitContext) => Promise<RateLimitResult>
function getRateLimitResult(context: RateLimitContext, storage: RateLimitStorage, config: RateLimitConfig): Promise<RateLimitResult>
interface RateLimiterMiddlewareOptions {
keyExtractor?: (req: unknown) => string
onLimited?: (result: RateLimitResult) => void
skip?: (req: unknown) => boolean
}
interface RateLimiterInstance {
check(context: RateLimitContext): Promise<RateLimitResult>
reset(key: string): Promise<void>
getState(key: string): Promise<RateLimitState | null>
}
```
### Error codes
```ts
type RateLimiterErrorCode =
| "RATE_STORAGE_ERROR"
| "RATE_INVALID_CONFIG"
| "RATE_KEY_MISSING"
| "RATE_STATE_CORRUPT"
class RateLimiterError extends SuaveplanError<RateLimiterErrorCode>
function registerRateLimiterErrorCodes(): void
```
---
## New additions for genesis
### 1. FixedWindow algorithm
A third algorithm (`FixedWindow`) complements `TokenBucket` and `SlidingWindow`. Uses a simple counter with a hard window boundary — lowest memory overhead, suitable for coarse limits.
```ts
interface FixedWindowOptions {
limit: number
windowMs: number
clock?: Clock
}
class FixedWindow {
constructor(options: FixedWindowOptions)
check(key: string): RateLimitResult
reset(key: string): void
}
```
### 2. Clock and Rng injection
`TokenBucket` refill timing and `SlidingWindow` / `FixedWindow` window boundaries use injectable `Clock` (from `@suaveplan/types`). No bare `Date.now()` at module scope. `MemoryStorage` TTL checks also use injectable `Clock`.
### 3. `RateLimiterInstance` orchestrator
`createRateLimiter` returns a `RateLimiterInstance` that:
- Selects algorithm (token-bucket / sliding-window / fixed-window) per rule or default config.
- Applies `rules` in priority order — first matching rule wins; bypass rules skip limiting.
- Applies `tenantMultipliers` to effective limit for matching tenant contexts.
- Emits `rate:limit:exceeded` on optional `bus` when `allowed === false`.
- Emits `rate:limit:reset` on `reset()`.
### 4. OTel instrumentation
- `rate_limiter.check` span + `rate_limiter.checks_total` counter (labels: `algorithm`, `allowed: bool`, `tenant_id`)
- `rate_limiter.check_duration_ms` histogram
- `rate_limiter.exceeded_total` counter (label: `key_prefix`, `tenant_id`)
- `rate_limiter.storage_get_total` / `rate_limiter.storage_set_total` counters
### 5. Zod schemas
`RateLimitConfigSchema`, `RateLimitRuleSchema`, `RateLimitContextSchema`, `RateLimiterOptionsSchema`, `TokenBucketOptionsSchema`, `SlidingWindowOptionsSchema`, `FixedWindowOptionsSchema`, `MemoryStorageOptionsSchema`.
### 6. Property-based tests (fast-check)
- TokenBucket invariant: `available` never exceeds `capacity`; `available` never goes below 0.
- SlidingWindow monotonicity: `remaining` never increases within a window without a `reset`.
- Tenant multiplier correctness: effective limit for tenant with multiplier `m` equals `config.limit * m`.
- Rule priority ordering: when two rules match, the one with higher `priority` is applied.
- Storage round-trip: `set(key, state)` then `get(key)` returns a deep-equal state (within TTL).
### 7. Benchmark suite
- `TokenBucket.consume` — 1 000 000 iterations (pure in-memory).
- `SlidingWindow.check` — 100 000 iterations with a 1 000-entry timestamp window.
- `FixedWindow.check` — 100 000 iterations.
- `createRateLimiter.check` with MemoryStorage — 100 000 iterations.
- `MemoryStorage.get` + `set` — 1 000 000 round-trips.
---
## File layout
```text
universal/auth/rate-limiter/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts
types/
rate-limiter.types.ts
rate-limiter.types.md
core/
token-bucket/
token-bucket.ts
token-bucket.md
token-bucket.types.ts
token-bucket.test.ts
index.ts
sliding-window/
sliding-window.ts
sliding-window.md
sliding-window.types.ts
sliding-window.test.ts
index.ts
fixed-window/
fixed-window.ts
fixed-window.md
fixed-window.types.ts
fixed-window.test.ts
index.ts
rate-limiter/
rate-limiter.ts # createRateLimiter + RateLimiterInstance
rate-limiter.md
rate-limiter.types.ts
rate-limiter.test.ts
index.ts
storage/
storage-adapter/
storage-adapter.ts # RateLimitStorage interface
storage-adapter.md
storage-adapter.types.ts
index.ts
memory-storage/
memory-storage.ts
memory-storage.md
memory-storage.test.ts
index.ts
middleware/
rate-limit-middleware/
rate-limit-middleware.ts # createIpRateLimiter, createRouteRateLimiter, etc.
rate-limit-middleware.md
rate-limit-middleware.types.ts
rate-limit-middleware.test.ts
index.ts
schemas/
rate-limiter.schemas.ts
rate-limiter.schemas.md
rate-limiter.schemas.test.ts
__benchmarks__/
rate-limiter.bench.ts
tests/
integration.test.ts
property.test.ts
```
---
## Impact
**Downstream packages**: API gateway and server packages will use `createRateLimiter` for request throttling. Auth plugins will use the tenant-specific configuration.
**Breaking changes from main-branch API**: Hono-specific types in `RateLimiterMiddlewareOptions` are replaced with framework-agnostic `MiddlewareFn`; a `createHonoAdapter` bridge is exported for compat. All other symbols are preserved. `FixedWindow` is a net-new addition.
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger` must be published to Verdaccio before this package can be built or tested.