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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 00:58 +0800
1"""JWT key normalization utilities."""
3from __future__ import annotations
5from typing import Any
7from lexigram.validation import SecretStr
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`.
15 Rejects plain :class:`str` values with a :exc:`TypeError` so that callers
16 cannot accidentally embed secrets in tracebacks or log output.
18 Args:
19 keys: Raw key mapping to normalize.
21 Returns:
22 Normalized mapping with all string values wrapped in
23 :class:`~lexigram.validation.SecretStr`.
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
57__all__ = ["normalize_jwt_keys"]