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

79 lines
3.2 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# 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"`