89 lines
3.6 KiB
Markdown
89 lines
3.6 KiB
Markdown
# foundation-fix-gap-2-tenant-chain
|
|
|
|
- **Status**: PROPOSED
|
|
- **Spec deltas**:
|
|
- ./specs/api-tenant-provider/spec.md
|
|
- ./specs/api-tenant-provider-memory/spec.md
|
|
|
|
## What & why
|
|
|
|
The post-Stage-4 foundation-coherence audit (Gap #2) flagged a shape
|
|
divergence between `TenantRecord` (the inventory row exposed by
|
|
`@suaveplan/api-tenant-provider`) and `TenantContext` (the per-request
|
|
shape exposed by `@suaveplan/api-types/tenant`): `TenantContext`
|
|
requires `slug`, but `TenantRecord` did not carry one. Every
|
|
`TenantProvider`-backed resolver was forced to synthesise
|
|
`slug = String(record.id)` at the boundary — the e2e foundation server
|
|
did exactly that, and the `api-tenant-resolver-jwt` resolver hardcodes
|
|
`slug = tenantId` for the same reason.
|
|
|
|
A second half of the gap: hosts that already have a `TenantProvider`
|
|
have to hand-roll the `header -> provider.lookup -> TenantContext`
|
|
chain at the start of every fixture or server. No reusable factory
|
|
existed.
|
|
|
|
This change closes both halves:
|
|
|
|
1. **`TenantRecord` gains required `slug: string` + optional `metadata`.**
|
|
Both are mirrored from `TenantContext`. `tenantRecordSchema` is
|
|
updated; every in-tree fixture passes the slug explicitly so the
|
|
`slug = id` synthesis bug cannot resurface.
|
|
2. **`@suaveplan/api-tenant-provider/chain` ships
|
|
`tenantResolverChainFromProvider(provider, opts?)` +
|
|
`recordToContext(record)`.** Three built-in strategies in fixed
|
|
order (header / JWT-claim / subdomain), optional LRU memoisation,
|
|
first-match-wins semantics. Returns a `TenantResolverChain`
|
|
conforming to `@suaveplan/api-types/tenant-resolver`.
|
|
3. **e2e/foundation-stack/src/server.ts** drops the hand-rolled chain
|
|
in favour of the factory, and the in-line fixtures gain explicit
|
|
slugs.
|
|
|
|
## Impact
|
|
|
|
### Affected dependents
|
|
|
|
- `@suaveplan/api-tenant-provider` — `TenantRecord` gains required
|
|
`slug` and optional `metadata`. Source-incompatible: existing
|
|
literal-constructed records without slug fail typecheck and
|
|
schema parse. In-tree consumers are updated as part of this change
|
|
(memory adapter, e2e fixture, all tests). Adds `./chain` subpath
|
|
export with `tenantResolverChainFromProvider` + `recordToContext`.
|
|
- `@suaveplan/api-tenant-provider-memory` — passes the `slug` through
|
|
unchanged; all internal tests updated.
|
|
- `@suaveplan/e2e-foundation-stack` — `buildTenantChain()` now wraps
|
|
the provider via the new factory; in-line fixtures gain
|
|
`slug: "tenant-1" | "tenant-2"`.
|
|
- `@suaveplan/api-tenant-resolver-jwt` — unchanged; still synthesises
|
|
`slug = tenantId` because the JWT carries only the id, not a slug.
|
|
Documenting this as deliberate (the JWT resolver has no inventory
|
|
lookup, so it cannot read slug from a record).
|
|
|
|
### Backward compatibility
|
|
|
|
`TenantRecord.slug` is REQUIRED — no v0.1.0 compatibility shim. Genesis
|
|
is bottom-up rebuild; no shipped consumer constructs `TenantRecord`
|
|
outside the packages updated in this change.
|
|
|
|
`tenantResolverChainFromProvider` is purely additive: a new subpath
|
|
export. No existing import path moves.
|
|
|
|
## Parity Checklist
|
|
|
|
This is a genesis-original fix; no upstream parity table required.
|
|
|
|
## Verification
|
|
|
|
`bunx turbo lint typecheck test build --filter=@suaveplan/api-tenant-provider --filter=@suaveplan/api-tenant-provider-memory --filter=@suaveplan/api-tenant-resolver-jwt`
|
|
→ green.
|
|
|
|
`bunx turbo test:coverage --filter=@suaveplan/api-tenant-provider --filter=@suaveplan/api-tenant-provider-memory`
|
|
→ 100/100/100/100 statements/branches/functions/lines.
|
|
|
|
E2E smoke against the new chain:
|
|
|
|
```bash
|
|
bun run src/server.ts &
|
|
curl -sH 'x-tenant-id: t1' http://127.0.0.1:3500/api/echo/tenanted
|
|
# => {"msg":"tenanted","tenantId":"t1","principal":null}
|
|
```
|