# Capability Spec Delta: api-tenant-resolver-jwt (api-tenant-resolver-jwt) **Package**: `@suaveplan/api-tenant-resolver-jwt` **Status**: PROPOSED ## ADDED Requirements ### Requirement: Resolve tenant from Principal The package SHALL implement `TenantResolver` such that `resolve(request, principal)` returns `principal.tenantId` when present, returns `undefined` (chain-advance sentinel) when principal is non-null but has no `tenantId`, and returns `undefined` when principal is null (unless `requirePrincipal: true`). #### Scenario: principal with tenantId resolves directly - **WHEN** `principal = { id: "u1", tenantId: "acme", scopes: [], claims: {} }` - **THEN** `resolve()` returns `"acme"` #### Scenario: principal without tenantId advances the chain - **WHEN** `principal = { id: "u1", scopes: [], claims: {} }` (no tenantId) - **THEN** `resolve()` returns `undefined` so the next resolver in the chain runs #### Scenario: null principal advances the chain by default - **WHEN** `principal === null` AND `requirePrincipal: false` - **THEN** `resolve()` returns `undefined` #### Scenario: requirePrincipal: true throws when principal is null - **WHEN** `principal === null` AND `requirePrincipal: true` - **THEN** `resolve()` throws `TENANT_JWT_PRINCIPAL_REQUIRED` **Verified by:** `src/factory/create-jwt-tenant-resolver.test.ts::"returns principal.tenantId when present"` AND `src/factory/create-jwt-tenant-resolver.test.ts::"returns undefined when tenantId missing"` AND `src/factory/create-jwt-tenant-resolver.test.ts::"throws when requirePrincipal and principal is null"` --- ### Requirement: Direct and worker-proxy surfaces both pass conformance #### Scenario: direct conforms - **WHEN** `runTenantResolverConformance("jwt-direct", () => createJwtTenantResolver(...))` runs - **THEN** every invariant passes #### Scenario: worker-proxy conforms - **WHEN** `runTenantResolverConformance("jwt-worker-proxy", ...)` runs over a real `MessageChannel` - **THEN** every invariant passes **Verified by:** `src/factory/create-jwt-tenant-resolver.spec-conformance.test.ts::"jwt-direct conforms to TenantResolver contract"` AND `src/proxy/create-jwt-tenant-resolver-worker-proxy.spec-conformance.test.ts::"jwt-worker-proxy conforms to TenantResolver contract"` --- ### Requirement: Options Zod-validated #### Scenario: invalid options throw at construction - **WHEN** `createJwtTenantResolver({ mode: "bogus" as never })` - **THEN** a `SuaveplanError` with code `TENANT_JWT_INVALID_OPTIONS` is thrown synchronously **Verified by:** `src/options/jwt-tenant-resolver-options.test.ts::"throws on invalid mode value"`