Initial Commit
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user