# @suaveplan/api-tenant-resolver-subdomain — tenant resolver for the Suaveplan API plugin system. Extracts the tenant slug from the request hostname (subdomain) — e.g. `acme.api.example.com` → tenant `acme` — strips a configured `baseDomain`, validates the remaining label, and returns a `TenantContext`. Multi-label support via `tenantLabelIndex` (default 0). `X-Forwarded-Host` is honoured only when `trustForwardedHost: true` AND the upstream IP matches `trustedProxyCidrs`; otherwise strict mode raises `TENANT_SUBDOMAIN_FORWARDED_HOST_UNTRUSTED`. Optional tenant `registry` callback for slug → id mapping. Strict-mode (`strict: true`) raises stable `TENANT_SUBDOMAIN_*` codes on every invalid path; non-strict returns `null` to advance the resolver chain. Stage-8 tenant resolver for the Suaveplan API plugin system. Reads the inbound `Host` header (and optionally a CIDR-trusted `X-Forwarded-Host`), strips a configured `baseDomain`, validates the remaining label, and returns a `TenantContext` for the host's per-request pipeline. ```ts import { createSubdomainTenantResolver } from "@suaveplan/api-tenant-resolver-subdomain"; const resolver = createSubdomainTenantResolver({ baseDomain: "api.example.com", }); const ctx = await resolver.resolve(request); // → { id: "acme", slug: "acme" } | null ``` ## Documentation tree - [error-codes](./src/error-codes/error-codes.md) — Defines the namespaced `TENANT_SUBDOMAIN.*` error-code registry plus the concrete `SubdomainTenantResolverError` class… - [error-codes](./src/error-codes/error-codes.md) — Defines the namespaced `TENANT_SUBDOMAIN.*` error-code registry plus the concrete `SubdomainTenantResolverError` class… - [forwarded](./src/forwarded/forwarded.md) — Implements CIDR-based trust enforcement for the optional `X-Forwarded-Host` header. Edge proxies (CDN, load balancer, r… - [forwarded](./src/forwarded/forwarded.md) — Implements CIDR-based trust enforcement for the optional `X-Forwarded-Host` header. Edge proxies (CDN, load balancer, r… - [options](./src/options/options.md) — Defines and validates the public option bundle accepted by `createSubdomainTenantResolver`. Every option is bounded by… - [options](./src/options/options.md) — Defines and validates the public option bundle accepted by `createSubdomainTenantResolver`. Every option is bounded by… - [otel](./src/otel/instruments.md) — Owns the single OTel counter `api.tenant.resolves_total` that the subdomain resolver increments on every invocation. Th… - [instruments](./src/otel/instruments.md) — Owns the single OTel counter `api.tenant.resolves_total` that the subdomain resolver increments on every invocation. Th… - [resolver](./src/resolver/resolver.md) — Builds the Stage-8 `TenantResolver` that derives the tenant from the inbound request's hostname (subdomain). For a host… - [resolver](./src/resolver/resolver.md) — Builds the Stage-8 `TenantResolver` that derives the tenant from the inbound request's hostname (subdomain). For a host… - [validate](./src/validate/validate.md) — Provides the default RFC-1035-style hostname-label validator (`defaultLabelValidator`) plus two pure string helpers (`n… - [validate](./src/validate/validate.md) — Provides the default RFC-1035-style hostname-label validator (`defaultLabelValidator`) plus two pure string helpers (`n… ## Why a dedicated package The tenant-resolver chain (design doc §6, step 4) runs after the session resolver. Public-facing deployments commonly serve every tenant at `.api.example.com` — the resolver job is to take the hostname, strip the apex, and emit a tenant slug. Collapsing that into a dedicated adapter — rather than baking it into `api-host` — keeps the contract orthogonal and lets operators chain `tenant-resolver-jwt`, `tenant-resolver-header`, and `tenant-resolver-subdomain` as separate links. ## Chain interaction The host runs resolvers left-to-right; the first non-`null` result wins: ``` [ tenant-resolver-jwt, tenant-resolver-header, tenant-resolver-subdomain ] | | | reads principal reads X-Tenant-Id reads sub.domain (typical bearer) (admin / S2S calls) (public site routes) ``` When the inbound host has no leading labels (the request hit the apex `api.example.com`), `tenant-resolver-subdomain` returns `null` and the next resolver runs. With `strict: true` the chain stops here and a 400-equivalent is surfaced via a stable `TENANT_SUBDOMAIN.*` code. ## Modules - `error-codes/` — `TENANT_SUBDOMAIN_CODES` + `SubdomainTenantResolverError`. - `validate/` — RFC-1035 label validator + `normalizeHost` + `stripBaseDomain`. - `forwarded/` — CIDR parser + `isTrustedProxy` matcher. - `options/` — Zod-validated `SubdomainTenantResolverOptions`. - `otel/` — `api.tenant.resolves_total` counter. - `resolver/` — `createSubdomainTenantResolver` (the entry point). ## Strict mode summary | Failure | non-strict | strict (raises code) | |----------------------------------|------------|--------------------------------------------------| | host missing / equals baseDomain | `null` | `null` (advance — not an error) | | host doesn't end with baseDomain | `null` | `TENANT_SUBDOMAIN.BASE_DOMAIN_MISMATCH` | | label fails validator | `null` | `TENANT_SUBDOMAIN.INVALID` | | registry returns null | `null` | `TENANT_SUBDOMAIN.NOT_FOUND_IN_REGISTRY` | | X-Forwarded-Host from untrusted | `null` | `TENANT_SUBDOMAIN.FORWARDED_HOST_UNTRUSTED` | | invalid options at construction | n/a | `TENANT_SUBDOMAIN.INVALID_OPTIONS` | ## Stability `0.1.0` — additive only. `api-host` wires this when `host.tenant.resolvers` includes `"subdomain"`.