Files
skills/plugins/document-navigator/docs/api-stack/api/api-tenant-resolver-subdomain
admin 9bf56a806b Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 01:10:00 +01:00
..

@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 concrete SubdomainTenantResolverError class…
    • error-codes — Defines the namespaced TENANT_SUBDOMAIN.* error-code registry plus the concrete SubdomainTenantResolverError class…
  • forwarded — Implements CIDR-based trust enforcement for the optional X-Forwarded-Host header. Edge proxies (CDN, load balancer, r…
    • forwarded — Implements CIDR-based trust enforcement for the optional X-Forwarded-Host header. Edge proxies (CDN, load balancer, r…
  • 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…
  • otel — Owns the single OTel counter api.tenant.resolves_total that the subdomain resolver increments on every invocation. Th…
    • instruments — Owns the single OTel counter api.tenant.resolves_total that the subdomain resolver increments on every invocation. Th…
  • resolver — Builds the Stage-8 TenantResolver that derives the tenant from the inbound request's hostname (subdomain). For a host…
    • resolver — Builds the Stage-8 TenantResolver that derives the tenant from the inbound request's hostname (subdomain). For a host…
  • 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…

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 + 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".