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

64 lines
2.7 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# 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"`
<!-- markdownlint-enable MD024 -->