Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View 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.