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