first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
# middleware
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `middleware` module ships `createTenantMiddleware(options)` — the
Hono middleware that resolves the inbound tenant, applies the status
gate, and writes the result under the single canonical
`c.set("tenant", tenant)` key. suaveplan deliberately collapses
upstream's three keys (`tenantId`, `tenant`, `tenantContext`) into
one; the helper accessors live in `context.ts` and read from the
same key. The module also augments Hono's `ContextVariableMap` so
`c.get("tenant")` returns a typed `Tenant` with no cast.
Status gating raises typed `TenantError` values: `deleted →
TENANT_NOT_FOUND`, `suspended → TENANT_SUSPENDED` (unless
`allowSuspended`), `pending → TENANT_PENDING`. A throw from the
underlying resolver is wrapped as `TENANT_RESOLUTION_FAILED` with the
original error preserved on `.cause`. An optional `onError(error, c)`
hook lets callers map errors to bespoke responses without losing the
typed contract.
## Features
- Single canonical context key — `c.set("tenant", tenant)` — typed
via the `ContextVariableMap` augmentation.
- Status gate covers all four statuses; `allowSuspended` opt-in lets
read-only paths serve suspended tenants.
- Excluded paths (default `/health, /ready, /docs, /openapi`) bypass
the resolver entirely so liveness probes never block on database
health.
- `onResolved(tenant, c)` callback fires after the gate passes — use
it to attach OTel attributes or per-tenant logger context.
- `onError(error, c)` maps `TenantError` to a `Response`; non-tenant
errors re-throw so the framework's global handler runs.
- `getTenant(c)` and `getTenantId(c)` typed helpers — read the
resolved tenant without `as Tenant` casts.
## Usage: Basic
```ts
import {
chainResolvers,
createTenantMiddleware,
headerResolver,
} from "@suaveplan/api-tenant";
import { Hono } from "hono";
const app = new Hono();
app.use(
"*",
createTenantMiddleware({
resolver: headerResolver("x-tenant-id", (id) =>
db.tenants.get(id)
),
})
);
app.get("/me", (c) => c.json({ tenant: c.get("tenant") }));
```
## Usage: Advanced
Composing a cached chained resolver with an error mapper:
```ts
import {
chainResolvers,
createTenantMiddleware,
headerResolver,
subdomainResolver,
TenantCache,
TenantError,
} from "@suaveplan/api-tenant";
const resolver = TenantCache.wrap(
chainResolvers(
headerResolver("x-tenant-id", lookupById),
subdomainResolver("suaveplan.io", lookupBySlug)
),
{ ttlMs: 30_000 }
);
app.use(
"*",
createTenantMiddleware({
resolver,
allowSuspended: false,
excludePaths: ["/health", "/metrics"],
onError: (error: TenantError, c) =>
c.json({ code: error.code, message: error.message }, 404),
onResolved: (tenant, c) => {
c.header("x-tenant-id", tenant.id);
},
})
);
```
## API Reference
- `createTenantMiddleware(options): MiddlewareHandler`.
- `interface TenantMiddlewareOptions` — `resolver`, `required?`,
`excludePaths?`, `allowSuspended?`, `onError?`, `onResolved?`.
- `getTenant(c): Tenant | undefined`.
- `getTenantId(c): string | undefined`.
- Module augmentation — `declare module "hono" { interface
ContextVariableMap { tenant: Tenant } }`.
## Implementation Notes
Status checks raise BEFORE `c.set` so a suspended tenant never lands
under `c.get("tenant")` — downstream handlers cannot accidentally
serve suspended data. Resolver throws are caught and re-wrapped as
`TENANT_RESOLUTION_FAILED` instead of bubbling up as a raw `Error`
because the framework's global error handler benefits from the
discriminated `code` field. The `tenant: Tenant` ambient declaration
is reachable from anywhere that imports any value from
`@suaveplan/api-tenant`, which is enforced by the side-effect import
in the package root barrel.