Files
2026-05-27 23:29:53 +01:00

103 lines
7.0 KiB
Markdown

# @suaveplan/api-tenant-resolver-header — header-based tenant resolver for the Suaveplan API plugin system. Implements the TenantResolver contract from @suaveplan/api-types by reading the tenant identifier from a configurable HTTP header (default X-Tenant-Id), validating against a regex (default ^[a-z0-9-]{1,64}$) and an optional registry (a `lookup`-shaped TenantLookup compatible with @suaveplan/api-tenant-provider), and enforcing trusted-source metadata so end-user clients cannot spoof tenant headers. Strict mode raises namespaced TENANT_HEADER_INVALID / NOT_FOUND_IN_REGISTRY / UNTRUSTED_SOURCE errors; non-strict mode returns null so the resolver chain advances. Emits api.tenant.resolves_total{resolver="header"} counters via the OTel global meter.
Stage-8 tenant resolver. Implements the `TenantResolver` contract from
`@suaveplan/api-types/tenant-resolver` by reading the tenant identifier from
a configurable HTTP header (default `X-Tenant-Id`), validating its shape
against a regex (default `^[a-z0-9-]{1,64}$`), optionally probing a
registry, and enforcing trusted-source metadata so end-user clients cannot
spoof the tenant.
```ts
import { createHeaderTenantResolver } from "@suaveplan/api-tenant-resolver-header";
const resolver = createHeaderTenantResolver();
// service-mesh middleware stamps request.metadata.source = "service-mesh"
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 stable error-code registry for `@suaveplan/api-tenant-resolver-header`. Every public failure path raises a…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable error-code registry for `@suaveplan/api-tenant-resolver-header`. Every public failure path raises a…
- [instruments](./src/instruments/instruments.md) — Builds the OTel instrument bundle the header tenant resolver emits on every `resolve()` invocation. Centralising instru…
- [instruments](./src/instruments/instruments.md) — Builds the OTel instrument bundle the header tenant resolver emits on every `resolve()` invocation. Centralising instru…
- [options](./src/options/options.md) — Defines the Zod-validated options schema accepted by `createHeaderTenantResolver`. Construction-time validation guarant…
- [options](./src/options/options.md) — Defines the Zod-validated options schema accepted by `createHeaderTenantResolver`. Construction-time validation guarant…
- [resolver](./src/resolver/resolver.md) — Implements the Stage-8 tenant resolver factory `createHeaderTenantResolver`. The resolver reads the tenant identifier f…
- [resolver](./src/resolver/resolver.md) — Implements the Stage-8 tenant resolver factory `createHeaderTenantResolver`. The resolver reads the tenant identifier f…
- [trust](./src/trust/trust.md) — Encapsulates the trusted-source enforcement consulted by the header tenant resolver before it acts on a header value. T…
- [trust](./src/trust/trust.md) — Encapsulates the trusted-source enforcement consulted by the header tenant resolver before it acts on a header value. T…
- [validate](./src/validate/validate.md) — Owns the default tenant-id validator regex and the validator / registry function signatures consumed by `createHeaderTe…
- [validate](./src/validate/validate.md) — Owns the default tenant-id validator regex and the validator / registry function signatures consumed by `createHeaderTe…
<!-- end:auto-docs -->
## Why a dedicated package
The tenant-resolver chain (design doc §6 step 4) runs after the session
resolver. Service-to-service traffic frequently carries the tenant in an
HTTP header (`X-Tenant-Id`, `X-Acme-Tenant`, …) because no end-user session
exists to resolve it from. This adapter handles that case while preventing
header spoofing from public traffic via the trusted-source gate.
## Chain interaction
The host runs resolvers left-to-right; the first non-`null` result wins:
```text
[ api-tenant-resolver-jwt, api-tenant-resolver-header, api-tenant-resolver-subdomain ]
| | |
reads principal reads X-Tenant-Id reads sub.domain
(typical bearer) (admin / S2S calls) (public site routes)
```
`api-tenant-resolver-header` returns `null` when the header is absent, when
the request source is untrusted (non-strict), when the value fails
validation (non-strict), or when the value isn't in the registry
(non-strict). In strict mode each of those paths raises the corresponding
`TENANT_HEADER_*` code.
## Modules
- `./resolver` — `createHeaderTenantResolver`,
`CreateHeaderTenantResolverOptions`.
- `./options` — `HeaderTenantResolverOptions`,
`parseHeaderTenantResolverOptions`, `ResolvedHeaderTenantResolverOptions`.
- `./validate` — `defaultTenantIdValidator`, `DEFAULT_TENANT_ID_PATTERN`,
`TenantIdValidator`, `TenantRegistryProbe`.
- `./trust` — `defaultMetadataExtractor`, `isTrustedSource`,
`RequestMetadataExtractor`, `RequestTrustMetadata`.
- `./instruments` — `createHeaderResolverInstruments`,
`HeaderResolverInstruments`, `HeaderResolveOutcome`.
- `./error-codes` — `HeaderTenantResolverError`,
`HeaderTenantResolverErrorCode`, `TENANT_HEADER_CODES`.
## Options
| Option | Default | Effect |
| ---------------- | -------------------- | ---------------------------------------------------------------------------- |
| `id` | `"header"` | Resolver id used in chain logs and metric tags. |
| `headerName` | `"X-Tenant-Id"` | HTTP header consulted; case-insensitive. |
| `strict` | `false` | When `true`, failures raise typed errors. When `false`, failures yield `null`. |
| `trustedSources` | `["service-mesh"]` | Values for `request.metadata.source` that may set the tenant header. |
| `validate` | `^[a-z0-9-]{1,64}$` | Validator applied to the header value. |
| `registry` | `undefined` | Optional sync-or-async membership probe; runs only after validator passes. |
| `getMetadata` | reads `req.metadata` | Override for the trust-metadata extractor. |
| `instruments` | OTel global meter | Override for the instrument bundle. |
## Error codes
- `TENANT_HEADER.INVALID` — header value failed validator (strict-only).
- `TENANT_HEADER.NOT_FOUND_IN_REGISTRY` — value not in registry (strict-only).
- `TENANT_HEADER.UNTRUSTED_SOURCE` — request source not in `trustedSources`
(strict-only).
- `TENANT_HEADER.INVALID_OPTIONS` — construction-time Zod failure.
## Observability
- Counter `api.tenant.resolves_total{resolver="header", outcome}` — exactly
one increment per `resolve()` call. `outcome` is one of `resolved`,
`absent`, `invalid`, `not_found_in_registry`, `untrusted_source`.