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>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
# @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
```
<!-- begin:auto-docs -->
## 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…
<!-- end:auto-docs -->
## 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"`.

View File

@@ -0,0 +1,84 @@
# `error-codes` — stable error-code registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the namespaced `TENANT_SUBDOMAIN.*` error-code registry plus the
concrete `SubdomainTenantResolverError` class for the
`@suaveplan/api-tenant-resolver-subdomain` package. Every public failure
path in the resolver constructs an error with one of these codes so
consumers can branch on a stable string value (e.g. `error.code ===
TENANT_SUBDOMAIN_CODES.BASE_DOMAIN_MISMATCH`) and `instanceof
SubdomainTenantResolverError` narrows `code` to the union type.
These codes back The host-side mapping (4xx vs 5xx response
shape) and the OTel `outcome` label so an operator dashboarding tenant
resolution sees the exact failure mode without parsing free-text
messages.
## Features
- Five stable codes: `INVALID`, `NOT_FOUND_IN_REGISTRY`,
`BASE_DOMAIN_MISMATCH`, `FORWARDED_HOST_UNTRUSTED`, `INVALID_OPTIONS`.
- `defineCodes` from `@suaveplan/error` — values are frozen and the
namespace prefix (`TENANT_SUBDOMAIN.`) is applied once.
- Subclass with public-constructor lint compliance and a redundant
`subdomainTenantResolverCode` field for downstream type narrowing.
## Usage: Basic
```ts
import {
SubdomainTenantResolverError,
TENANT_SUBDOMAIN_CODES,
} from "@suaveplan/api-tenant-resolver-subdomain/error-codes";
throw new SubdomainTenantResolverError(
TENANT_SUBDOMAIN_CODES.INVALID,
"Label 'ACME!' contains forbidden characters"
);
```
## Usage: Advanced
```ts
import {
SubdomainTenantResolverError,
TENANT_SUBDOMAIN_CODES,
} from "@suaveplan/api-tenant-resolver-subdomain/error-codes";
try {
await resolver.resolve(request);
} catch (e) {
if (e instanceof SubdomainTenantResolverError) {
switch (e.code) {
case TENANT_SUBDOMAIN_CODES.BASE_DOMAIN_MISMATCH:
return new Response("Wrong domain", { status: 400 });
case TENANT_SUBDOMAIN_CODES.FORWARDED_HOST_UNTRUSTED:
return new Response("Forbidden", { status: 403 });
default:
throw e;
}
}
throw e;
}
```
## API Reference
- `TENANT_SUBDOMAIN_CODES` — frozen record of code constants.
- `SubdomainTenantResolverErrorCode` — union of the registered codes.
- `SubdomainTenantResolverError` — concrete subclass of
`SuaveplanError<SubdomainTenantResolverErrorCode>`.
## Implementation Notes
The registry is built with `defineCodes("TENANT_SUBDOMAIN", [...] as const)`
so the namespace prefix is enforced once and every value is statically
typed as `TENANT_SUBDOMAIN.<NAME>`. The error subclass exposes a public
constructor (per the api-stack public-constructor lint rule) and carries
a `subdomainTenantResolverCode` field so callers that down-cast on the
narrow class don't have to re-narrow the inherited `code` field.

View File

