Files
2026-05-27 23:29:53 +01:00

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 a TenantResolver. Reads ctx.principal.tenantId; returns it if present, otherwise returns the chain-sentinel undefined (so the next resolver in the chain runs).
  • ADDED: createJwtTenantResolverWorkerProxy(port, opts) — proxy surface.
  • ADDED: JwtTenantResolverOptions — Zod schema with requirePrincipal: boolean (default false; if true, throws TENANT_JWT_PRINCIPAL_REQUIRED when principal is null) and mode: "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 Principal projection — that responsibility belongs to api-auth-jwt's claimToPrincipal mapping.
  • Header- or subdomain-based resolution — api-tenant-resolver-header and api-tenant-resolver-subdomain (Stage 8).
  • Tenant enumeration (for perTenant: true jobs) — 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 → returns undefined (advance chain); null principal → undefined unless requirePrincipal: 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.