69 lines
2.9 KiB
Markdown
69 lines
2.9 KiB
Markdown
# `api-host` — Foundation gap 5: configurable default tenant
|
|
|
|
**Package**: `@suaveplan/api-host`
|
|
**Status**: PROPOSED
|
|
**Type**: behavior-change
|
|
|
|
## Why
|
|
|
|
`packages/api-stack/api/api-host/src/request-pipeline/request-pipeline.ts`
|
|
hard-coded a `{id: "public" as TenantId, slug: "public"}` fallback
|
|
whenever the configured `TenantResolverChain` returned `null` on an
|
|
auth-optional route. The `"public"` literal became load-bearing — every
|
|
handler that introspected `ctx.tenantId` had to treat the string
|
|
`"public"` as a sentinel rather than receiving an honest absence.
|
|
|
|
This blocked deployments that:
|
|
|
|
- Genuinely run multi-tenant traffic where there is no public tenant
|
|
row in the database, and want resolver miss → `400` not silent fallback.
|
|
- Run an actual tenant with `id="public"` and need to distinguish "the
|
|
literal public tenant" from "no tenant resolved".
|
|
- Want to defer the policy choice (`null` vs. stub vs. error) to the
|
|
operator's bootstrap module rather than the framework.
|
|
|
|
## What changes
|
|
|
|
ADDED: `HostConfig.defaultTenant?: TenantContext | null` and
|
|
`RequestPipelineOptions.defaultTenant?: TenantContext | null`.
|
|
|
|
MODIFIED: §6 step 1 ("Tenant resolve") of the request pipeline.
|
|
|
|
- When the resolver chain returns `null` AND the route's `auth` policy
|
|
is `undefined`/`null` (auth-optional):
|
|
- `defaultTenant: undefined` (the new default) → `ctx.tenant === null`,
|
|
`ctx.tenantId === null`.
|
|
- `defaultTenant: null` (explicit) → identical to `undefined`.
|
|
- `defaultTenant: TenantContext` → inject verbatim. This preserves
|
|
pre-foundation-gap-5 behaviour for migrators by setting
|
|
`{ id: "public", slug: "public" }`.
|
|
|
|
- When the route's `auth` is required AND the resolver chain misses,
|
|
the pipeline still raises `HOST.TENANT_NOT_RESOLVED` (400) regardless
|
|
of `defaultTenant` — the fallback is a convenience for public routes,
|
|
not a silent bypass of tenant-required policy.
|
|
|
|
Downstream pipeline steps now tolerate `tenantCtx === null`:
|
|
|
|
- Rate-limit key falls back to `"anon"` when no principal AND no tenant
|
|
is bound.
|
|
- Per-request logger meta carries `tenantId: null`.
|
|
- `succeed()` and `fail()` propagate `tenantId: null` into
|
|
`PipelineResult.tenantId`.
|
|
- `PipelineCapabilityResolver.forRequest` accepts
|
|
`tenant: TenantContext | null`.
|
|
|
|
`RequestContext.tenant` / `RequestContext.tenantId` remain declared as
|
|
non-nullable in `@suaveplan/api-types`; handlers consuming tenant-bound
|
|
capabilities MUST gate on `tenant === null` first when their host is
|
|
configured with `defaultTenant: null`. The contract is documented in
|
|
`host.md` and `request-pipeline.types.ts`.
|
|
|
|
## Migration
|
|
|
|
Operators relying on the legacy `"public"` stub MUST set
|
|
`defaultTenant: { id: "public", slug: "public" }` in `createHost(...)`.
|
|
The E2E foundation-stack server has been migrated this way to keep its
|
|
existing `tenantId === "public"` assertion green; new deployments SHOULD
|
|
leave the field unset.
|