# Tasks: enterprise-jwt-tenant **Change ID**: enterprise-jwt-tenant **Package**: `@suaveplan/jwt-tenant` **Target path**: `universal/auth/jwt-tenant` **Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/crypto`, `@suaveplan/crypto-keys`, `@suaveplan/retry` --- ## 1. Scaffold - [x] Create directory `universal/auth/jwt-tenant/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`. - [x] Write `package.json` at version `0.1.0` with: - `"type": "module"`, `"sideEffects": false` - `exports` map with top-level `"."` plus subpaths: `"./jwks/jwks-endpoint/jwks-endpoint"`, `"./jwks/in-memory-key-store/in-memory-key-store"`, `"./keys/key-generator/key-generator"`, `"./token/token-issuer/token-issuer"`, `"./token/token-verifier/token-verifier"`, `"./schemas/jwt-tenant.schemas"`, `"./types/jwt-tenant.types"` - `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/crypto workspace:^`, `@suaveplan/crypto-keys workspace:^`, `@suaveplan/retry workspace:^`, `jose catalog:auth`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build` - `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `mitata catalog:testing`, `@suaveplan/testing workspace:^` - [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`. - [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config` (pool: `forks`). - [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench` scripts to `package.json`. --- ## 2. Types and errors ### 2a. Types (`src/types/jwt-tenant.types.ts` + `.md`) - [x] Define all public interfaces and type aliases from `proposal.md`: `KeyAlgorithm`, `KeyPair`, `SerializedKeyPair`, `JWK`, `JWKS`, `TenantJWTPayload`, `TokenIssuerConfig`, `CreateTokenOptions`, `TokenVerifierConfig`, `VerifyTokenOptions`, `VerifyTokenResult`, `JWKSEndpointOptions`, `JWKSEndpointConfig`, `TenantKeyStore`, `KeyGenOptions`. - [x] Add genesis-only optional fields: `clock?: Clock` on `TokenIssuerConfig`, `TokenVerifierConfig`, `JWKSEndpointOptions`; `signal?: AbortSignal` and `retryOptions?: RetryOptions` on `TokenVerifierConfig`. - [x] Write `jwt-tenant.types.md` (≥200 words, API reference for every type). ### 2b. Error codes (`src/error-codes.ts`) - [x] Define `JwtTenantErrorCode` union — nine codes: `JWT_INVALID_SIGNATURE`, `JWT_EXPIRED`, `JWT_INVALID_CLAIMS`, `JWT_MISSING_TENANT`, `JWT_TENANT_MISMATCH`, `JWT_KEY_NOT_FOUND`, `JWT_JWKS_FETCH_FAILED`, `JWT_ALGORITHM_UNSUPPORTED`, `JWT_SERIALIZATION_FAILED`. - [x] Define `JWT_TENANT_ERROR_METADATA` with `httpStatus`, `severity`, `retryable`, `userMessageKey`, `category`, `description` per code. - [x] Export `registerJwtTenantErrorCodes()` as idempotent eager side-effect. - [x] Export `JwtTenantError extends SuaveplanError`. ### 2c. Zod schemas (`src/schemas/jwt-tenant.schemas.ts` + `.md`) - [x] Write Zod schemas: `TokenIssuerConfigSchema`, `CreateTokenOptionsSchema`, `TokenVerifierConfigSchema`, `VerifyTokenOptionsSchema`, `JWKSEndpointOptionsSchema`, `KeyGenOptionsSchema`, `SerializedKeyPairSchema`. - [x] Write `jwt-tenant.schemas.md`. - [x] Write `jwt-tenant.schemas.test.ts` — parse valid/invalid for each schema. --- ## 3. Key generation (`src/keys/key-generator/`) - [x] Implement `generateKeyPair(algorithm?, options?)`: - Use `jose`'s `generateKeyPair` under the hood. - Accept optional `{ clock?: Clock; idGenerator?: IdGenerator }` in `KeyGenOptions` for deterministic `keyId` and `createdAt`. - Instrument with `jwt.key_generate` span and `jwt.keygen_duration_ms` histogram. - [x] Implement `serializeKeyPair(keyPair)` — exports private + public key as JWK via `jose`; returns `SerializedKeyPair`. - [x] Implement `deserializeKeyPair(serialized)` — imports keys back via `jose`; throws `JwtTenantError` with `JWT_SERIALIZATION_FAILED` on failure. - [x] Implement `keyPairToJWK(keyPair)` — public key only (no `d` parameter in output). - [x] Implement `importPublicKeyFromJWK(jwk)`. - [x] Implement `calculateKeyExpiration(createdAt, ttlMs)`. - [x] Implement `isKeyPairExpired(keyPair, clock?)`. - [x] Implement `getAlgorithmFamily(algorithm)`. - [x] Write `key-generator.test.ts` — generation for each supported algorithm, serialize/deserialize round-trip, JWK public-only assertion (no private key leakage), expiry check with FakeClock, algorithm family mapping. Each `it` has `{ timeout: 30000 }`. - [x] Write `key-generator.md`. --- ## 4. In-memory key store (`src/jwks/in-memory-key-store/`) - [x] Implement `InMemoryKeyStore`: - `addKey(keyPair)` — stores in internal Map keyed by `keyId`. - `getKeyById(keyId)` — returns `null` if not found or key is expired (lazy check via `clock`). - `getActiveKeys(tenantId?)` — returns non-expired keys; if `tenantId` provided, filter by `payload.tenant_id` metadata (if stored). - `removeKey(keyId)`. - Constructor accepts optional `clock?: Clock`. - [x] Write `in-memory-key-store.test.ts` — add/get/remove keys, expiry with FakeClock, multi-tenant filtering, getActiveKeys excludes expired. Each `it` has `{ timeout: 5000 }`. - [x] Write `in-memory-key-store.md`. --- ## 5. JWKS endpoint (`src/jwks/jwks-endpoint/`) - [x] Implement `JWKSEndpoint(options)`: - `getJWKS(config?)` — calls `keyStore.getActiveKeys()`, maps to JWKs via `keyPairToJWK`, returns `JWKS`. Optionally includes expired keys if `config.includeExpired`. - `handleRequest(request)` — responds with `{ status: 200, body: JSON.stringify(jwks), headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=...' } }`. - Add `jwt.jwks_cache_hit_total` / `jwt.jwks_cache_miss_total` OTel counters (cache = whether the JWKS response was computed fresh or returned from an internal TTL cache). - [x] Implement `createInMemoryKeyStore`, `createJWKSEndpoint`, `formatJWKSResponse`. - [x] Write `jwks-endpoint.test.ts` — get JWKS with active/expired keys, `handleRequest` response shape, cache headers, empty key store. Each `it` has `{ timeout: 5000 }`. - [x] Write `jwks-endpoint.md`. --- ## 6. Token issuer (`src/token/token-issuer/`) - [x] Implement `TokenIssuer`: - `createToken(keyPair, options)`: - Build `TenantJWTPayload` from `CreateTokenOptions`. - Use injectable `clock.now()` for `iat`; compute `exp` from `expiresIn` string via duration parser. - Use `jose`'s `SignJWT` to sign. - Instrument with `jwt.sign` span and `jwt.tokens_signed_total` counter (label `algorithm`). - Throw `JwtTenantError` with `JWT_ALGORITHM_UNSUPPORTED` if algorithm is not in the supported set. - [x] Implement `createTokenIssuer(config)` factory. - [x] Implement standalone `signToken(keyPair, options, config?)`. - [x] Write `token-issuer.test.ts` — sign with each algorithm, payload fields preserved (tenantId, domains, role, permissions, sessionId, additionalClaims), expiry duration parsing, clock injection for deterministic iat/exp, unsupported algorithm error. Each `it` has `{ timeout: 30000 }`. - [x] Write `token-issuer.md`. --- ## 7. Token verifier (`src/token/token-verifier/`) - [x] Implement `TokenVerifier`: - `verifyToken(token, options?)`: - Decode protected header to get `kid`. - Resolve key from local key store (if `config.keyStore` is set) or fetch remote JWKS (if `config.jwksUri` is set). - Remote JWKS fetch: use `jose`'s `createRemoteJWKSet`; cache result for `config.cacheMaxAge` ms (TTL checked via injectable `clock`); pass `config.signal` to abort. Retry on network failure using `@suaveplan/retry`. - Verify signature, exp/nbf/iss/aud claims via `jose`'s `jwtVerify`. - Apply `VerifyTokenOptions` post-checks (requiredClaims, requiredRole, requiredPermissions, requiredTenantId, requiredDomains). - Map `jose` errors to `JwtTenantError` codes. - Instrument with `jwt.verify` span and `jwt.tokens_verified_total` counter (labels `valid`, `algorithm`). - Instrument JWKS fetch with `jwt.jwks_fetch` span and counter (label `cached`). - `clearCache()` — wipes the JWKS cache. - [x] Implement `createTokenVerifier(config)` factory. - [x] Implement `verifyTokenWithKey(token, key, options?)`. - [x] Write `token-verifier.test.ts` (≤500 lines) — valid token, expired token, wrong issuer, wrong audience, missing tenant, tenant mismatch, required role not present, required permissions subset, JWKS cache hit/miss with FakeClock, AbortSignal cancellation, retry on transient failure. `{ timeout: 30000 }`. - [x] Write `token-verifier.md`. --- ## 8. OTel instrumentation (cross-cutting review) - [x] Confirm all spans and counters from `proposal.md §4` are wired in each module. - [x] Add `tests/otel.test.ts` — in-memory exporter; assert counters increment for sign, verify (valid + invalid), keygen, JWKS cache hit/miss. --- ## 9. Property-based tests (`tests/property.test.ts`) Use `fast-check`. All test timeouts `{ timeout: 30000 }`. - [x] **Sign → verify round-trip**: for any valid `CreateTokenOptions`, `verifyToken(signToken(kp, opts))` returns `valid === true` with matching `tenant_id`. - [x] **Expired token detection**: token with `exp` in the past (FakeClock advanced) returns `valid === false`. - [x] **Serialization round-trip**: `deserializeKeyPair(serializeKeyPair(kp))` yields a key pair with the same `keyId` and `algorithm` that can sign/verify a round-trip token. - [x] **Tenant claim preservation**: `payload.tenant_id === options.tenantId` for all valid sign/verify pairs. --- ## 10. Benchmarks (`src/__benchmarks__/jwt-tenant.bench.ts`) Use `mitata`. - [x] `keygen_ES256` / `keygen_EdDSA`: 1 000 iterations each. - [x] `signToken_ES256`: 10 000 iterations. - [x] `verifyToken_cached`: 10 000 iterations (warm in-memory key store, no JWKS fetch). - [x] `verifyToken_cache_hit_vs_miss`: 10 000 iterations each with JWKS cache enabled vs fresh fetch path. - [x] Document placeholder numbers in `README.md` under `## Benchmarks`. --- ## 11. Docs Per-file `.md` (all ≥200 words): - [x] `src/types/jwt-tenant.types.md` - [x] `src/keys/key-generator/key-generator.md` - [x] `src/jwks/in-memory-key-store/in-memory-key-store.md` - [x] `src/jwks/jwks-endpoint/jwks-endpoint.md` - [x] `src/token/token-issuer/token-issuer.md` - [x] `src/token/token-verifier/token-verifier.md` - [x] `src/schemas/jwt-tenant.schemas.md` Root README: - [x] Write `universal/auth/jwt-tenant/README.md`: purpose, install, quick-start (generate key + sign + verify), multi-tenant JWKS endpoint guide, modules table, benchmarks placeholder. --- ## 12. Verify and publish - [x] Run `bunx turbo lint --filter=@suaveplan/jwt-tenant` — zero errors. - [x] Run `bunx turbo typecheck --filter=@suaveplan/jwt-tenant` — zero errors. - [x] Run `bunx turbo test --filter=@suaveplan/jwt-tenant` — all tests pass. - [x] Run `bunx turbo test:coverage --filter=@suaveplan/jwt-tenant` — 100/100/100/100. - [x] Run `bun run docs:check --filter=@suaveplan/jwt-tenant` — zero errors. - [x] Confirm no `TODO`, `FIXME`, `@deprecated`, untyped `any`, `ts-ignore`, or `.skip` in `src/` or `tests/`. - [x] Confirm `"sideEffects": false` in `package.json`. - [x] Run `bun scripts/publish.ts --package @suaveplan/jwt-tenant`. - [x] Update `CLAUDE.md` package listing to include `@suaveplan/jwt-tenant`. - [x] Archive: move `openspec/changes/enterprise-jwt-tenant/` to `openspec/archive/2026-04-24-enterprise-jwt-tenant/` and fold spec delta into `openspec/specs/jwt-tenant/spec.md`.