3.6 KiB
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:
TenantRecordgains requiredslug: string+ optionalmetadata. Both are mirrored fromTenantContext.tenantRecordSchemais updated; every in-tree fixture passes the slug explicitly so theslug = idsynthesis bug cannot resurface.@suaveplan/api-tenant-provider/chainshipstenantResolverChainFromProvider(provider, opts?)+recordToContext(record). Three built-in strategies in fixed order (header / JWT-claim / subdomain), optional LRU memoisation, first-match-wins semantics. Returns aTenantResolverChainconforming to@suaveplan/api-types/tenant-resolver.- 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—TenantRecordgains requiredslugand optionalmetadata. 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./chainsubpath export withtenantResolverChainFromProvider+recordToContext.@suaveplan/api-tenant-provider-memory— passes theslugthrough unchanged; all internal tests updated.@suaveplan/e2e-foundation-stack—buildTenantChain()now wraps the provider via the new factory; in-line fixtures gainslug: "tenant-1" | "tenant-2".@suaveplan/api-tenant-resolver-jwt— unchanged; still synthesisesslug = tenantIdbecause 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:
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}