# `require-tenant` ## Purpose `requireTenant` extracts the request principal from the Hono context slot `c.get("user")` (populated by `@suaveplan/api-plugin-auth` before route dispatch) and either returns its non-empty `tenantId` plus optional `id` or throws `CampaignsApiError` carrying the `CAMPAIGNS_API.TENANT_REQUIRED` code with HTTP 401 wired into the error's context. This is the single tenant-presence gate every campaign, suppression, and webhook route relies on, so handlers can treat `tenantId` as a definitely-present string thereafter. The helper does not consult a tenant resolver fallback (no `?? "default"` short-circuit). Multi-tenant correctness is the campaigns plugin's primary obligation — silently falling back to a shared tenant would let one tenant's misconfiguration leak data into another's namespace. Failing closed with a 401 surfaces the problem loudly. ## Features - Pure synchronous read; no I/O, no DB, no allocator churn. - Treats `null`, `undefined`, missing `tenantId`, non-string `tenantId`, and empty-string `tenantId` as equally invalid. - Returns a frozen-shape object (`{ id, tenantId }`) — `id` is optional in the principal but always a string in the return value (falls back to `""`). - Throws `CampaignsApiError(TENANT_REQUIRED)` with `context.statusCode === 401` so the package error handler can map uniformly. ## Usage: Basic ```ts import { requireTenant } from "@suaveplan/api-plugin-campaigns"; app.openapi(listRoute, async (c) => { const { tenantId } = requireTenant(c); const result = await c.get("campaignRepository").list({ tenantId }); return c.json({ campaigns: result, total: result.length }, 200); }); ``` ## Usage: Advanced ```ts import { CAMPAIGNS_API_CODES, CampaignsApiError, requireTenant, } from "@suaveplan/api-plugin-campaigns"; async function authorize(c: Context, target: { tenantId: string }) { const { tenantId } = requireTenant(c); if (target.tenantId !== tenantId) { throw new CampaignsApiError( CAMPAIGNS_API_CODES.CAMPAIGN_NOT_FOUND, "campaign belongs to a different tenant", { context: { statusCode: 404 } } ); } } ``` ## API Reference - `interface TenantPrincipal { readonly id: string; readonly tenantId: string }` - `requireTenant(c: Context): TenantPrincipal` — returns the principal or throws `CampaignsApiError(TENANT_REQUIRED)`. ## Implementation Notes The principal type is cast through `unknown` rather than imported from `@suaveplan/api-plugin-auth` because the campaigns plugin must type-check in isolation (consumers may swap in a different auth provider). The structural check (`typeof principal.tenantId === "string" && principal.tenantId.length > 0`) is intentionally explicit: TypeScript's narrowing for record-typed `unknown` does not collapse the two checks into one. The empty-string check is the load-bearing one — Zod's `min(1)` on the auth side prevents most empty values, but defending against an out-of-band setter (e.g. a test fixture passing `{ tenantId: "" }`) keeps the contract honest.