identity-plan-kit 0.2.4__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/PKG-INFO +198 -1
  2. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/README.md +197 -0
  3. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/pyproject.toml +2 -2
  4. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/handlers/oauth_routes.py +3 -1
  5. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/repositories/token_repo.py +10 -4
  6. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/repositories/user_repo.py +11 -1
  7. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/services/auth_service.py +37 -18
  8. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/config.py +19 -0
  9. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/kit.py +35 -2
  10. identity_plan_kit-0.2.5/src/identity_plan_kit/plans/cache/plan_cache.py +236 -0
  11. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/domain/exceptions.py +30 -0
  12. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/handlers/plan_routes.py +4 -0
  13. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/repositories/plan_repo.py +12 -1
  14. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/repositories/usage_repo.py +32 -28
  15. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/services/plan_service.py +342 -7
  16. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/security.py +80 -0
  17. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_security_comprehensive.py +108 -0
  18. identity_plan_kit-0.2.5/tests/integration/test_concurrent_limit_merge.py +391 -0
  19. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/test_concurrent_token_operations.py +4 -3
  20. identity_plan_kit-0.2.5/tests/integration/test_concurrent_user_deactivation.py +448 -0
  21. identity_plan_kit-0.2.5/tests/integration/test_plan_cache_race.py +404 -0
  22. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/test_quota_concurrency.py +8 -5
  23. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/plans/test_plan_cache_concurrent.py +116 -0
  24. identity_plan_kit-0.2.5/tests/plans/test_plan_service.py +897 -0
  25. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/uv.lock +1 -1
  26. identity_plan_kit-0.2.4/src/identity_plan_kit/plans/cache/plan_cache.py +0 -143
  27. identity_plan_kit-0.2.4/tests/plans/test_plan_service.py +0 -431
  28. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/.gitignore +0 -0
  29. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/Makefile +0 -0
  30. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/alembic/env.py +0 -0
  31. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/alembic/script.py.mako +0 -0
  32. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/alembic/versions/20250124_000000_initial_schema.py +0 -0
  33. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/alembic/versions/20250127_000000_add_password_hash.py +0 -0
  34. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/alembic.ini +0 -0
  35. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/integration/__init__.py +0 -0
  36. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/integration/fastapi_integration.py +0 -0
  37. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/integration/sample_alembic_env.py +0 -0
  38. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/integration/webhook_integration.py +0 -0
  39. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/playing/admin_usage.py +0 -0
  40. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/playing/basic_usage.py +0 -0
  41. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/playing/custom_error_formats.py +0 -0
  42. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/playing/extending_models.html +0 -0
  43. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/playing/extension_example.py +0 -0
  44. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/prod/.env.production.example +0 -0
  45. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/prod/Dockerfile +0 -0
  46. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/prod/docker-compose.yml +0 -0
  47. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/prod/nginx.conf +0 -0
  48. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/prod/production_setup.py +0 -0
  49. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/examples/prod/prometheus.yml +0 -0
  50. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/README.md +0 -0
  51. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/__init__.py +0 -0
  52. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/config.py +0 -0
  53. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/locustfile.py +0 -0
  54. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/test_auth.py +0 -0
  55. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/test_database_stress.py +0 -0
  56. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/test_mixed_scenarios.py +0 -0
  57. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/test_plans_quota.py +0 -0
  58. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/test_quota_direct.py +0 -0
  59. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/test_rbac_cache.py +0 -0
  60. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/loadtests/utils.py +0 -0
  61. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/__init__.py +0 -0
  62. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/admin/__init__.py +0 -0
  63. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/admin/auth.py +0 -0
  64. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/admin/views.py +0 -0
  65. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/__init__.py +0 -0
  66. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/dependencies.py +0 -0
  67. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/domain/__init__.py +0 -0
  68. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/domain/entities.py +0 -0
  69. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/domain/exceptions.py +0 -0
  70. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/dto/__init__.py +0 -0
  71. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/dto/requests.py +0 -0
  72. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/dto/responses.py +0 -0
  73. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/handlers/__init__.py +0 -0
  74. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/models/__init__.py +0 -0
  75. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/models/refresh_token.py +0 -0
  76. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/models/user.py +0 -0
  77. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/models/user_provider.py +0 -0
  78. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/repositories/__init__.py +0 -0
  79. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/services/__init__.py +0 -0
  80. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/services/oauth_service.py +0 -0
  81. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/auth/uow.py +0 -0
  82. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/cli.py +0 -0
  83. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/migrations.py +0 -0
  84. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/__init__.py +0 -0
  85. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/cache/__init__.py +0 -0
  86. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/dependencies.py +0 -0
  87. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/domain/__init__.py +0 -0
  88. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/domain/entities.py +0 -0
  89. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/dto/__init__.py +0 -0
  90. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/dto/responses.py +0 -0
  91. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/dto/usage.py +0 -0
  92. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/handlers/__init__.py +0 -0
  93. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/models/__init__.py +0 -0
  94. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/models/feature.py +0 -0
  95. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/models/feature_usage.py +0 -0
  96. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/models/plan.py +0 -0
  97. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/models/plan_limit.py +0 -0
  98. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/models/plan_permission.py +0 -0
  99. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/models/user_plan.py +0 -0
  100. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/repositories/__init__.py +0 -0
  101. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/services/__init__.py +0 -0
  102. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/plans/uow.py +0 -0
  103. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/__init__.py +0 -0
  104. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/cache/__init__.py +0 -0
  105. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/cache/permission_cache.py +0 -0
  106. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/dependencies.py +0 -0
  107. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/domain/__init__.py +0 -0
  108. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/domain/entities.py +0 -0
  109. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/domain/exceptions.py +0 -0
  110. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/models/__init__.py +0 -0
  111. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/models/permission.py +0 -0
  112. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/models/role.py +0 -0
  113. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/models/role_permission.py +0 -0
  114. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/repositories/__init__.py +0 -0
  115. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/repositories/rbac_repo.py +0 -0
  116. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/services/__init__.py +0 -0
  117. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/services/rbac_service.py +0 -0
  118. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/rbac/uow.py +0 -0
  119. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/__init__.py +0 -0
  120. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/audit.py +0 -0
  121. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/circuit_breaker.py +0 -0
  122. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/cleanup_scheduler.py +0 -0
  123. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/database.py +0 -0
  124. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/error_formatter.py +0 -0
  125. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/exception_handlers.py +0 -0
  126. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/exceptions.py +0 -0
  127. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/graceful_shutdown.py +0 -0
  128. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/health.py +0 -0
  129. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/http_utils.py +0 -0
  130. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/lockout.py +0 -0
  131. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/logging.py +0 -0
  132. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/metrics.py +0 -0
  133. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/models.py +0 -0
  134. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/rate_limiter.py +0 -0
  135. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/request_id.py +0 -0
  136. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/schemas.py +0 -0
  137. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/state_store.py +0 -0
  138. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/uow.py +0 -0
  139. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/src/identity_plan_kit/shared/uuid7.py +0 -0
  140. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/__init__.py +0 -0
  141. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/__init__.py +0 -0
  142. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_auth_service.py +0 -0
  143. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_circuit_breaker.py +0 -0
  144. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_csrf_state.py +0 -0
  145. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_dependencies.py +0 -0
  146. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_domain_entities.py +0 -0
  147. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_http_utils.py +0 -0
  148. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_lockout.py +0 -0
  149. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_lockout_concurrent.py +0 -0
  150. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_oauth_service_resilience.py +0 -0
  151. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/auth/test_token_security.py +0 -0
  152. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/conftest.py +0 -0
  153. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/__init__.py +0 -0
  154. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/conftest.py +0 -0
  155. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/test_concurrent_plan_operations.py +0 -0
  156. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/test_concurrent_user_creation.py +0 -0
  157. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/test_limit_boundaries.py +0 -0
  158. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/test_token_cleanup.py +0 -0
  159. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/test_token_repository.py +0 -0
  160. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/test_usage_periods.py +0 -0
  161. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/integration/test_user_repository.py +0 -0
  162. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/plans/__init__.py +0 -0
  163. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/plans/test_domain_entities.py +0 -0
  164. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/plans/test_period_edge_cases.py +0 -0
  165. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/rbac/__init__.py +0 -0
  166. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/rbac/test_permission_cache.py +0 -0
  167. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/rbac/test_permission_cache_concurrent.py +0 -0
  168. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/rbac/test_rbac_cache_stampede.py +0 -0
  169. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/rbac/test_rbac_service.py +0 -0
  170. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/shared/test_audit_pii_masking.py +0 -0
  171. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/shared/test_circuit_breaker_concurrent.py +0 -0
  172. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/shared/test_cleanup_scheduler_concurrent.py +0 -0
  173. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/shared/test_config_validation.py +0 -0
  174. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/shared/test_http_utils_security.py +0 -0
  175. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/shared/test_rate_limiter.py +0 -0
  176. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/shared/test_rate_limiter_concurrent.py +0 -0
  177. {identity_plan_kit-0.2.4 → identity_plan_kit-0.2.5}/tests/shared/test_state_store_concurrent.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: identity-plan-kit
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Modern FastAPI library for authentication, RBAC, subscription plans, and usage tracking
5
5
  Author-email: harut <harut.avetisyan2002@gmail.com>