@@ -0,0 +1,83 @@
# `forwarded` — `X-Forwarded-Host` trust enforcement
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements CIDR-based trust enforcement for the optional
`X-Forwarded-Host` header. Edge proxies (CDN, load balancer, reverse
proxy) rewrite the inbound `Host` to their internal hostname and stash
the public host on `X-Forwarded-Host`. The resolver may honour that
header, but ONLY when (a) the operator opted in via
`trustForwardedHost: true` and (b) the upstream IP belongs to a
trusted-proxy CIDR — otherwise a malicious client could spoof its
tenant by sending `X-Forwarded-Host: victim.api.example.com` directly
to the origin.
This module owns the parser (`parseCidr`, `parseIp`) and the matcher
(`cidrMatches`, `isTrustedProxy`) used by the resolver hot-path.
## Features
- IPv4 and IPv6 support (including `::` shorthand and zone-id stripping).
- Byte-array comparison with bit-aware prefix masking — no string
manipulation in the hot-path.
- Family-disjoint match — an IPv6 CIDR never matches an IPv4 address.
- `parseCidr` returns `null` on malformed input so the caller (options
parser) surfaces a stable `INVALID_OPTIONS` error rather than
silently widening the trust set.
## Usage: Basic
```ts
import {
isTrustedProxy,
parseCidr,
} from "@suaveplan/api-tenant-resolver-subdomain/forwarded";
const cidrs = ["10.0.0.0/8", "2001:db8::/32"]
.map((c) => parseCidr(c)!)
.filter((c) => c !== null);
isTrustedProxy("10.4.7.2", cidrs); // → true
isTrustedProxy("8.8.8.8", cidrs); // → false
```
## Usage: Advanced
```ts
import {
cidrMatches,
parseCidr,
parseIp,
} from "@suaveplan/api-tenant-resolver-subdomain/forwarded";
const cidr = parseCidr("172.16.0.0/12");
const ip = parseIp("172.16.5.99");
if (cidr !== null && ip !== null && cidrMatches(ip, cidr)) {
// ...trusted...
}
```
## API Reference
- `ParsedCidr` — `{ bytes: Uint8Array; prefix: number; family: 4 | 6 }`.
- `parseCidr(input: string): ParsedCidr | null`.
- `parseIp(addr: string): { bytes: Uint8Array; family: 4 | 6 } | null`.
- `cidrMatches(ip, cidr): boolean`.
- `isTrustedProxy(remoteAddress, cidrs): boolean`.
## Implementation Notes
IPv6 parsing handles the canonical `::` double-colon expansion, zone-id
suffixes (`%eth0` is stripped — never meaningful for trust checks),
and rejects > 8 groups or non-hex content. Prefix matching extracts
the full-byte run with a byte-equality loop, then applies a
high-bit mask to the trailing partial byte; `prefix === 0` is the
"match everything in this family" case and is handled implicitly
(loop runs zero times, tail short-circuit returns `true`).
The module is dependency-free — no `node:net`, no `ipaddr.js`. This
keeps the package universal and the CIDR parsing observable in
tests without runtime injection.

View File

@@ -0,0 +1,84 @@
# `options` — Zod-validated resolver options
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines and validates the public option bundle accepted by
`createSubdomainTenantResolver`. Every option is bounded by a Zod
schema at construction time so an operator that misspells a key,
supplies an invalid CIDR, or passes a negative `tenantLabelIndex`
discovers the mistake immediately rather than at the first inbound
request. The resolved bundle replaces every `??` ladder so the
resolver body stays small and the hot-path is allocation-free.
The shape mirrors `@suaveplan/api-tenant-resolver-header` /
`-jwt` (id + strict + validator + registry) plus the subdomain-specific
fields (`baseDomain`, `tenantLabelIndex`, `trustForwardedHost`,
`trustedProxyCidrs`) and a `getTransport` extractor so the host can
inject framework-specific request metadata.
## Features
- Strict Zod schema — extra keys fail validation.
- CIDR pre-parsing at construction time so the per-request hot-path is
byte-array prefix comparison only.
- Stable `INVALID_OPTIONS` code on every failure (Zod, CIDR parse,
missing option object).
- `LabelValidator` and `TenantRegistryLookup` opt-in extension points.
## Usage: Basic
```ts
import { parseSubdomainTenantResolverOptions } from "@suaveplan/api-tenant-resolver-subdomain/options";
const resolved = parseSubdomainTenantResolverOptions({
baseDomain: "api.example.com",
});
// → { id: "subdomain", baseDomain: "api.example.com",
// tenantLabelIndex: 0, strict: false, ... }
```
## Usage: Advanced
```ts
import type { TenantId } from "@suaveplan/api-types/tenant";
import { parseSubdomainTenantResolverOptions } from "@suaveplan/api-tenant-resolver-subdomain/options";
const registry = new Map<string, TenantId>([
["acme", "t_acme" as TenantId],
]);
const resolved = parseSubdomainTenantResolverOptions({
id: "primary",
baseDomain: "api.example.com",
tenantLabelIndex: 0,
strict: true,
trustForwardedHost: true,
trustedProxyCidrs: ["10.0.0.0/8", "2001:db8::/32"],
registry: (slug) => registry.get(slug) ?? null,
});
```
## API Reference
- `SubdomainTenantResolverOptions` — public input bundle.
- `ResolvedSubdomainTenantResolverOptions` — internal, every default
applied, CIDRs pre-parsed.
- `parseSubdomainTenantResolverOptions(input): Resolved...`.
- `TenantRegistryLookup` — slug → `TenantId | null` (sync or async).
- `RequestTransport` — `{ host, forwardedHost, remoteAddress }`.
- `TransportExtractor` — `(request: Request) => RequestTransport`.
## Implementation Notes
The schema is `.strict()` so a typo (`requirePrincipal` instead of
`requireTenant`) is a hard error at construction. `validate` and
`registry` are stripped before the Zod parse — `z.function()` is
deprecated and unnecessary here because runtime function calls already
fail loudly if the wrong shape is passed. `trustedProxyCidrs` is
re-parsed through `parseCidr` so every CIDR is normalised to a
`ParsedCidr` once; this is the only allocation per resolver
construction (per-request matching uses the cached array).

