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.

View File

@@ -0,0 +1,207 @@
# Tasks: enterprise-rate-limiter
**Change ID**: enterprise-rate-limiter
**Package**: `@suaveplan/rate-limiter`
**Target path**: `universal/auth/rate-limiter`
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`
---
## 1. Scaffold
- [x] Create directory `universal/auth/rate-limiter/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`.
- [x] Write `package.json` at version `0.1.0` with:
- `"type": "module"`, `"sideEffects": false`
- `exports` map with top-level `"."` plus subpaths: `"./core/token-bucket/token-bucket"`, `"./core/sliding-window/sliding-window"`, `"./core/fixed-window/fixed-window"`, `"./core/rate-limiter/rate-limiter"`, `"./storage/storage-adapter/storage-adapter"`, `"./storage/memory-storage/memory-storage"`, `"./middleware/rate-limit-middleware/rate-limit-middleware"`, `"./schemas/rate-limiter.schemas"`, `"./types/rate-limiter.types"`
- `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/events workspace:^`, `@suaveplan/logger workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `mitata catalog:testing`, `@suaveplan/testing workspace:^`
- [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config` (pool: `forks`).
- [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench` scripts to `package.json`.
---
## 2. Types and errors
### 2a. Types (`src/types/rate-limiter.types.ts` + `.md`)
- [x] Define all public interfaces from `proposal.md`: `RateLimitResult`, `RateLimitConfig`, `RateLimitRule`, `RateLimitCondition`, `RateLimitContext`, `RateLimiterOptions`, `TokenBucketState`, `SlidingWindowState`, `CounterState`, `RateLimitState`, `RateLimitAlgorithm`, `TenantRateLimitConfig`, `RateLimiterEvents`, `RateLimitedPayload`, `RateLimitResetPayload`, `RateLimiterInstance`, `RateLimiterMiddlewareOptions`, `MiddlewareFn`.
- [x] Write `rate-limiter.types.md` (≥200 words, API reference for every type).
### 2b. Error codes (`src/error-codes.ts`)
- [x] Define `RateLimiterErrorCode` union — four codes: `RATE_STORAGE_ERROR`, `RATE_INVALID_CONFIG`, `RATE_KEY_MISSING`, `RATE_STATE_CORRUPT`.
- [x] Define `RATE_ERROR_METADATA` with `httpStatus`, `severity`, `retryable`, `userMessageKey`, `category`, `description` per code.
- [x] Export `registerRateLimiterErrorCodes()` as idempotent eager side-effect.
- [x] Export `RateLimiterError extends SuaveplanError<RateLimiterErrorCode>`.
### 2c. Zod schemas (`src/schemas/rate-limiter.schemas.ts` + `.md`)
- [x] Write Zod schemas: `RateLimitConfigSchema`, `RateLimitRuleSchema`, `RateLimitContextSchema`, `RateLimiterOptionsSchema`, `TokenBucketOptionsSchema`, `SlidingWindowOptionsSchema`, `FixedWindowOptionsSchema`, `MemoryStorageOptionsSchema`.
- [x] Write `rate-limiter.schemas.md`.
- [x] Write `rate-limiter.schemas.test.ts` — parse valid/invalid for each schema.
---
## 3. Storage adapter (`src/storage/storage-adapter/`)
- [x] Define `RateLimitStorage` interface: `get`, `set`, `delete`, `clear`.
- [x] Write `storage-adapter.types.ts`.
- [x] Write `storage-adapter.md`.
---
## 4. MemoryStorage (`src/storage/memory-storage/`)
- [x] Implement `MemoryStorage` using a `Map<string, { state: RateLimitState; expiresAt: number }>`:
- `get(key)` — returns `null` if missing or expired (lazy check via injectable `clock`).
- `set(key, state, ttl)` — stores with `expiresAt = clock.now() + ttl`.
- `delete(key)` — removes entry.
- `clear()` — wipes map.
- `maxSize` enforcement: on `set`, if map exceeds `maxSize`, evict the entry with the soonest `expiresAt`.
- [x] Write `memory-storage.test.ts` — get/set round-trip, TTL expiry with FakeClock, maxSize eviction, delete, clear. Each `it` has `{ timeout: 5000 }`.
- [x] Write `memory-storage.md`.
---
## 5. TokenBucket (`src/core/token-bucket/`)
- [x] Implement `TokenBucket(options)`:
- Internal state: `tokens: number`, `lastRefill: number` (epoch ms via `clock`).
- `consume(tokens?)`:
- First, compute elapsed ms since `lastRefill` via `clock.now()`; add `elapsed * refillRate` tokens (capped at `capacity`).
- If `tokens` available, decrement and return `{ allowed: true, remaining: ..., limit: capacity, resetIn: timeUntilNextToken }`.
- If insufficient, return `{ allowed: false, remaining: 0, limit: capacity, resetIn: ..., retryAfter: ... }`.
- `refill(tokens?)` — explicitly adds tokens (used in tests and manual top-ups).
- Getters: `available`, `capacity`.
- Validate options with `TokenBucketOptionsSchema`; throw `RateLimiterError` with `RATE_INVALID_CONFIG` on invalid options.
- [x] Write `token-bucket.test.ts` — consume within capacity, consume exhausted, automatic refill via FakeClock, explicit refill, `available` getter, capacity cap, invalid options error. Each `it` has `{ timeout: 5000 }`.
- [x] Write `token-bucket.md`.
---
## 6. SlidingWindow (`src/core/sliding-window/`)
- [x] Implement `SlidingWindow(options)`:
- Internal state: `Map<string, number[]>` — per-key array of request timestamps.
- `check(key)`:
- Prune timestamps older than `clock.now() - windowMs`.
- If `remaining > 0`, push current timestamp, return `{ allowed: true, remaining: ... }`.
- Otherwise return `{ allowed: false, remaining: 0, retryAfter: oldest_ts + windowMs - now }`.
- `reset(key)` — clears the key's timestamp array.
- Validate options with `SlidingWindowOptionsSchema`.
- [x] Write `sliding-window.test.ts` — allow up to limit, deny on exceed, timestamps pruned outside window (FakeClock), reset, multiple keys isolated. Each `it` has `{ timeout: 5000 }`.
- [x] Write `sliding-window.md`.
---
## 7. FixedWindow (`src/core/fixed-window/`)
- [x] Implement `FixedWindow(options)`:
- Internal state: `Map<string, { count: number; windowStart: number }>`.
- `check(key)`:
- Compute current window start = `Math.floor(clock.now() / windowMs) * windowMs`.
- If stored `windowStart !== currentWindowStart`, reset count to 0 and update `windowStart`.
- If `count < limit`, increment and return `{ allowed: true, remaining: limit - count - 1, resetIn: currentWindowStart + windowMs - clock.now() }`.
- Otherwise return `{ allowed: false, remaining: 0, retryAfter: currentWindowStart + windowMs - clock.now() }`.
- `reset(key)` — removes the key's state.
- Validate options with `FixedWindowOptionsSchema`.
- [x] Write `fixed-window.test.ts` — allow within limit, deny on exceed, window rolls over (FakeClock), reset, multiple keys. Each `it` has `{ timeout: 5000 }`.
- [x] Write `fixed-window.md`.
---
## 8. RateLimiter orchestrator (`src/core/rate-limiter/`)
- [x] Implement `createRateLimiter(options)` returning `RateLimiterInstance`:
- `check(context)`:
1. Evaluate `options.rules` in descending priority order; first matching rule (conditions met) wins.
2. If a rule has `bypass: true`, return `{ allowed: true, remaining: Infinity, limit: Infinity, resetIn: 0 }`.
3. Apply `tenantMultipliers` to effective limit if `context.tenantId` matches.
4. Build a composite storage key: `${keyPrefix}:${context.key}`.
5. Delegate to the selected algorithm (default: `sliding-window`; configurable per-rule via `rule.algorithm`).
6. If `!result.allowed`, emit `rate:limit:exceeded` on `bus`.
7. Instrument with `rate_limiter.check` span, `rate_limiter.checks_total` counter, `rate_limiter.check_duration_ms` histogram.
- `reset(key)` — deletes all storage keys matching the composite key pattern; emits `rate:limit:reset`.
- `getState(key)` — delegates to `storage.get`.
- [x] Write `rate-limiter.test.ts` (≤500 lines) — default algorithm, rule priority selection, bypass rule, tenant multiplier, bus event on denied, storage delegation, reset, getState. `{ timeout: 5000 }` / `{ timeout: 30000 }`.
- [x] Write `rate-limiter.md`.
---
## 9. Middleware helpers (`src/middleware/rate-limit-middleware/`)
- [x] Implement `createRateLimitChecker(options)` — returns `(context) => Promise<RateLimitResult>` closure over a `RateLimiterInstance`.
- [x] Implement `createIpRateLimiter(config, storage?)` — creates a limiter keyed on IP with default sliding-window.
- [x] Implement `createRouteRateLimiter(routes, storage?)` — creates a limiter with per-route `RateLimitConfig`, keyed on `path`.
- [x] Implement `getRateLimitResult(context, storage, config)` — one-shot check without creating a full `RateLimiterInstance`.
- [x] All functions return `MiddlewareFn` (framework-agnostic); include `createHonoAdapter` bridge.
- [x] Write `rate-limit-middleware.test.ts` — each helper: allowed path, denied path, IP extraction, route matching, Hono adapter. Each `it` has `{ timeout: 5000 }`.
- [x] Write `rate-limit-middleware.md`.
---
## 10. OTel instrumentation (cross-cutting review)
- [x] Confirm all spans and counters from `proposal.md §4` are wired.
- [x] Add `tests/otel.test.ts` — in-memory exporter; assert `rate_limiter.checks_total` increments, `rate_limiter.exceeded_total` increments on denied checks, `rate_limiter.check_duration_ms` records.
---
## 11. Property-based tests (`tests/property.test.ts`)
Use `fast-check`. All test timeouts `{ timeout: 30000 }`.
- [x] **TokenBucket `available` invariant**: after any sequence of `consume` and `refill` calls, `available` is always in `[0, capacity]`.
- [x] **SlidingWindow monotonicity**: within a window, `remaining` never increases without a `reset` or window rollover.
- [x] **Tenant multiplier correctness**: effective limit = `config.limit * multiplier`; `remaining` at the effective limit is 0.
- [x] **Rule priority**: when two rules both match a context, the higher-priority rule's config is applied.
- [x] **Storage round-trip**: `MemoryStorage.set(k, s, ttl)` then `MemoryStorage.get(k)` returns a structurally equal state (within TTL).
---
## 12. Benchmarks (`src/__benchmarks__/rate-limiter.bench.ts`)
Use `mitata`.
- [x] `token_bucket_consume`: 1 000 000 `consume()` calls (pure in-memory, no storage).
- [x] `sliding_window_check`: 100 000 `check()` calls with a 1 000-timestamp window.
- [x] `fixed_window_check`: 100 000 `check()` calls.
- [x] `rate_limiter_memory_storage`: 100 000 `check()` calls via `createRateLimiter` + `MemoryStorage`.
- [x] `memory_storage_get_set`: 1 000 000 round-trips.
- [x] Document placeholder numbers in `README.md` under `## Benchmarks`.
---
## 13. Docs
Per-file `.md` (all ≥200 words):
- [x] `src/types/rate-limiter.types.md`
- [x] `src/storage/storage-adapter/storage-adapter.md`
- [x] `src/storage/memory-storage/memory-storage.md`
- [x] `src/core/token-bucket/token-bucket.md`
- [x] `src/core/sliding-window/sliding-window.md`
- [x] `src/core/fixed-window/fixed-window.md`
- [x] `src/core/rate-limiter/rate-limiter.md`
- [x] `src/middleware/rate-limit-middleware/rate-limit-middleware.md`
- [x] `src/schemas/rate-limiter.schemas.md`
Root README:
- [x] Write `universal/auth/rate-limiter/README.md`: purpose, install, quick-start (token-bucket + sliding-window comparison), multi-tenant guide, middleware adapter guide, modules table, benchmarks placeholder.
---
## 14. Verify and publish
- [x] Run `bunx turbo lint --filter=@suaveplan/rate-limiter` — zero errors.
- [x] Run `bunx turbo typecheck --filter=@suaveplan/rate-limiter` — zero errors.
- [x] Run `bunx turbo test --filter=@suaveplan/rate-limiter` — all tests pass.
- [x] Run `bunx turbo test:coverage --filter=@suaveplan/rate-limiter` — 100/100/100/100.
- [x] Run `bun run docs:check --filter=@suaveplan/rate-limiter` — zero errors.
- [x] Confirm no `TODO`, `FIXME`, `@deprecated`, untyped `any`, `ts-ignore`, or `.skip` in `src/` or `tests/`.
- [x] Confirm `"sideEffects": false` in `package.json`.
- [x] Run `bun scripts/publish.ts --package @suaveplan/rate-limiter`.
- [x] Update `CLAUDE.md` package listing to include `@suaveplan/rate-limiter`.
- [x] Archive: move `openspec/changes/enterprise-rate-limiter/` to `openspec/archive/2026-04-24-enterprise-rate-limiter/` and fold spec delta into `openspec/specs/rate-limiter/spec.md`.