6
6
  License: MIT
@@ -73,6 +73,183 @@ Modern FastAPI library for authentication, RBAC, subscription plans, and usage t
73
73
  - **Account Lockout**: Brute-force protection
74
74
  - **CLI Tools**: Database migrations and management
75
75
 
76
+ ---
77
+
78
+ ## What IPK Provides vs What You Must Implement
79
+
80
+ ### Provided Out of the Box
81
+
82
+ | Feature | Description |
83
+ |---------|-------------|
84
+ | **Google OAuth Authentication** | Complete OAuth 2.0 flow with CSRF protection |
85
+ | **JWT Token Management** | Access/refresh token generation, validation, and rotation |
86
+ | **Token Theft Detection** | Automatic account deactivation when revoked token is reused |
87
+ | **Default Roles** | Pre-configured `admin` and `user` roles |
88
+ | **Default Plans** | Pre-configured `free` and `pro` plans |
89
+ | **Permission Infrastructure** | `check_permission()`, `require_permission()` methods |
90
+ | **Atomic Quota Checking** | Race-condition safe quota consumption (TOCTOU protected) |
91
+ | **Account Lockout** | Brute-force protection with configurable thresholds |
92
+ | **Token Cleanup** | Automatic expired token removal |
93
+ | **Health Checks** | Kubernetes-ready `/health/live` and `/health/ready` probes |
94
+ | **Graceful Shutdown** | Request draining before shutdown |
95
+ | **Database Migrations** | All tables created via `ipk db upgrade` |
96
+ | **Prometheus Metrics** | Optional observability (requires `[metrics]` extra) |
97
+ | **Audit Logging** | Security events logged automatically |
98
+ | **Idempotent Token Refresh** | Duplicate refresh requests return same tokens (30s window) |
99
+ | **Idempotent Quota Consumption** | When `idempotency_key` provided, prevents double-deduction |
100
+ | **Rate Limiting** | Configurable per-endpoint rate limits |
101
+
102
+ ### You Must Implement
103
+
104
+ | Responsibility | Why | Example |
105
+ |----------------|-----|---------|
106
+ | **Define Features** | IPK doesn't know what features your app has | `"api_calls"`, `"ai_generations"`, `"storage_gb"` |
107
+ | **Assign Features to Plans** | Business decision about what's included | Free: 100 API calls, Pro: unlimited |
108
+ | **Set Plan Limits** | Your pricing/quota decisions | `{"api_calls": 1000, "ai_generations": 50}` |
109
+ | **Define Permission Codes** | App-specific access control | `"admin.users.delete"`, `"reports.export"` |
110
+ | **Assign Permissions to Roles** | Which roles can do what | Admin gets `"admin.*"`, User gets `"feature.use"` |
111
+ | **Payment Webhook Integration** | IPK doesn't handle payments | Call `plan_service.assign_plan()` from Stripe webhook |
112
+ | **Webhook Deduplication** | Payment providers retry webhooks | Check `event_id` before processing (see below) |
113
+ | **Quota Checks in Routes** | IPK doesn't auto-enforce quotas | Call `check_and_consume_quota()` in your endpoints |
114
+ | **Authorization for Plan Ops** | IPK doesn't verify callers by default | Verify webhook signatures, check admin role |
115
+
116
+ ---
117
+
118
+ ## Critical Warnings
119
+
120
+ ### Production Deployments (Multi-Instance)
121
+
122
+ > **Redis is REQUIRED for multi-instance deployments**
123
+
124
+ Without Redis, each instance has its own:
125
+ - OAuth state storage (login fails if callback hits different instance)
126
+ - Rate limit counters (users bypass limits via different instances)
127
+ - Permission cache (inconsistent permissions across instances)
128
+
129
+ ```bash
130
+ # Set Redis URL for production
131
+ IPK_REDIS_URL=redis://localhost:6379/0
132
+ IPK_REQUIRE_REDIS=true # Fail-fast if Redis unavailable
133
+ ```
134
+
135
+ ### Token Theft Protection
136
+
137
+ When a refresh token is used **after being revoked**, IPK assumes token theft:
138
+ 1. ALL tokens for that user are revoked
139
+ 2. User account is **automatically deactivated**
140
+ 3. User must contact support to reactivate
141
+
142
+ This is intentional security behavior, not a bug.
143
+
144
+ ### Webhook Idempotency (NOT Auto-Handled)
145
+
146
+ **Plan management methods do NOT handle idempotency.** If your webhook provider retries requests (Stripe retries on timeout), duplicate calls will create issues.
147
+
148
+ **You MUST implement deduplication:**
149
+
150
+ ```python
151
+ @app.post("/webhooks/stripe")
152
+ async def stripe_webhook(request: Request):
153
+ event = stripe.Webhook.construct_event(...)
154
+
155
+ # CRITICAL: Check if already processed
156
+ if await is_event_processed(event.id):
157
+ return {"status": "already_processed"}
158
+
159
+ if event.type == "checkout.session.completed":
160
+ await kit.plan_service.assign_plan(
161
+ user_id=UUID(event.data.object.client_reference_id),
162
+ plan_code="pro",
163
+ )
164
+
165
+ await mark_event_processed(event.id)
166
+ return {"status": "ok"}
167
+ ```
168
+
169
+ ### Authorization is Your Responsibility
170
+
171
+ Plan management methods (`assign_plan`, `cancel_plan`, `update_plan_limits`, `reset_usage`) are **privileged operations**. IPK does NOT verify the caller is authorized.
172
+
173
+ **You MUST verify authorization before calling:**
174
+
175
+ ```python
176
+ # Option 1: Verify in your code before calling
177
+ @app.post("/admin/users/{user_id}/plan")
178
+ async def admin_assign_plan(user_id: UUID, current_user: User = Depends(CurrentUser(kit))):
179
+ # Verify caller is admin
180
+ await kit.rbac_service.require_permission(
181
+ current_user.id, current_user.role_id, "admin.plans.manage"
182
+ )
183
+ await kit.plan_service.assign_plan(user_id=user_id, plan_code="pro")
184
+
185
+ # Option 2: Configure authorization callback
186
+ async def check_authorization(operation: str, target_user_id: UUID, context: dict | None) -> bool:
187
+ if context and context.get("is_webhook"):
188
+ return True # Verified webhooks are authorized
189
+ if context and context.get("is_admin"):
190
+ return True
191
+ return False
192
+
193
+ kit = IdentityPlanKit(config, authorization_callback=check_authorization)
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Integration Examples
199
+
200
+ ### Quota Tracking in Routes
201
+
202
+ ```python
203
+ @app.post("/generate")
204
+ async def generate(user: User = Depends(CurrentUser(kit))):
205
+ usage = await kit.plan_service.check_and_consume_quota(
206
+ user_id=user.id,
207
+ feature_code="ai_generations",
208
+ amount=1,
209
+ idempotency_key=request.headers.get("X-Idempotency-Key"), # Safe retries
210
+ )
211
+
212
+ if not usage.has_access:
213
+ raise HTTPException(
214
+ status_code=429,
215
+ detail=f"Quota exceeded: {usage.used}/{usage.limit}"
216
+ )
217
+
218
+ # Your feature logic here
219
+ return {"remaining": usage.remaining}
220
+ ```
221
+
222
+ ### Permission Enforcement
223
+
224
+ ```python
225
+ @app.delete("/admin/users/{user_id}")
226
+ async def delete_user(user_id: UUID, current_user: User = Depends(CurrentUser(kit))):
227
+ await kit.rbac_service.require_permission(
228
+ user_id=current_user.id,
229
+ role_id=current_user.role_id,
230
+ permission_code="admin.users.delete",
231
+ )
232
+ # Delete logic here
233
+ ```
234
+
235
+ ### Idempotency for Quota Consumption
236
+
237
+ ```python
238
+ # Generate a unique key per logical operation
239
+ idempotency_key = f"{user.id}:{request_id}:generate_image"
240
+
241
+ # First request: checks quota, consumes 1, caches result
242
+ # Retry request: returns cached result, no double-deduction
243
+ result = await kit.plan_service.check_and_consume_quota(
244
+ user_id=user.id,
245
+ feature_code="ai_generation",
246
+ amount=1,
247
+ idempotency_key=idempotency_key,
248
+ )
249
+ ```
250
+
251
+ ---
252
+
76
253
  ## Installation
77
254
 
78
255
  ```bash