View File

@@ -0,0 +1,76 @@
# `otel/instruments` — OpenTelemetry instruments
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the single OTel counter `api.tenant.resolves_total` that the
subdomain resolver increments on every invocation. The counter is
labelled with `{ resolver: "subdomain", resolver_id, outcome }` where
`outcome ∈ { "resolved", "advance", "rejected" }`. The label-set is
designed so a host composing multiple resolvers (`jwt` → `header` →
`subdomain`) can aggregate across the chain by `resolver` and still
slice per-adapter by `resolver_id`.
Instruments are resolved lazily on first access so a test harness can
install a custom `MeterProvider` BEFORE importing any production module
and capture every observation. `reset...Instruments()` is the
companion test-only seam.
## Features
- Lazy meter resolution against the global `MeterProvider`.
- Single counter, three outcome labels — no over-instrumented surface.
- Test reset hook for deterministic per-test observation.
## Usage: Basic
```ts
import { getSubdomainTenantResolverInstruments } from "@suaveplan/api-tenant-resolver-subdomain/otel";
const { resolves } = getSubdomainTenantResolverInstruments();
resolves.add(1, { resolver: "subdomain", outcome: "resolved" });
```
## Usage: Advanced
```ts
import {
getSubdomainTenantResolverInstruments,
resetSubdomainTenantResolverInstruments,
} from "@suaveplan/api-tenant-resolver-subdomain/otel";
// Test harness — install MeterProvider then reset the cache so the
// next access binds to the test provider.
resetSubdomainTenantResolverInstruments();
const inst = getSubdomainTenantResolverInstruments();
// ...run resolver, flush exporter, assert datapoints...
```
## API Reference
- `SubdomainTenantResolverInstruments` — `{ meter, resolves }`.
- `getSubdomainTenantResolverInstruments(): Instruments`.
- `resetSubdomainTenantResolverInstruments(): void` (test-only).
## Implementation Notes
The label `resolver` is the canonical chain identifier (always
`"subdomain"` in this package). `resolver_id` carries the per-instance
id from `SubdomainTenantResolverOptions.id` (default `"subdomain"`) so
operators that chain multiple subdomain resolvers — e.g. `tenant` and
`region` mounted at different `tenantLabelIndex` values — still see
disjoint metric streams. `outcome` is the failure-mode discriminator
matching the `TENANT_SUBDOMAIN.*` code namespace (`rejected` ↔ any of
`INVALID`, `BASE_DOMAIN_MISMATCH`, `FORWARDED_HOST_UNTRUSTED`,
`NOT_FOUND_IN_REGISTRY`).
The cached instrument bundle pattern is shared with every other
api-stack OTel-bearing package — the meter and counter handles bind
once and the `add` calls reuse the same descriptors so the OTel SDK
amortises the attribute-set allocation per data-point. Tests stamp
the global `MeterProvider` from the `tests/setup/polyfills.ts`
side-effect import, then call `resetSubdomainTenantResolverInstruments`
in `afterEach` so each test sees its own counter slice.

View File

