Files
suaveplan/openspec/specs/api-tenant-resolver-subdomain/spec.md
2026-05-27 23:29:53 +01:00

3.2 KiB

Capability Spec Delta: api-tenant-resolver-subdomain (api-tenant-resolver-subdomain)

Package: @suaveplan/api-tenant-resolver-subdomain Status: PROPOSED

Every requirement below carries a **Verified by:** line at authoring time.

ADDED Requirements

Requirement: Resolves tenant from request host subdomain

The package SHALL expose createSubdomainTenantResolver({ baseDomain, validate, registry }) that reads request.host, strips baseDomain, validates the remaining label, and returns { tenantId, source: "subdomain" } when valid.

Scenario: leftmost label resolves as tenant

  • WHEN request.host = "acme.api.example.com" AND baseDomain = "api.example.com"
  • THEN the resolver returns { tenantId: "acme", source: "subdomain" }

Verified by: src/resolver/resolver.test.ts::"leftmost label resolves as tenant"


Requirement: Base-domain mismatch handled per strict mode

The package SHALL raise TENANT_SUBDOMAIN_BASE_DOMAIN_MISMATCH in strict mode when the host does not end with baseDomain. In non-strict mode it returns null.

Scenario: mismatched base domain raises in strict

  • WHEN request.host = "acme.evil.example.com" AND baseDomain = "api.example.com" AND strict: true
  • THEN the resolver raises TENANT_SUBDOMAIN_BASE_DOMAIN_MISMATCH

Verified by: src/resolver/resolver.test.ts::"mismatched base domain raises in strict"


Requirement: Multi-label deployments via tenantLabelIndex

The package SHALL accept tenantLabelIndex (default 0) to support hosts like <tenant>.<region>.api.example.com.

Scenario: middle-label deployment

  • WHEN request.host = "acme.eu.api.example.com" AND tenantLabelIndex = 0 AND baseDomain = "api.example.com"
  • THEN tenant is acme (label 0 of acme.eu)

Verified by: src/resolver/resolver.test.ts::"multi-label deployment resolves by index"


Requirement: X-Forwarded-Host trust requires CIDR allowlist

The package SHALL only consult X-Forwarded-Host when trustForwardedHost: true AND the upstream IP matches trustedProxyCidrs. Untrusted upstream raises TENANT_SUBDOMAIN_FORWARDED_HOST_UNTRUSTED in strict mode.

Scenario: untrusted proxy ignored

  • WHEN X-Forwarded-Host is set AND the upstream IP is not in trustedProxyCidrs AND strict: true
  • THEN the resolver raises TENANT_SUBDOMAIN_FORWARDED_HOST_UNTRUSTED

Scenario: trusted proxy honoured

  • WHEN the upstream IP matches trustedProxyCidrs AND X-Forwarded-Host is well-formed
  • THEN the forwarded host is used to resolve the tenant

Verified by: src/forwarded/forwarded.test.ts::"untrusted proxy raises FORWARDED_HOST_UNTRUSTED" AND src/forwarded/forwarded.test.ts::"trusted proxy honours forwarded host"


Requirement: Passes runTenantResolverConformance

The package SHALL pass runTenantResolverConformance from @suaveplan/api-tenant-provider/testing.

Scenario: conformance passes

  • WHEN runTenantResolverConformance("subdomain", makeSubdomainResolver) runs
  • THEN every harness it(...) passes

Verified by: src/resolver/resolver.spec-conformance.test.ts::"subdomain resolver passes runTenantResolverConformance"