@@ -407,6 +584,26 @@ All configuration is via environment variables or `IdentityPlanKitConfig`:
407
584
  | `IPK_METRICS_PATH` | Path for metrics endpoint | `/metrics` |
408
585
  | `IPK_ENABLE_AUTO_CLEANUP` | Auto cleanup expired tokens | `true` |
409
586
  | `IPK_CLEANUP_INTERVAL_HOURS` | Cleanup interval in hours | `6.0` |
587
+ | `IPK_DATABASE_STATEMENT_TIMEOUT_MS` | PostgreSQL statement timeout | `30000` |
588
+ | `IPK_TOKEN_REFRESH_IDEMPOTENCY_TTL_SECONDS` | Token refresh idempotency window | `30` |
589
+ | `IPK_QUOTA_IDEMPOTENCY_TTL_SECONDS` | Quota consumption idempotency window | `60` |
590
+ | `IPK_REQUIRE_REDIS` | Fail if Redis unavailable | Auto (true in prod) |
591
+
592
+ ### Important Configuration Notes
593
+
594
+ **Database Statement Timeout**: Long-running queries are killed after 30s by default. Adjust for migrations:
595
+
596
+ ```bash
597
+ # For API servers (shorter timeout)
598
+ IPK_DATABASE_STATEMENT_TIMEOUT_MS=10000 # 10 seconds
599
+
600
+ # For migration scripts (longer timeout)
601
+ IPK_DATABASE_STATEMENT_TIMEOUT_MS=300000 # 5 minutes
602
+ ```
603
+
604
+ **Token Refresh Idempotency**: Duplicate refresh requests within 30s return the same tokens. This prevents "token revoked" errors when clients retry due to network issues.
605
+
606
+ **Quota Idempotency**: When `idempotency_key` is provided to `check_and_consume_quota()`, duplicate requests within 60s return cached results without double-deducting.
410
607
 