@@ -0,0 +1,100 @@
# `resolver` — `createSubdomainTenantResolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Builds The `TenantResolver` that derives the tenant from the
inbound request's hostname (subdomain). For a host
`acme.api.example.com` configured with `baseDomain: "api.example.com"`,
the resolver returns `{ id: "acme" as TenantId, slug: "acme" }`. The
chain-advance sentinel (`null`) is returned when the host is missing,
equals the base domain, or fails validation in non-strict mode. Strict
mode throws a `SubdomainTenantResolverError` carrying one of four
discriminator codes so the host can emit a 4xx-shaped response.
The transport extractor is dependency-injected so the resolver works
identically under Node / Bun / Cloudflare Workers / Hono — every host
attaches `host`, `forwardedHost`, and `remoteAddress` via whichever
framework primitive it prefers.
## Features
- Pure function — no per-request allocations beyond the returned
`TenantContext`.
- Multi-label support via `tenantLabelIndex` (default 0 — leftmost).
- Optional `registry` callback for slug → `TenantId` projection.
- `X-Forwarded-Host` consulted only when `trustForwardedHost: true` AND
the upstream IP matches `trustedProxyCidrs`.
- OTel counter on every outcome (`resolved | advance | rejected`).
- Strict-mode parity with the JWT / header siblings.
## Usage: Basic
```ts
import { createSubdomainTenantResolver } from "@suaveplan/api-tenant-resolver-subdomain";
const resolver = createSubdomainTenantResolver({
baseDomain: "api.example.com",
});
const req = new Request("https://acme.api.example.com/v1/health", {
headers: { host: "acme.api.example.com" },
});
const ctx = await resolver.resolve(req);
// → { id: "acme", slug: "acme" }
```
## Usage: Advanced
```ts
import type { TenantId } from "@suaveplan/api-types/tenant";
import { createSubdomainTenantResolver } from "@suaveplan/api-tenant-resolver-subdomain";
const registry = new Map<string, TenantId>([
["acme", "t_acme" as TenantId],
]);
const resolver = createSubdomainTenantResolver({
id: "primary",
baseDomain: "api.example.com",
tenantLabelIndex: 0,
strict: true,
trustForwardedHost: true,
trustedProxyCidrs: ["10.0.0.0/8"],
registry: (slug) => registry.get(slug) ?? null,
getTransport: (req) => ({
host: req.headers.get("host"),
forwardedHost: req.headers.get("x-forwarded-host"),
remoteAddress: (req as unknown as { ip?: string }).ip ?? null,
}),
});
const req = new Request("https://internal.lb/v1/x", {
headers: {
host: "internal.lb",
"x-forwarded-host": "acme.eu.api.example.com",
},
});
Object.defineProperty(req, "ip", { value: "10.4.7.2" });
const ctx = await resolver.resolve(req);
// → { id: "t_acme", slug: "acme" }
```
## API Reference
- `createSubdomainTenantResolver(options): TenantResolver`.
- `defaultTransportExtractor: TransportExtractor`.
## Implementation Notes
The host-selection step is split into a small `chooseHost` helper so
the strict-mode `FORWARDED_HOST_UNTRUSTED` branch is reachable without
duplicating the base / forwarded fallback for the non-trust path.
Outcomes are recorded as soon as the branch is committed (before the
strict-mode `throw`) so the counter is monotonic across strict and
non-strict deployments — operators get the same dashboard regardless
of mode. The registry callback may be synchronous (`Map.get`) or async
(DB lookup); the awaited result is always coerced to `TenantId | null`.

View File

@@ -0,0 +1,78 @@
# `validate` — tenant-label validation
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the default RFC-1035-style hostname-label validator
(`defaultLabelValidator`) plus two pure string helpers (`normalizeHost`,
`stripBaseDomain`) that the resolver uses to take an inbound `Host`
header all the way down to a candidate tenant slug. Splitting these
helpers out of the resolver body keeps the resolver readable and the
host-parsing edge cases unit-testable in isolation.
Tenant labels are persisted as `tenantId` and compared against by
downstream RBAC / row-level filters, so the canonical form is enforced
here: lower-case ASCII, 1..63 characters, `[a-z0-9-]`, no leading or
trailing hyphen.
## Features
- `defaultLabelValidator` — RFC 1035 §2.3.1 LDH-style validator.
- `MAX_LABEL_LENGTH` constant exported for downstream schemas.
- `normalizeHost(raw)` — strip `:port` suffix, lower-case, refuse the
empty / starts-with-`:` cases.
- `stripBaseDomain(host, baseDomain)` — tri-state return:
`string` (labels), `null` (exact match — no tenant), `undefined`
(mismatch — caller decides whether to raise).
## Usage: Basic
```ts
import {
defaultLabelValidator,
normalizeHost,
stripBaseDomain,
} from "@suaveplan/api-tenant-resolver-subdomain/validate";
const host = normalizeHost("Acme.API.Example.com:443");
// → "acme.api.example.com"
const labels = stripBaseDomain(host!, "api.example.com");
// → "acme"
defaultLabelValidator(labels!); // → true
```
## Usage: Advanced
```ts
import { defaultLabelValidator } from "@suaveplan/api-tenant-resolver-subdomain/validate";
const RESERVED = new Set(["www", "admin", "api"]);
const customValidator = (label: string): boolean => {
if (!defaultLabelValidator(label)) return false;
return !RESERVED.has(label);
};
// → pass `customValidator` as `validate` in SubdomainTenantResolverOptions
```
## API Reference
- `LabelValidator` — `(label: string) => boolean`.
- `defaultLabelValidator: LabelValidator`.
- `MAX_LABEL_LENGTH: 63`.
- `normalizeHost(raw: string | null | undefined): string | null`.
- `stripBaseDomain(host, baseDomain): string | null | undefined`.
## Implementation Notes
The label validator is hand-coded against the character-code range
(`0x30..0x39`, `0x61..0x7a`, `0x2d`) rather than a regex so the
hot-path has no allocator overhead. The tri-state `stripBaseDomain`
return is the standard "missing vs absent vs malformed" pattern from
the rest of the codebase: callers MUST distinguish "host is exactly the
apex" (advance the chain — common for marketing pages) from "host has
the wrong apex" (strict-mode error). `normalizeHost` does not honour
IPv6 bracketed literals (`[::1]`) — tenant resolution by IPv6 host is
not a supported deployment shape.