64 lines
2.7 KiB
Markdown
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 -->
|