411
608
  ## Prometheus Metrics (Optional)
412
609
 
@@ -13,6 +13,183 @@ Modern FastAPI library for authentication, RBAC, subscription plans, and usage t
13
13
  - **Account Lockout**: Brute-force protection
14
14
  - **CLI Tools**: Database migrations and management
15
15
 
16
+ ---
17
+
18
+ ## What IPK Provides vs What You Must Implement
19
+
20
+ ### Provided Out of the Box
21
+
22
+ | Feature | Description |
23
+ |---------|-------------|
24
+ | **Google OAuth Authentication** | Complete OAuth 2.0 flow with CSRF protection |
25
+ | **JWT Token Management** | Access/refresh token generation, validation, and rotation |
26
+ | **Token Theft Detection** | Automatic account deactivation when revoked token is reused |
27
+ | **Default Roles** | Pre-configured `admin` and `user` roles |
28
+ | **Default Plans** | Pre-configured `free` and `pro` plans |
29
+ | **Permission Infrastructure** | `check_permission()`, `require_permission()` methods |
30
+ | **Atomic Quota Checking** | Race-condition safe quota consumption (TOCTOU protected) |
31
+ | **Account Lockout** | Brute-force protection with configurable thresholds |
32
+ | **Token Cleanup** | Automatic expired token removal |
33
+ | **Health Checks** | Kubernetes-ready `/health/live` and `/health/ready` probes |
34
+ | **Graceful Shutdown** | Request draining before shutdown |
35
+ | **Database Migrations** | All tables created via `ipk db upgrade` |
36
+ | **Prometheus Metrics** | Optional observability (requires `[metrics]` extra) |
37
+ | **Audit Logging** | Security events logged automatically |
38
+ | **Idempotent Token Refresh** | Duplicate refresh requests return same tokens (30s window) |
39
+ | **Idempotent Quota Consumption** | When `idempotency_key` provided, prevents double-deduction |
40
+ | **Rate Limiting** | Configurable per-endpoint rate limits |
41
+
42
+ ### You Must Implement
43
+
44
+ | Responsibility | Why | Example |
45
+ |----------------|-----|---------|
46
+ | **Define Features** | IPK doesn't know what features your app has | `"api_calls"`, `"ai_generations"`, `"storage_gb"` |
47
+ | **Assign Features to Plans** | Business decision about what's included | Free: 100 API calls, Pro: unlimited |
48
+ | **Set Plan Limits** | Your pricing/quota decisions | `{"api_calls": 1000, "ai_generations": 50}` |
49
+ | **Define Permission Codes** | App-specific access control | `"admin.users.delete"`, `"reports.export"` |
50
+ | **Assign Permissions to Roles** | Which roles can do what | Admin gets `"admin.*"`, User gets `"feature.use"` |
51
+ | **Payment Webhook Integration** | IPK doesn't handle payments | Call `plan_service.assign_plan()` from Stripe webhook |
52
+ | **Webhook Deduplication** | Payment providers retry webhooks | Check `event_id` before processing (see below) |
53
+ | **Quota Checks in Routes** | IPK doesn't auto-enforce quotas | Call `check_and_consume_quota()` in your endpoints |
54
+ | **Authorization for Plan Ops** | IPK doesn't verify callers by default | Verify webhook signatures, check admin role |
55
+
56
+ ---
57
+
58
+ ## Critical Warnings
59
+
60
+ ### Production Deployments (Multi-Instance)
61
+
62
+ > **Redis is REQUIRED for multi-instance deployments**
63
+
64
+ Without Redis, each instance has its own:
65
+ - OAuth state storage (login fails if callback hits different instance)
66
+ - Rate limit counters (users bypass limits via different instances)
67
+ - Permission cache (inconsistent permissions across instances)
68
+
69
+ ```bash
70
+ # Set Redis URL for production
71
+ IPK_REDIS_URL=redis://localhost:6379/0
72
+ IPK_REQUIRE_REDIS=true # Fail-fast if Redis unavailable
73
+ ```
74
+
75
+ ### Token Theft Protection
76
+
77
+ When a refresh token is used **after being revoked**, IPK assumes token theft:
78
+ 1. ALL tokens for that user are revoked
79
+ 2. User account is **automatically deactivated**
80
+ 3. User must contact support to reactivate
81
+
82
+ This is intentional security behavior, not a bug.
83
+
84
+ ### Webhook Idempotency (NOT Auto-Handled)
85
+
86
+ **Plan management methods do NOT handle idempotency.** If your webhook provider retries requests (Stripe retries on timeout), duplicate calls will create issues.
87
+
88
+ **You MUST implement deduplication:**
89
+
90
+ ```python
91
+ @app.post("/webhooks/stripe")
92
+ async def stripe_webhook(request: Request):
93
+ event = stripe.Webhook.construct_event(...)
94
+
95
+ # CRITICAL: Check if already processed
96
+ if await is_event_processed(event.id):
97
+ return {"status": "already_processed"}
98
+
99
+ if event.type == "checkout.session.completed":
100
+ await kit.plan_service.assign_plan(
101
+ user_id=UUID(event.data.object.client_reference_id),
102
+ plan_code="pro",
103
+ )
104
+
105
+ await mark_event_processed(event.id)
106
+ return {"status": "ok"}
107
+ ```
108
+
109
+ ### Authorization is Your Responsibility
110
+
111
+ Plan management methods (`assign_plan`, `cancel_plan`, `update_plan_limits`, `reset_usage`) are **privileged operations**. IPK does NOT verify the caller is authorized.
112
+
113
+ **You MUST verify authorization before calling:**
114
+
115
+ ```python
116
+ # Option 1: Verify in your code before calling
117
+ @app.post("/admin/users/{user_id}/plan")
118
+ async def admin_assign_plan(user_id: UUID, current_user: User = Depends(CurrentUser(kit))):
119
+ # Verify caller is admin
120
+ await kit.rbac_service.require_permission(
121
+ current_user.id, current_user.role_id, "admin.plans.manage"
122
+ )
123
+ await kit.plan_service.assign_plan(user_id=user_id, plan_code="pro")
124
+
125
+ # Option 2: Configure authorization callback
126
+ async def check_authorization(operation: str, target_user_id: UUID, context: dict | None) -> bool:
127
+ if context and context.get("is_webhook"):
128
+ return True # Verified webhooks are authorized
129
+ if context and context.get("is_admin"):
130
+ return True
131
+ return False
132
+
133
+ kit = IdentityPlanKit(config, authorization_callback=check_authorization)
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Integration Examples
139
+
140
+ ### Quota Tracking in Routes
141
+
142
+ ```python
143
+ @app.post("/generate")
144
+ async def generate(user: User = Depends(CurrentUser(kit))):
145
+ usage = await kit.plan_service.check_and_consume_quota(
146
+ user_id=user.id,
147
+ feature_code="ai_generations",
148
+ amount=1,
149
+ idempotency_key=request.headers.get("X-Idempotency-Key"), # Safe retries
150
+ )
151
+
152
+ if not usage.has_access:
153
+ raise HTTPException(
154
+ status_code=429,
155
+ detail=f"Quota exceeded: {usage.used}/{usage.limit}"
156
+ )
157
+
158
+ # Your feature logic here
159
+ return {"remaining": usage.remaining}
160
+ ```
161
+
162
+ ### Permission Enforcement
163
+
164
+ ```python
165
+ @app.delete("/admin/users/{user_id}")
166
+ async def delete_user(user_id: UUID, current_user: User = Depends(CurrentUser(kit))):
167
+ await kit.rbac_service.require_permission(
168
+ user_id=current_user.id,
169
+ role_id=current_user.role_id,
170
+ permission_code="admin.users.delete",
171
+ )
172
+ # Delete logic here
173
+ ```
174
+
175
+ ### Idempotency for Quota Consumption
176
+
177
+ ```python
178
+ # Generate a unique key per logical operation
179
+ idempotency_key = f"{user.id}:{request_id}:generate_image"
180
+
181
+ # First request: checks quota, consumes 1, caches result
182
+ # Retry request: returns cached result, no double-deduction
183
+ result = await kit.plan_service.check_and_consume_quota(
184
+ user_id=user.id,
185
+ feature_code="ai_generation",
186
+ amount=1,
187
+ idempotency_key=idempotency_key,
188
+ )
189
+ ```
190
+
191
+ ---
192
+
16
193
  ## Installation
17
194
 
18
195
  ```bash
