3.2 KiB
@suaveplan/api-tenant-resolver-jwt — Principal-derived tenant resolver
Package: @suaveplan/api-tenant-resolver-jwt
Status: PROPOSED
Type: new-package
Stage: 4 (Base adapters) — see umbrella openspec/changes/api-plugin-foundation/
Why
Per design doc §6 step 4, the tenant resolver chain runs after the session resolver. When the session is a JWT carrying tenant_id (or tid), the tenant resolver's job is trivial: read ctx.principal.tenantId. Collapsing this into a dedicated adapter — rather than encoding it inside api-auth-jwt — keeps the two contracts orthogonal and lets operators chain it with header- or subdomain-based resolvers (Stage 8) for paths the JWT doesn't cover.
When paired with api-auth-jwt, this resolver effectively degenerates to an identity read; its presence makes the data-flow explicit and lets the conformance suite test the empty/missing-tenant edge cases uniformly.
Stage 4 invariant: both direct and worker-proxy surfaces ship; both pass runTenantResolverConformance (§5.2a, §13.4).
What changes
Net-new package depending on @suaveplan/api-tenant-resolver (contract), @suaveplan/api-types, @suaveplan/api-worker-rpc, @suaveplan/error, zod.
- ADDED:
createJwtTenantResolver(opts)— direct surface returning aTenantResolver. Readsctx.principal.tenantId; returns it if present, otherwise returns the chain-sentinelundefined(so the next resolver in the chain runs). - ADDED:
createJwtTenantResolverWorkerProxy(port, opts)— proxy surface. - ADDED:
JwtTenantResolverOptions— Zod schema withrequirePrincipal: boolean(default false; if true, throwsTENANT_JWT_PRINCIPAL_REQUIREDwhen principal is null) andmode: "identity" | "claim-fallback"(default"identity"). - ADDED:
TENANT_JWT_*error codes (TENANT_JWT_PRINCIPAL_REQUIRED,TENANT_JWT_INVALID_OPTIONS).
Out of scope
- Reading tenant from arbitrary JWT claims independent of
Principalprojection — that responsibility belongs toapi-auth-jwt'sclaimToPrincipalmapping. - Header- or subdomain-based resolution —
api-tenant-resolver-headerandapi-tenant-resolver-subdomain(Stage 8). - Tenant enumeration (for
perTenant: truejobs) —api-tenant-provider-memory(this stage).
Source mapping
Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.
| Requirement | Design doc section |
|---|---|
| Tenant resolver chain step 4 | §6 |
| First match wins; sentinel for chain advance | §6 |
| Worker-proxy variant required | §5.2a |
| Conformance both surfaces | §13.4 |
Verification approach
src/factory/create-jwt-tenant-resolver.test.ts— principal with tenantId → returns it; principal without tenantId → returnsundefined(advance chain); null principal →undefinedunlessrequirePrincipal: true(throws).src/factory/create-jwt-tenant-resolver.spec-conformance.test.ts—runTenantResolverConformance("jwt-direct", ...).src/proxy/create-jwt-tenant-resolver-worker-proxy.test.ts— proxy unit tests.src/proxy/create-jwt-tenant-resolver-worker-proxy.spec-conformance.test.ts—runTenantResolverConformance("jwt-worker-proxy", ...).
Migration impact
None — additive only.