# middleware _(no submodules)_ ## 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.