@@ -347,6 +524,26 @@ All configuration is via environment variables or `IdentityPlanKitConfig`:
347
524
  | `IPK_METRICS_PATH` | Path for metrics endpoint | `/metrics` |
348
525
  | `IPK_ENABLE_AUTO_CLEANUP` | Auto cleanup expired tokens | `true` |
349
526
  | `IPK_CLEANUP_INTERVAL_HOURS` | Cleanup interval in hours | `6.0` |
527
+ | `IPK_DATABASE_STATEMENT_TIMEOUT_MS` | PostgreSQL statement timeout | `30000` |
528
+ | `IPK_TOKEN_REFRESH_IDEMPOTENCY_TTL_SECONDS` | Token refresh idempotency window | `30` |
529
+ | `IPK_QUOTA_IDEMPOTENCY_TTL_SECONDS` | Quota consumption idempotency window | `60` |
530
+ | `IPK_REQUIRE_REDIS` | Fail if Redis unavailable | Auto (true in prod) |
531
+
532
+ ### Important Configuration Notes
533
+
534
+ **Database Statement Timeout**: Long-running queries are killed after 30s by default. Adjust for migrations:
535
+
536
+ ```bash
537
+ # For API servers (shorter timeout)
538
+ IPK_DATABASE_STATEMENT_TIMEOUT_MS=10000 # 10 seconds
539
+
540
+ # For migration scripts (longer timeout)
541
+ IPK_DATABASE_STATEMENT_TIMEOUT_MS=300000 # 5 minutes
542
+ ```
543
+
544
+ **Token Refresh Idempotency**: Duplicate refresh requests within 30s return the same tokens. This prevents "token revoked" errors when clients retry due to network issues.
545
+
546
+ **Quota Idempotency**: When `idempotency_key` is provided to `check_and_consume_quota()`, duplicate requests within 60s return cached results without double-deducting.
350
547
 
