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>
7.0 KiB
@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.
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
Documentation tree
- error-codes — Defines the stable error-code registry for
@suaveplan/api-tenant-resolver-header. Every public failure path raises a…- error-codes — Defines the stable error-code registry for
@suaveplan/api-tenant-resolver-header. Every public failure path raises a…
- error-codes — Defines the stable error-code registry for
- instruments — Builds the OTel instrument bundle the header tenant resolver emits on every
resolve()invocation. Centralising instru…- instruments — Builds the OTel instrument bundle the header tenant resolver emits on every
resolve()invocation. Centralising instru…
- instruments — Builds the OTel instrument bundle the header tenant resolver emits on every
- options — Defines the Zod-validated options schema accepted by
createHeaderTenantResolver. Construction-time validation guarant…- options — Defines the Zod-validated options schema accepted by
createHeaderTenantResolver. Construction-time validation guarant…
- options — Defines the Zod-validated options schema accepted by
- resolver — Implements the Stage-8 tenant resolver factory
createHeaderTenantResolver. The resolver reads the tenant identifier f…- resolver — Implements the Stage-8 tenant resolver factory
createHeaderTenantResolver. The resolver reads the tenant identifier f…
- resolver — Implements the Stage-8 tenant resolver factory
- trust — Encapsulates the trusted-source enforcement consulted by the header tenant resolver before it acts on a header value. T…
- trust — Encapsulates the trusted-source enforcement consulted by the header tenant resolver before it acts on a header value. T…
- validate — Owns the default tenant-id validator regex and the validator / registry function signatures consumed by `createHeaderTe…
- validate — Owns the default tenant-id validator regex and the validator / registry function signatures consumed by `createHeaderTe…
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:
[ 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 intrustedSources(strict-only).TENANT_HEADER.INVALID_OPTIONS— construction-time Zod failure.
Observability
- Counter
api.tenant.resolves_total{resolver="header", outcome}— exactly one increment perresolve()call.outcomeis one ofresolved,absent,invalid,not_found_in_registry,untrusted_source.