Coverage for src/lexigram/auth/authn/_key_utils.py: 82%

22 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-26 00:58 +0800

1"""JWT key normalization utilities.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from lexigram.validation import SecretStr 

8 

9 

10def normalize_jwt_keys( 

11 keys: dict[str, Any], 

12) -> dict[str, SecretStr | dict[str, SecretStr]]: 

13 """Coerce all JWT key material to :class:`~lexigram.validation.SecretStr`. 

14 

15 Rejects plain :class:`str` values with a :exc:`TypeError` so that callers 

16 cannot accidentally embed secrets in tracebacks or log output. 

17 

18 Args: 

19 keys: Raw key mapping to normalize. 

20 

21 Returns: 

22 Normalized mapping with all string values wrapped in 

23 :class:`~lexigram.validation.SecretStr`. 

24 

25 Raises: 

26 TypeError: If any key material is a plain ``str`` or an unsupported type. 

27 """ 

28 result: dict[str, SecretStr | dict[str, SecretStr]] = {} 

29 for k, v in keys.items(): 

30 if isinstance(v, dict): 

31 inner: dict[str, SecretStr] = {} 

32 for sk, sv in v.items(): 

33 if isinstance(sv, str): 

34 raise TypeError( 

35 f"JWTTokenManager: key '{k}.{sk}' must be SecretStr, got str. " 

36 "Pass SecretStr('your-secret') instead of plain strings." 

37 ) 

38 if not isinstance(sv, SecretStr): 

39 raise TypeError( 

40 f"JWTTokenManager: key '{k}.{sk}' must be SecretStr, got {type(sv).__name__}." 

41 ) 

42 inner[sk] = sv 

43 result[k] = inner 

44 elif isinstance(v, str): 

45 raise TypeError( 

46 f"JWTTokenManager: key '{k}' must be SecretStr, got str. " 

47 "Pass SecretStr('your-secret') instead of plain strings." 

48 ) 

49 elif not isinstance(v, SecretStr): 

50 raise TypeError( 

51 f"JWTTokenManager: key '{k}' must be SecretStr, got {type(v).__name__}." 

52 ) 

53 result[k] = v 

54 return result 

55 

56 

57__all__ = ["normalize_jwt_keys"]