351
548
  ## Prometheus Metrics (Optional)
352
549
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "identity-plan-kit"
3
- version = "0.2.4"
3
+ version = "0.2.5"
4
4
  description = "Modern FastAPI library for authentication, RBAC, subscription plans, and usage tracking"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -140,7 +140,7 @@ testpaths = ["tests"]
140
140
  addopts = "-v --tb=short"
141
141
 
142
142
  [tool.bumpversion]
143
- current_version = "0.2.4"
143
+ current_version = "0.2.5"
144
144
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
145
145
  serialize = ["{major}.{minor}.{patch}"]
146
146
  tag = true
@@ -294,7 +294,8 @@ def create_auth_router(config: IdentityPlanKitConfig) -> APIRouter: # noqa: PLR
294
294
  summary="Get current user",
295
295
  description="Get information about the authenticated user",
296
296
  )
297
- async def get_me(user: CurrentUser) -> ResponseModel[UserResponse]:
297
+ @rate_limiter.limit(config.rate_limit_profile)
298
+ async def get_me(request: Request, user: CurrentUser) -> ResponseModel[UserResponse]:
298
299
  """Get current authenticated user."""
299
300
  return ResponseModel.ok(
300
301
  data=UserResponse(
@@ -313,6 +314,7 @@ def create_auth_router(config: IdentityPlanKitConfig) -> APIRouter: # noqa: PLR
313
314
  summary="Get user profile",
314
315
  description="Get complete user profile including role, permissions, and current plan",
315
316
  )
317
+ @rate_limiter.limit(config.rate_limit_profile)
316
318
  async def get_profile(request: Request, user: CurrentUser) -> ResponseModel[ProfileResponse]:
317
319
  """Get complete user profile with permissions and plan."""
318
320
  kit = request.app.state.identity_plan_kit
@@ -146,6 +146,9 @@ class RefreshTokenRepository:
146
146
  Uses batch deletion to prevent long-running transactions and
147
147
  table-level locks on large datasets.
148
148
 
149
+ Uses FOR UPDATE SKIP LOCKED to allow concurrent cleanup operations
150
+ to process different batches of tokens without conflicts.
151
+
149
152
  Should be run periodically via background job.
150
153
 
151
154
  Args:
@@ -157,7 +160,8 @@ class RefreshTokenRepository:
157
160
  # Import here - delete is only needed for cleanup operations
158
161
  from sqlalchemy import delete # noqa: PLC0415
159
162
 
160
- # First, get IDs to delete (with limit)
163
+ # Select IDs to delete with SKIP LOCKED to allow concurrent cleanup
164
+ # Each concurrent call will get a different batch of rows
161
165
  select_stmt = (
162
166
  select(RefreshTokenModel.id)
163
167
  .where(
@@ -165,6 +169,7 @@ class RefreshTokenRepository:
165
169
  | (RefreshTokenModel.revoked_at.is_not(None))
166
170
  )
167
171
  .limit(batch_size)
172
+ .with_for_update(skip_locked=True)
168
173
  )
169
174
  result = await self._session.execute(select_stmt)
170
175
  ids_to_delete = [row[0] for row in result.fetchall()]
@@ -172,12 +177,13 @@ class RefreshTokenRepository:
172
177
  if not ids_to_delete:
173
178
  return 0
174
179
 
175
- # Delete by IDs
180
+ # Delete by IDs and get actual count
176
181
  delete_stmt = delete(RefreshTokenModel).where(RefreshTokenModel.id.in_(ids_to_delete))
177
- await self._session.execute(delete_stmt)
182
+ delete_result = await self._session.execute(delete_stmt)
178
183
  await self._session.flush()
179
184
 
180
- count = len(ids_to_delete)
185
+ # Use rowcount from DELETE to get actual deleted count
186
+ count = delete_result.rowcount
181
187
  logger.info(
182
188
  "refresh_tokens_cleaned",
183
189
  count=count,
@@ -22,12 +22,18 @@ class UserRepository:
22
22
  def __init__(self, session: AsyncSession) -> None:
23
23
  self._session = session
24
24
 
25
- async def get_by_id(self, user_id: UUID) -> User | None:
25
+ async def get_by_id(
26
+ self,
27
+ user_id: UUID,
28
+ for_update: bool = False,
29
+ ) -> User | None:
26
30
  """
27
31
  Get user by ID.
28
32
 
29
33
  Args:
30
34
  user_id: User UUID
35
+ for_update: If True, lock the row for update (prevents race conditions
36
+ in operations that depend on current user state like is_active)
31
37
 
32
38
  Returns:
33
39
  User entity or None if not found
@@ -35,6 +41,10 @@ class UserRepository:
35
41
  stmt = (
36
42
  select(UserModel).options(selectinload(UserModel.role)).where(UserModel.id == user_id)
37
43
  )
44
+
45
+ if for_update:
46
+ stmt = stmt.with_for_update()
47
+
38
48
  result = await self._session.execute(stmt)
39
49
  model = result.scalar_one_or_none()
40
50
 
@@ -37,6 +37,8 @@ from identity_plan_kit.shared.security import (
37
37
  create_access_token,
38
38
  create_refresh_token,
39
39
  decode_token,
40
+ decrypt_from_cache,
41
+ encrypt_for_cache,
40
42
  hash_password,
41
43
  hash_token,
42
44
  verify_password,
@@ -347,17 +349,28 @@ class AuthService:
347
349
 
348
350
  # Return the EXACT same tokens that were generated on first request
349
351
  # This ensures true idempotency - client gets identical response
350
- cached_access = cached_data.get("access_token")
351
- cached_refresh = cached_data.get("refresh_token")
352
+ # SECURITY: Tokens are encrypted in cache - decrypt them
353
+ encrypted_access = cached_data.get("access_token_enc")
354
+ encrypted_refresh = cached_data.get("refresh_token_enc")
352
355
 
353
- if cached_access and cached_refresh:
354
- return user, cached_access, cached_refresh
356
+ if encrypted_access and encrypted_refresh:
357
+ cached_access = decrypt_from_cache(encrypted_access, self._secret_key)
358
+ cached_refresh = decrypt_from_cache(encrypted_refresh, self._secret_key)
355
359
 
356
- # Fallback: cache entry malformed, proceed with normal flow
357
- logger.warning(
358
- "token_refresh_idempotency_cache_malformed",
359
- user_id=str(user.id),
360
- )
360
+ if cached_access and cached_refresh:
361
+ return user, cached_access, cached_refresh
362
+
363
+ # Decryption failed - possible key rotation or tampering
364
+ logger.warning(
365
+ "token_refresh_idempotency_decryption_failed",
366
+ user_id=str(user.id),
367
+ )
368
+ else:
369
+ # Fallback: cache entry malformed, proceed with normal flow
370
+ logger.warning(
371
+ "token_refresh_idempotency_cache_malformed",
372
+ user_id=str(user.id),
373
+ )
361
374
 
362
375
  async with self._create_uow() as uow:
363
376
  # Find token by hash with row lock (prevents race condition)
@@ -405,7 +418,8 @@ class AuthService:
405
418
 
406
419
  # P1 SECURITY FIX: Deactivate user to prevent further access
407
420
  # User must contact support to reactivate
408
- user = await uow.users.get_by_id(stored_token.user_id)
421
+ # Use FOR UPDATE to prevent race condition with concurrent requests
422
+ user = await uow.users.get_by_id(stored_token.user_id, for_update=True)
409
423
  if user and user.is_active:
410
424
  await uow.users.deactivate(
411
425
  stored_token.user_id,
@@ -418,8 +432,11 @@ class AuthService:
418
432
 
419
433
  raise TokenInvalidError("Token has been revoked - account secured")
420
434
 
421
- # Get user
422
- user = await uow.users.get_by_id(stored_token.user_id)
435
+ # Get user with FOR UPDATE lock to prevent race condition
436
+ # where user is deactivated between this check and token creation
437
+ # Without this, a deactivated user could get new tokens if the
438
+ # deactivation happens after this read but before the commit
439
+ user = await uow.users.get_by_id(stored_token.user_id, for_update=True)
423
440
  if user is None:
424
441
  raise UserNotFoundError()
425
442
 
@@ -452,14 +469,15 @@ class AuthService:
452
469
 
453
470
  # Cache the actual tokens for idempotency
454
471
  # This ensures retries get the EXACT same tokens (true idempotency)
455
- # Security note: Short TTL limits exposure window, and tokens
456
- # are already transmitted over network. State store should be secured.
472
+ # SECURITY: Encrypt tokens before storing in cache to prevent
473
+ # exposure if cache (Redis) is compromised. Uses Fernet encryption
474
+ # with key derived from secret_key.
457
475
  await state_store.set(
458
476
  idempotency_key,
459
477
  {
460
478
  "user_id": str(user.id),
461
- "access_token": access_token,
462
- "refresh_token": new_refresh_token,
479
+ "access_token_enc": encrypt_for_cache(access_token, self._secret_key),
480
+ "refresh_token_enc": encrypt_for_cache(new_refresh_token, self._secret_key),
463
481
  },
464
482
  ttl_seconds=self._config.token_refresh_idempotency_ttl_seconds,
465
483
  )
@@ -610,8 +628,9 @@ class AuthService:
610
628
  self._validate_password(password)
611
629
 
612
630
  async with self._create_uow() as uow:
613
- # Get user with lock to prevent race conditions
614
- user = await uow.users.get_by_id(user_id)
631
+ # Get user with FOR UPDATE lock to prevent race conditions
632
+ # This ensures user isn't deactivated between this check and password set
633
+ user = await uow.users.get_by_id(user_id, for_update=True)
615
634
 
616
635
  if user is None:
617
636
  raise UserNotFoundError()
@@ -326,6 +326,17 @@ class IdentityPlanKitConfig(BaseSettings):
326
326
  "Allows clients to safely retry refresh requests within this window.",
327
327
  )
328
328
 
329
+ # Quota consumption idempotency
330
+ quota_idempotency_ttl_seconds: int = Field(
331
+ default=60,
332
+ ge=0,
333
+ le=300,
334
+ description="TTL for quota consumption idempotency cache in seconds. "
335
+ "When an idempotency_key is provided to check_and_consume_quota(), "
336
+ "duplicate requests within this window return the cached result "
337
+ "instead of consuming quota again. Set to 0 to disable idempotency.",
338
+ )
339
+
329
340
  # Rate limiting
330
341
  rate_limit_login: str = Field(
331
342
  default="20/minute",
@@ -343,6 +354,14 @@ class IdentityPlanKitConfig(BaseSettings):
343
354
  default="10/minute",
344
355
  description="Rate limit for logout endpoint",
345
356
  )
357
+ rate_limit_profile: str = Field(
358
+ default="60/minute",
359
+ description="Rate limit for profile endpoints (/auth/me, /auth/profile)",
360
+ )
361
+ rate_limit_plans: str = Field(
362
+ default="60/minute",
363
+ description="Rate limit for plan endpoints (/plans)",
364
+ )
346
365
 
347
366
  # Proxy settings
348
367
  trust_proxy_headers: bool = Field(