@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.
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 — Defines the namespaced
TENANT_SUBDOMAIN.*error-code registry plus the concreteSubdomainTenantResolverErrorclass…- error-codes — Defines the namespaced
TENANT_SUBDOMAIN.*error-code registry plus the concreteSubdomainTenantResolverErrorclass…
- error-codes — Defines the namespaced
- forwarded — Implements CIDR-based trust enforcement for the optional
X-Forwarded-Hostheader. Edge proxies (CDN, load balancer, r…- forwarded — Implements CIDR-based trust enforcement for the optional
X-Forwarded-Hostheader. Edge proxies (CDN, load balancer, r…
- forwarded — Implements CIDR-based trust enforcement for the optional
- options — Defines and validates the public option bundle accepted by
createSubdomainTenantResolver. Every option is bounded by…- options — Defines and validates the public option bundle accepted by
createSubdomainTenantResolver. Every option is bounded by…
- options — Defines and validates the public option bundle accepted by
- otel — Owns the single OTel counter
api.tenant.resolves_totalthat the subdomain resolver increments on every invocation. Th…- instruments — Owns the single OTel counter
api.tenant.resolves_totalthat the subdomain resolver increments on every invocation. Th…
- instruments — Owns the single OTel counter
- resolver — Builds the Stage-8
TenantResolverthat derives the tenant from the inbound request's hostname (subdomain). For a host…- resolver — Builds the Stage-8
TenantResolverthat derives the tenant from the inbound request's hostname (subdomain). For a host…
- resolver — Builds the Stage-8
- validate — Provides the default RFC-1035-style hostname-label validator (
defaultLabelValidator) plus two pure string helpers (`n…- validate — Provides the default RFC-1035-style hostname-label validator (
defaultLabelValidator) plus two pure string helpers (`n…
- validate — Provides the default RFC-1035-style hostname-label validator (
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 <slug>.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 +isTrustedProxymatcher.options/— Zod-validatedSubdomainTenantResolverOptions.otel/—api.tenant.resolves_totalcounter.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".