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,85 @@
# selector
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Resolves the rate-limit bucket key for a per-request frame, given the
route-declared (or host-defaulted) `RateLimitKeySelector`. The
selector kind controls how the middleware fans the per-route budget
across distinct callers — `"tenant"` shares the bucket across every
principal in a tenant, `"principal"` isolates by authenticated user,
`"ip"` falls back to the network identity, and the `"custom"` form
gives the route author a free-form projection.
## Features
- Discriminated union `RateLimitKeySelector =
"tenant" | "principal" | "ip" | { kind: "custom"; select }`.
- Default selection (`selector === undefined`) prefers
`"principal"` when authenticated, else `"ip"` — matching design doc
§6 step 4a.
- Strict-mode fallback to the literal `"anon"` when the chosen kind's
source is `null` (no tenant, no principal, no IP). The middleware
never crashes; the limit just shares a single bucket across every
truly-anonymous caller.
- Returned `ResolvedKey` carries both the bucket key string AND the
`kind` label so the OTel counter can attach a stable
`key_kind` attribute without re-parsing the key.
## Usage: Basic
```ts
import { resolveRateLimitKey } from "@suaveplan/api-rate-limit/selector";
const { key, kind } = resolveRateLimitKey(
{ tenantId: "t1", principal: { id: "alice" }, ip: "10.0.0.1", request: new Request("https://h/") },
"principal"
);
// → { key: "principal:alice", kind: "principal" }
```
## Usage: Advanced
A custom selector that buckets per `(tenant, route-action-token)` so
two different mutations on the same route share fan-in but distinct
buckets:
```ts
import { resolveRateLimitKey, type RateLimitKeySelector } from "@suaveplan/api-rate-limit/selector";
const custom: RateLimitKeySelector = {
kind: "custom",
select: (ctx) => {
const u = new URL(ctx.request.url);
const action = u.searchParams.get("action") ?? "default";
return `${ctx.tenantId ?? "anon"}:${action}`;
},
};
const r = resolveRateLimitKey(
{ tenantId: "t1", principal: null, ip: "10.0.0.1", request: new Request("https://h/q?action=write") },
custom
);
// → { key: "t1:write", kind: "custom" }
```
## API Reference
- `type RateLimitKeySelector =
"tenant" | "principal" | "ip" |
{ readonly kind: "custom"; readonly select: (ctx: RateLimitContext) => string }`
- `interface RateLimitContext { tenantId: string | null; principal: { id: string } | null; ip: string | null; request: Request }`
- `interface ResolvedKey { readonly key: string; readonly kind: "tenant" | "principal" | "ip" | "custom" }`
- `function resolveRateLimitKey(ctx, selector?): ResolvedKey`
## Implementation Notes
The function is pure — it does not consult environment, the clock, or
the store. That makes it trivial to unit-test the default selector
chain against a structural `RateLimitContext` literal without
spinning up a full request pipeline. The custom selector kind is
deliberately the only variant that takes a callback; everything else
is data so the selector is plain JSON-serialisable for
`host.json`-level overrides.