svc-infra 0.1.593__tar.gz → 0.1.594__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.

Potentially problematic release.


This version of svc-infra might be problematic. Click here for more details.

Files changed (253) hide show
  1. {svc_infra-0.1.593 → svc_infra-0.1.594}/PKG-INFO +1 -1
  2. {svc_infra-0.1.593 → svc_infra-0.1.594}/pyproject.toml +5 -1
  3. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/provider/aiydan.py +28 -2
  4. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/service.py +113 -20
  5. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/apf_payments/router.py +3 -1
  6. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/add.py +10 -0
  7. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/gaurd.py +67 -5
  8. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +76 -36
  9. svc_infra-0.1.594/src/svc_infra/api/fastapi/auth/routers/session_router.py +63 -0
  10. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/settings.py +2 -0
  11. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/sql/users.py +13 -1
  12. svc_infra-0.1.594/src/svc_infra/api/fastapi/dependencies/ratelimit.py +66 -0
  13. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/middleware/ratelimit.py +26 -11
  14. svc_infra-0.1.594/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +30 -0
  15. svc_infra-0.1.594/src/svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
  16. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/setup.py +2 -1
  17. svc_infra-0.1.594/src/svc_infra/obs/metrics/__init__.py +53 -0
  18. svc_infra-0.1.594/src/svc_infra/obs/metrics.py +52 -0
  19. svc_infra-0.1.594/src/svc_infra/security/audit.py +130 -0
  20. svc_infra-0.1.594/src/svc_infra/security/audit_service.py +73 -0
  21. svc_infra-0.1.594/src/svc_infra/security/headers.py +39 -0
  22. svc_infra-0.1.594/src/svc_infra/security/hibp.py +91 -0
  23. svc_infra-0.1.594/src/svc_infra/security/jwt_rotation.py +53 -0
  24. svc_infra-0.1.594/src/svc_infra/security/lockout.py +96 -0
  25. svc_infra-0.1.594/src/svc_infra/security/models.py +245 -0
  26. svc_infra-0.1.594/src/svc_infra/security/org_invites.py +128 -0
  27. svc_infra-0.1.594/src/svc_infra/security/passwords.py +77 -0
  28. svc_infra-0.1.594/src/svc_infra/security/permissions.py +148 -0
  29. svc_infra-0.1.594/src/svc_infra/security/session.py +89 -0
  30. svc_infra-0.1.594/src/svc_infra/security/signed_cookies.py +80 -0
  31. svc_infra-0.1.593/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
  32. {svc_infra-0.1.593 → svc_infra-0.1.594}/README.md +0 -0
  33. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/__init__.py +0 -0
  34. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/README.md +0 -0
  35. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/__init__.py +0 -0
  36. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/alembic.py +0 -0
  37. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/models.py +0 -0
  38. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
  39. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/provider/base.py +0 -0
  40. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/provider/registry.py +0 -0
  41. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
  42. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/schemas.py +0 -0
  43. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/apf_payments/settings.py +0 -0
  44. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/__init__.py +0 -0
  45. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/__init__.py +0 -0
  46. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
  47. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
  48. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
  49. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
  50. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
  51. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
  52. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
  53. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
  54. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
  55. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
  56. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
  57. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
  58. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
  59. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
  60. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
  61. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
  62. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/security.py +0 -0
  63. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
  64. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/auth/state.py +0 -0
  65. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
  66. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/cache/add.py +0 -0
  67. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
  68. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/http.py +0 -0
  69. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
  70. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
  71. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
  72. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
  73. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
  74. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
  75. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
  76. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/sql/add.py +0 -0
  77. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -0
  78. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
  79. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/db/sql/session.py +0 -0
  80. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
  81. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
  82. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
  83. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
  84. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
  85. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
  86. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/dual/public.py +0 -0
  87. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/dual/router.py +0 -0
  88. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
  89. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/dx.py +0 -0
  90. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/ease.py +0 -0
  91. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
  92. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
  93. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
  94. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
  95. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
  96. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
  97. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
  98. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
  99. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
  100. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +0 -0
  101. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
  102. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
  103. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
  104. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
  105. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
  106. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
  107. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/openapi/mutators.py +0 -0
  108. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
  109. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
  110. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
  111. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/pagination.py +0 -0
  112. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
  113. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
  114. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
  115. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
  116. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/paths/user.py +0 -0
  117. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
  118. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
  119. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/app/README.md +0 -0
  120. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/app/__init__.py +0 -0
  121. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/app/env.py +0 -0
  122. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/app/logging/__init__.py +0 -0
  123. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/app/logging/add.py +0 -0
  124. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/app/logging/filter.py +0 -0
  125. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/app/logging/formats.py +0 -0
  126. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/app/root.py +0 -0
  127. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/README.md +0 -0
  128. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/__init__.py +0 -0
  129. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/backend.py +0 -0
  130. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/decorators.py +0 -0
  131. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/demo.py +0 -0
  132. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/keys.py +0 -0
  133. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/recache.py +0 -0
  134. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/resources.py +0 -0
  135. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/tags.py +0 -0
  136. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/ttl.py +0 -0
  137. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cache/utils.py +0 -0
  138. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/__init__.py +0 -0
  139. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/__init__.py +0 -0
  140. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
  141. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
  142. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
  143. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
  144. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
  145. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
  146. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
  147. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +0 -0
  148. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
  149. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/help.py +0 -0
  150. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
  151. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
  152. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/foundation/__init__.py +0 -0
  153. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/foundation/runner.py +0 -0
  154. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
  155. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/__init__.py +0 -0
  156. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/crud_schema.py +0 -0
  157. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/__init__.py +0 -0
  158. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/base.py +0 -0
  159. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/constants.py +0 -0
  160. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/core.py +0 -0
  161. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/indexes.py +0 -0
  162. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/management.py +0 -0
  163. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/mongo/README.md +0 -0
  164. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/mongo/__init__.py +0 -0
  165. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/mongo/client.py +0 -0
  166. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
  167. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/mongo/templates/__init__.py +0 -0
  168. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
  169. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
  170. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
  171. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/repository.py +0 -0
  172. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/resource.py +0 -0
  173. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/scaffold.py +0 -0
  174. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/service.py +0 -0
  175. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
  176. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/types.py +0 -0
  177. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/nosql/utils.py +0 -0
  178. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/README.md +0 -0
  179. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/__init__.py +0 -0
  180. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/apikey.py +0 -0
  181. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/authref.py +0 -0
  182. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/base.py +0 -0
  183. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/constants.py +0 -0
  184. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/core.py +0 -0
  185. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/management.py +0 -0
  186. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/repository.py +0 -0
  187. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/resource.py +0 -0
  188. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/scaffold.py +0 -0
  189. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/service.py +0 -0
  190. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
  191. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/__init__.py +0 -0
  192. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/models_schemas/__init__.py +0 -0
  193. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
  194. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
  195. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
  196. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
  197. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/setup/__init__.py +0 -0
  198. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
  199. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
  200. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
  201. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
  202. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/types.py +0 -0
  203. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/uniq.py +0 -0
  204. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
  205. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/sql/utils.py +0 -0
  206. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/db/utils.py +0 -0
  207. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/mcp/__init__.py +0 -0
  208. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
  209. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/README.md +0 -0
  210. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/__init__.py +0 -0
  211. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/add.py +0 -0
  212. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/cloud_dash.py +0 -0
  213. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/metrics/asgi.py +0 -0
  214. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/metrics/base.py +0 -0
  215. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/metrics/http.py +0 -0
  216. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
  217. {svc_infra-0.1.593/src/svc_infra/obs/metrics → svc_infra-0.1.594/src/svc_infra/obs/providers}/__init__.py +0 -0
  218. {svc_infra-0.1.593/src/svc_infra/obs/providers → svc_infra-0.1.594/src/svc_infra/obs/providers/compose_cloud}/__init__.py +0 -0
  219. {svc_infra-0.1.593/src/svc_infra/obs/providers/compose_cloud → svc_infra-0.1.594/src/svc_infra/obs/providers/compose_cloud/templates}/__init__.py +0 -0
  220. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
  221. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
  222. {svc_infra-0.1.593/src/svc_infra/obs/providers/compose_cloud/templates → svc_infra-0.1.594/src/svc_infra/obs/providers/grafana}/__init__.py +0 -0
  223. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
  224. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
  225. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
  226. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
  227. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
  228. {svc_infra-0.1.593/src/svc_infra/obs/providers/grafana → svc_infra-0.1.594/src/svc_infra/obs/providers/grafana/dashboards}/__init__.py +0 -0
  229. {svc_infra-0.1.593/src/svc_infra/obs/providers/grafana/dashboards → svc_infra-0.1.594/src/svc_infra/obs/providers/grafana/templates}/__init__.py +0 -0
  230. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
  231. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
  232. {svc_infra-0.1.593/src/svc_infra/obs/providers/grafana/templates → svc_infra-0.1.594/src/svc_infra/obs/providers/grafana/templates/provisioning}/__init__.py +0 -0
  233. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
  234. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
  235. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/settings.py +0 -0
  236. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
  237. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
  238. {svc_infra-0.1.593/src/svc_infra/obs/providers/grafana/templates/provisioning → svc_infra-0.1.594/src/svc_infra/obs/templates/sidecars}/__init__.py +0 -0
  239. {svc_infra-0.1.593/src/svc_infra/obs/templates/sidecars → svc_infra-0.1.594/src/svc_infra/obs/templates/sidecars/compose}/__init__.py +0 -0
  240. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
  241. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
  242. {svc_infra-0.1.593/src/svc_infra/obs/templates/sidecars/compose → svc_infra-0.1.594/src/svc_infra/obs/templates/sidecars/fly}/__init__.py +0 -0
  243. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
  244. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
  245. {svc_infra-0.1.593/src/svc_infra/obs/templates/sidecars/fly → svc_infra-0.1.594/src/svc_infra/obs/templates/sidecars/k8s}/__init__.py +0 -0
  246. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
  247. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
  248. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
  249. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
  250. {svc_infra-0.1.593/src/svc_infra/obs/templates/sidecars/k8s → svc_infra-0.1.594/src/svc_infra/obs/templates/sidecars/railway}/__init__.py +0 -0
  251. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
  252. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/py.typed +0 -0
  253. {svc_infra-0.1.593 → svc_infra-0.1.594}/src/svc_infra/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.593
3
+ Version: 0.1.594
4
4
  Summary: Infrastructure for building and deploying prod-ready services
5
5
  License: MIT
6
6
  Keywords: fastapi,sqlalchemy,alembic,auth,infra,async,pydantic
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "svc-infra"
3
- version = "0.1.593"
3
+ version = "0.1.594"
4
4
  description = "Infrastructure for building and deploying prod-ready services"
5
5
  authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
6
6
  license = "MIT"
@@ -119,6 +119,10 @@ testpaths = ["tests"]
119
119
  python_files = ["test_*.py", "*_test.py"]
120
120
  python_classes = ["Test*",]
121
121
  python_functions = ["test_*"]
122
+ markers = [
123
+ "security: Security and auth hardening tests",
124
+ "ratelimit: Rate limiting and abuse protection tests",
125
+ ]
122
126
  filterwarnings = [
123
127
  "ignore:The `route` decorator is deprecated:DeprecationWarning:starlette.*",
124
128
  ]
@@ -5,6 +5,7 @@ from datetime import date, datetime, timezone
5
5
  from typing import Any, Optional, Sequence, Tuple
6
6
 
7
7
  from svc_infra.apf_payments.schemas import (
8
+ BalanceAmount,
8
9
  BalanceSnapshotOut,
9
10
  CustomerOut,
10
11
  CustomerUpsertIn,
@@ -277,13 +278,38 @@ def _usage_record_to_out(data: dict[str, Any]) -> UsageRecordOut:
277
278
  provider_price_id=(
278
279
  str(data.get("provider_price_id")) if data.get("provider_price_id") else None
279
280
  ),
281
+ action=(str(data.get("action")) if data.get("action") else None),
280
282
  )
281
283
 
282
284
 
283
285
  def _balance_snapshot_to_out(data: dict[str, Any]) -> BalanceSnapshotOut:
286
+ def _normalize(side: Any) -> list[dict[str, Any]]:
287
+ if isinstance(side, list):
288
+ out: list[dict[str, Any]] = []
289
+ for item in side:
290
+ if isinstance(item, dict) and "currency" in item and "amount" in item:
291
+ out.append(
292
+ {
293
+ "currency": str(item["currency"]).upper(),
294
+ "amount": int(item["amount"] or 0),
295
+ }
296
+ )
297
+ return out
298
+ if isinstance(side, dict):
299
+ return [
300
+ {"currency": str(cur).upper(), "amount": int(amt or 0)} for cur, amt in side.items()
301
+ ]
302
+ return []
303
+
284
304
  return BalanceSnapshotOut(
285
- available=data.get("available", {}),
286
- pending=data.get("pending", {}),
305
+ available=[
306
+ BalanceAmount(currency=i["currency"], amount=i["amount"])
307
+ for i in _normalize(data.get("available"))
308
+ ],
309
+ pending=[
310
+ BalanceAmount(currency=i["currency"], amount=i["amount"])
311
+ for i in _normalize(data.get("pending"))
312
+ ],
287
313
  )
288
314
 
289
315
 
@@ -65,9 +65,24 @@ def _default_provider_name() -> str:
65
65
 
66
66
 
67
67
  class PaymentsService:
68
+ """Payments service facade wrapping provider adapters and persisting key rows.
68
69
 
69
- def __init__(self, session: AsyncSession, provider_name: Optional[str] = None):
70
+ NOTE: tenant_id is now required for all persistence operations. This is a breaking
71
+ change; callers must supply a valid tenant scope. (Future: could allow multi-tenant
72
+ mapping via adapter registry.)
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ session: AsyncSession,
78
+ *,
79
+ tenant_id: str,
80
+ provider_name: Optional[str] = None,
81
+ ):
82
+ if not tenant_id:
83
+ raise ValueError("tenant_id is required for PaymentsService")
70
84
  self.session = session
85
+ self.tenant_id = tenant_id
71
86
  self._provider_name = (provider_name or _default_provider_name()).lower()
72
87
  self._adapter = None # resolved on first use
73
88
 
@@ -118,6 +133,7 @@ class PaymentsService:
118
133
  # If your PayCustomer model has additional columns (email/name), include them here.
119
134
  self.session.add(
120
135
  PayCustomer(
136
+ tenant_id=self.tenant_id,
121
137
  provider=out.provider,
122
138
  provider_customer_id=out.provider_customer_id,
123
139
  user_id=data.user_id,
@@ -132,6 +148,7 @@ class PaymentsService:
132
148
  out = await adapter.create_intent(data, user_id=user_id)
133
149
  self.session.add(
134
150
  PayIntent(
151
+ tenant_id=self.tenant_id,
135
152
  provider=out.provider,
136
153
  provider_intent_id=out.provider_intent_id,
137
154
  user_id=user_id,
@@ -167,6 +184,32 @@ class PaymentsService:
167
184
  async def refund(self, provider_intent_id: str, data: RefundIn) -> IntentOut:
168
185
  adapter = self._get_adapter()
169
186
  out = await adapter.refund(provider_intent_id, data)
187
+ # Create ledger entry if amount present and not already recorded
188
+ pi = await self.session.scalar(
189
+ select(PayIntent).where(PayIntent.provider_intent_id == provider_intent_id)
190
+ )
191
+ if pi:
192
+ amount = int(data.amount) if data.amount is not None else out.amount
193
+ # Guard against duplicates (same provider_ref + kind)
194
+ existing = await self.session.scalar(
195
+ select(LedgerEntry).where(
196
+ LedgerEntry.provider_ref == provider_intent_id,
197
+ LedgerEntry.kind == "refund",
198
+ )
199
+ )
200
+ if amount > 0 and not existing:
201
+ self.session.add(
202
+ LedgerEntry(
203
+ tenant_id=self.tenant_id,
204
+ provider=pi.provider,
205
+ provider_ref=provider_intent_id,
206
+ user_id=pi.user_id,
207
+ amount=+amount,
208
+ currency=out.currency,
209
+ kind="refund",
210
+ status="posted",
211
+ )
212
+ )
170
213
  return out
171
214
 
172
215
  # --- Webhooks -------------------------------------------------------------
@@ -176,6 +219,7 @@ class PaymentsService:
176
219
  parsed = await adapter.verify_and_parse_webhook(signature, payload)
177
220
  self.session.add(
178
221
  PayEvent(
222
+ tenant_id=self.tenant_id,
179
223
  provider=provider,
180
224
  provider_event_id=parsed["id"],
181
225
  type=parsed.get("type", ""),
@@ -199,6 +243,7 @@ class PaymentsService:
199
243
  intent.status = "succeeded"
200
244
  self.session.add(
201
245
  LedgerEntry(
246
+ tenant_id=self.tenant_id,
202
247
  provider=intent.provider,
203
248
  provider_ref=provider_intent_id,
204
249
  user_id=intent.user_id,
@@ -217,17 +262,27 @@ class PaymentsService:
217
262
  select(PayIntent).where(PayIntent.provider_intent_id == pi_id)
218
263
  )
219
264
  if intent:
220
- self.session.add(
221
- LedgerEntry(
222
- provider=intent.provider,
223
- provider_ref=charge_obj.get("id"),
224
- user_id=intent.user_id,
225
- amount=+amount,
226
- currency=currency,
227
- kind="capture",
228
- status="posted",
265
+ # Avoid duplicate capture entries
266
+ existing = await self.session.scalar(
267
+ select(LedgerEntry).where(
268
+ LedgerEntry.provider_ref == charge_obj.get("id"),
269
+ LedgerEntry.kind == "capture",
229
270
  )
230
271
  )
272
+ if not existing:
273
+ self.session.add(
274
+ LedgerEntry(
275
+ tenant_id=self.tenant_id,
276
+ provider=intent.provider,
277
+ provider_ref=charge_obj.get("id"),
278
+ user_id=intent.user_id,
279
+ amount=+amount,
280
+ currency=currency,
281
+ kind="capture",
282
+ status="posted",
283
+ )
284
+ )
285
+ intent.captured = True
231
286
 
232
287
  async def _post_refund(self, charge_obj: dict):
233
288
  amount = int(charge_obj.get("amount_refunded") or 0)
@@ -237,22 +292,31 @@ class PaymentsService:
237
292
  select(PayIntent).where(PayIntent.provider_intent_id == pi_id)
238
293
  )
239
294
  if intent and amount > 0:
240
- self.session.add(
241
- LedgerEntry(
242
- provider=intent.provider,
243
- provider_ref=charge_obj.get("id"),
244
- user_id=intent.user_id,
245
- amount=+amount,
246
- currency=currency,
247
- kind="refund",
248
- status="posted",
295
+ existing = await self.session.scalar(
296
+ select(LedgerEntry).where(
297
+ LedgerEntry.provider_ref == charge_obj.get("id"),
298
+ LedgerEntry.kind == "refund",
249
299
  )
250
300
  )
301
+ if not existing:
302
+ self.session.add(
303
+ LedgerEntry(
304
+ tenant_id=self.tenant_id,
305
+ provider=intent.provider,
306
+ provider_ref=charge_obj.get("id"),
307
+ user_id=intent.user_id,
308
+ amount=+amount,
309
+ currency=currency,
310
+ kind="refund",
311
+ status="posted",
312
+ )
313
+ )
251
314
 
252
315
  async def attach_payment_method(self, data: PaymentMethodAttachIn) -> PaymentMethodOut:
253
316
  out = await self._get_adapter().attach_payment_method(data)
254
317
  # Upsert locally for quick listing
255
318
  pm = PayPaymentMethod(
319
+ tenant_id=self.tenant_id,
256
320
  provider=out.provider,
257
321
  provider_customer_id=out.provider_customer_id,
258
322
  provider_method_id=out.provider_method_id,
@@ -283,6 +347,7 @@ class PaymentsService:
283
347
  out = await self._get_adapter().create_product(data)
284
348
  self.session.add(
285
349
  PayProduct(
350
+ tenant_id=self.tenant_id,
286
351
  provider=out.provider,
287
352
  provider_product_id=out.provider_product_id,
288
353
  name=out.name,
@@ -295,6 +360,7 @@ class PaymentsService:
295
360
  out = await self._get_adapter().create_price(data)
296
361
  self.session.add(
297
362
  PayPrice(
363
+ tenant_id=self.tenant_id,
298
364
  provider=out.provider,
299
365
  provider_price_id=out.provider_price_id,
300
366
  provider_product_id=out.provider_product_id,
@@ -312,6 +378,7 @@ class PaymentsService:
312
378
  out = await self._get_adapter().create_subscription(data)
313
379
  self.session.add(
314
380
  PaySubscription(
381
+ tenant_id=self.tenant_id,
315
382
  provider=out.provider,
316
383
  provider_subscription_id=out.provider_subscription_id,
317
384
  provider_price_id=out.provider_price_id,
@@ -340,6 +407,7 @@ class PaymentsService:
340
407
  out = await self._get_adapter().create_invoice(data)
341
408
  self.session.add(
342
409
  PayInvoice(
410
+ tenant_id=self.tenant_id,
343
411
  provider=out.provider,
344
412
  provider_invoice_id=out.provider_invoice_id,
345
413
  provider_customer_id=out.provider_customer_id,
@@ -432,6 +500,27 @@ class PaymentsService:
432
500
  pi.status = out.status
433
501
  if out.status in ("succeeded", "requires_capture"): # Stripe specifics vary
434
502
  pi.captured = True if out.status == "succeeded" else pi.captured
503
+ # Add capture ledger entry if succeeded and not already posted
504
+ if out.status == "succeeded":
505
+ existing = await self.session.scalar(
506
+ select(LedgerEntry).where(
507
+ LedgerEntry.provider_ref == provider_intent_id,
508
+ LedgerEntry.kind == "capture",
509
+ )
510
+ )
511
+ if not existing:
512
+ self.session.add(
513
+ LedgerEntry(
514
+ tenant_id=self.tenant_id,
515
+ provider=pi.provider,
516
+ provider_ref=provider_intent_id,
517
+ user_id=pi.user_id,
518
+ amount=+out.amount,
519
+ currency=out.currency,
520
+ kind="capture",
521
+ status="posted",
522
+ )
523
+ )
435
524
  return out
436
525
 
437
526
  async def list_intents(self, f: IntentListFilter) -> tuple[list[IntentOut], str | None]:
@@ -475,6 +564,7 @@ class PaymentsService:
475
564
  out = await self._get_adapter().create_setup_intent(data)
476
565
  self.session.add(
477
566
  PaySetupIntent(
567
+ tenant_id=self.tenant_id,
478
568
  provider=out.provider,
479
569
  provider_setup_intent_id=out.provider_setup_intent_id,
480
570
  user_id=None,
@@ -510,6 +600,7 @@ class PaymentsService:
510
600
  else:
511
601
  self.session.add(
512
602
  PaySetupIntent(
603
+ tenant_id=self.tenant_id,
513
604
  provider=out.provider,
514
605
  provider_setup_intent_id=out.provider_setup_intent_id,
515
606
  user_id=None,
@@ -549,6 +640,7 @@ class PaymentsService:
549
640
  else:
550
641
  self.session.add(
551
642
  PayDispute(
643
+ tenant_id=self.tenant_id,
552
644
  provider=out.provider,
553
645
  provider_dispute_id=out.provider_dispute_id,
554
646
  provider_charge_id=None, # set if adapter returns it
@@ -594,6 +686,7 @@ class PaymentsService:
594
686
  else:
595
687
  self.session.add(
596
688
  PayPayout(
689
+ tenant_id=self.tenant_id,
597
690
  provider=out.provider,
598
691
  provider_payout_id=out.provider_payout_id,
599
692
  amount=out.amount,
@@ -678,10 +771,10 @@ class PaymentsService:
678
771
  if not row:
679
772
  self.session.add(
680
773
  PayCustomer(
774
+ tenant_id=self.tenant_id,
681
775
  provider=out.provider,
682
776
  provider_customer_id=out.provider_customer_id,
683
777
  user_id=None,
684
- tenant_id="",
685
778
  )
686
779
  )
687
780
  return out
@@ -70,7 +70,9 @@ def _tx_kind(kind: str) -> Literal["payment", "refund", "fee", "payout", "captur
70
70
 
71
71
  # --- deps ---
72
72
  async def get_service(session: SqlSessionDep) -> PaymentsService:
73
- return PaymentsService(session=session)
73
+ # TODO: derive tenant_id from auth/session context; placeholder requires explicit value.
74
+ # For now, use a fixed test tenant id; production integration must override.
75
+ return PaymentsService(session=session, tenant_id="test_tenant")
74
76
 
75
77
 
76
78
  # --- routers grouped by auth posture (same prefix is fine; FastAPI merges) ---
@@ -11,6 +11,7 @@ from svc_infra.api.fastapi.auth.mfa.router import mfa_router
11
11
  from svc_infra.api.fastapi.auth.routers.account import account_router
12
12
  from svc_infra.api.fastapi.auth.routers.apikey_router import apikey_router
13
13
  from svc_infra.api.fastapi.auth.routers.oauth_router import oauth_router_with_backend
14
+ from svc_infra.api.fastapi.auth.routers.session_router import build_session_router
14
15
  from svc_infra.api.fastapi.db.sql.users import get_fastapi_users
15
16
  from svc_infra.api.fastapi.paths.prefix import AUTH_PREFIX, USER_PREFIX
16
17
  from svc_infra.app.env import CURRENT_ENVIRONMENT, DEV_ENV, LOCAL_ENV
@@ -73,6 +74,15 @@ def install_user_routers(
73
74
  include_in_schema=include_in_docs,
74
75
  dependencies=[Depends(login_client_gaurd)],
75
76
  )
77
+ # Session/device listing & revocation endpoints (AuthSession model)
78
+ # Mounted under the user prefix so final paths become /{user_prefix}/sessions/... (e.g., /users/sessions/me)
79
+ # The router itself has a /sessions prefix.
80
+ app.include_router(
81
+ build_session_router(),
82
+ prefix=user_prefix,
83
+ tags=["Session Management"],
84
+ include_in_schema=include_in_docs,
85
+ )
76
86
  app.include_router(
77
87
  register_router,
78
88
  prefix=user_prefix,
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
3
4
  from datetime import datetime, timezone
4
5
 
5
6
  from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
@@ -65,6 +66,9 @@ def auth_session_router(
65
66
  router = public_router()
66
67
  policy = auth_policy or DefaultAuthPolicy(get_auth_settings())
67
68
 
69
+ from svc_infra.api.fastapi.db.sql import SqlSessionDep
70
+ from svc_infra.security.lockout import get_lockout_status, record_attempt
71
+
68
72
  @router.post("/login", name="auth:jwt.login")
69
73
  async def login(
70
74
  request: Request,
@@ -74,27 +78,78 @@ def auth_session_router(
74
78
  client_id: str | None = Form(None),
75
79
  client_secret: str | None = Form(None),
76
80
  user_manager=Depends(fapi.get_user_manager),
81
+ session: SqlSessionDep = Depends(),
77
82
  ):
78
- # 1) lookup user (normalize email)
79
83
  strategy = auth_backend.get_strategy()
80
-
81
84
  email = username.strip().lower()
85
+ # Compute IP hash for lockout correlation
86
+ client_ip = getattr(request.client, "host", None)
87
+ ip_hash = hashlib.sha256(client_ip.encode()).hexdigest() if client_ip else None
88
+
89
+ # Pre-check lockout by IP to avoid enumeration
90
+ try:
91
+ status_lo = await get_lockout_status(session, user_id=None, ip_hash=ip_hash)
92
+ if status_lo.locked and status_lo.next_allowed_at:
93
+ retry = int(
94
+ (status_lo.next_allowed_at - datetime.now(timezone.utc)).total_seconds()
95
+ )
96
+ raise HTTPException(
97
+ status_code=429,
98
+ detail="account_locked",
99
+ headers={"Retry-After": str(max(0, retry))},
100
+ )
101
+ except Exception:
102
+ pass
103
+
104
+ # Lookup user
82
105
  user = await user_manager.user_db.get_by_email(email)
83
106
  if not user:
84
107
  _, _ = _pwd.verify_and_update(password, _DUMMY_BCRYPT)
108
+ try:
109
+ await record_attempt(session, user_id=None, ip_hash=ip_hash, success=False)
110
+ except Exception:
111
+ pass
85
112
  raise HTTPException(400, "LOGIN_BAD_CREDENTIALS")
86
113
 
87
- # 2) verify status + password
114
+ # Status checks
88
115
  if not getattr(user, "is_active", True):
89
116
  raise HTTPException(401, "account_disabled")
90
117
 
91
118
  hashed = getattr(user, "hashed_password", None) or getattr(user, "password_hash", None)
92
119
  if not hashed:
93
- # No password set (likely OAuth-only account)
120
+ try:
121
+ await record_attempt(
122
+ session, user_id=getattr(user, "id", None), ip_hash=ip_hash, success=False
123
+ )
124
+ except Exception:
125
+ pass
94
126
  raise HTTPException(400, "LOGIN_BAD_CREDENTIALS")
95
127
 
128
+ # Check lockout for this user + IP before verifying password
129
+ try:
130
+ status_user = await get_lockout_status(
131
+ session, user_id=getattr(user, "id", None), ip_hash=ip_hash
132
+ )
133
+ if status_user.locked and status_user.next_allowed_at:
134
+ retry = int(
135
+ (status_user.next_allowed_at - datetime.now(timezone.utc)).total_seconds()
136
+ )
137
+ raise HTTPException(
138
+ status_code=429,
139
+ detail="account_locked",
140
+ headers={"Retry-After": str(max(0, retry))},
141
+ )
142
+ except Exception:
143
+ pass
144
+
96
145
  ok, new_hash = _pwd.verify_and_update(password, hashed)
97
146
  if not ok:
147
+ try:
148
+ await record_attempt(
149
+ session, user_id=getattr(user, "id", None), ip_hash=ip_hash, success=False
150
+ )
151
+ except Exception:
152
+ pass
98
153
  raise HTTPException(400, "LOGIN_BAD_CREDENTIALS")
99
154
 
100
155
  # If the hash needs upgrading, persist it (optional but recommended)
@@ -106,7 +161,6 @@ def auth_session_router(
106
161
  try:
107
162
  await user_manager.user_db.update(user)
108
163
  except Exception:
109
- # don't block login if updating hash fails; log if you have logging here
110
164
  pass
111
165
 
112
166
  if getattr(user, "is_verified") is False:
@@ -130,6 +184,14 @@ def auth_session_router(
130
184
  # don’t block login if this write fails
131
185
  pass
132
186
 
187
+ # Record successful attempt (for audit)
188
+ try:
189
+ await record_attempt(
190
+ session, user_id=getattr(user, "id", None), ip_hash=ip_hash, success=True
191
+ )
192
+ except Exception:
193
+ pass
194
+
133
195
  # 5) mint token and set cookie
134
196
  token = await strategy.write_token(user)
135
197
  st = get_auth_settings()
@@ -28,6 +28,8 @@ from svc_infra.api.fastapi.paths.auth import (
28
28
  OAUTH_LOGIN_PATH,
29
29
  OAUTH_REFRESH_PATH,
30
30
  )
31
+ from svc_infra.security.models import RefreshToken
32
+ from svc_infra.security.session import issue_session_and_refresh, rotate_session_refresh
31
33
 
32
34
 
33
35
  def _gen_pkce_pair() -> tuple[str, str]:
@@ -466,9 +468,13 @@ async def _validate_and_decode_jwt_token(raw_token: str) -> str:
466
468
 
467
469
 
468
470
  async def _set_cookie_on_response(
469
- resp: Response, auth_backend: AuthenticationBackend, user: Any
471
+ resp: Response,
472
+ auth_backend: AuthenticationBackend,
473
+ user: Any,
474
+ *,
475
+ refresh_raw: str,
470
476
  ) -> None:
471
- """Set authentication cookie on response."""
477
+ """Set authentication (JWT) and refresh cookies on response."""
472
478
  st = get_auth_settings()
473
479
  strategy = auth_backend.get_strategy()
474
480
  jwt_token = await strategy.write_token(user)
@@ -477,6 +483,7 @@ async def _set_cookie_on_response(
477
483
  if same_site_lit == "none" and not bool(st.session_cookie_secure):
478
484
  raise HTTPException(500, "session_cookie_samesite=None requires session_cookie_secure=True")
479
485
 
486
+ # Access/Auth cookie (short-lived JWT)
480
487
  resp.set_cookie(
481
488
  key=_cookie_name(st),
482
489
  value=jwt_token,
@@ -488,6 +495,18 @@ async def _set_cookie_on_response(
488
495
  path="/",
489
496
  )
490
497
 
498
+ # Refresh cookie (opaque token, longer lived)
499
+ resp.set_cookie(
500
+ key=getattr(st, "session_cookie_name", "svc_session"),
501
+ value=refresh_raw,
502
+ max_age=60 * 60 * 24 * 7, # 7 days default
503
+ httponly=True,
504
+ secure=bool(st.session_cookie_secure),
505
+ samesite=same_site_lit,
506
+ domain=_cookie_domain(st),
507
+ path="/",
508
+ )
509
+
491
510
 
492
511
  def _clean_oauth_session_state(request: Request, provider: str) -> None:
493
512
  """Clean up transient OAuth session state."""
@@ -641,9 +660,18 @@ def _create_oauth_router(
641
660
  user.last_login = datetime.now(timezone.utc)
642
661
  await session.commit()
643
662
 
644
- # Create response with auth cookie
663
+ # Create session + initial refresh token
664
+ raw_refresh, _rt = await issue_session_and_refresh(
665
+ session,
666
+ user_id=user.id,
667
+ tenant_id=getattr(user, "tenant_id", None),
668
+ user_agent=str(request.headers.get("user-agent", ""))[:512],
669
+ ip_hash=None,
670
+ )
671
+
672
+ # Create response with auth + refresh cookies
645
673
  resp = RedirectResponse(url=redirect_url, status_code=status.HTTP_302_FOUND)
646
- await _set_cookie_on_response(resp, auth_backend, user)
674
+ await _set_cookie_on_response(resp, auth_backend, user, refresh_raw=raw_refresh)
647
675
 
648
676
  # Clean up session state
649
677
  _clean_oauth_session_state(request, provider)
@@ -667,50 +695,62 @@ def _create_oauth_router(
667
695
  """Refresh authentication token."""
668
696
  st = get_auth_settings()
669
697
 
670
- # Read and validate cookie
671
- name = _cookie_name(st)
672
- raw = request.cookies.get(name)
673
- if not raw:
698
+ # Read and validate auth JWT cookie
699
+ name_auth = _cookie_name(st)
700
+ raw_auth = request.cookies.get(name_auth)
701
+ if not raw_auth:
674
702
  raise HTTPException(401, "missing_token")
675
703
 
676
- # Validate and decode JWT token
677
- user_id = await _validate_and_decode_jwt_token(raw)
704
+ # Validate and decode JWT token to get user id
705
+ user_id = await _validate_and_decode_jwt_token(raw_auth)
678
706
 
679
707
  # Load user
680
708
  user = await session.get(user_model, user_id)
681
709
  if not user:
682
710
  raise HTTPException(401, "invalid_token")
683
711
 
684
- # Handle MFA if required
685
- if await policy.should_require_mfa(user):
686
- pre = await get_mfa_pre_jwt_writer().write(user)
687
- redirect_url = str(getattr(st, "post_login_redirect", "/"))
688
- allow_hosts = parse_redirect_allow_hosts(getattr(st, "redirect_allow_hosts_raw", None))
689
- require_https = bool(getattr(st, "session_cookie_secure", False))
690
- _validate_redirect(redirect_url, allow_hosts, require_https=require_https)
691
-
692
- nxt = request.query_params.get("next")
693
- if nxt:
694
- try:
695
- _validate_redirect(nxt, allow_hosts, require_https=require_https)
696
- redirect_url = nxt
697
- except HTTPException:
698
- pass
699
-
700
- qs = urlencode({"mfa": "required", "pre_token": pre})
701
- return RedirectResponse(url=f"{redirect_url}?{qs}", status_code=status.HTTP_302_FOUND)
702
-
703
- # Create response with new token
704
- resp = Response(status_code=204)
705
- await _set_cookie_on_response(resp, auth_backend, user)
706
-
707
- # Optional: notify policy hook
712
+ # Obtain refresh cookie
713
+ refresh_cookie_name = getattr(st, "session_cookie_name", "svc_session")
714
+ raw_refresh = request.cookies.get(refresh_cookie_name)
715
+ if not raw_refresh:
716
+ raise HTTPException(401, "missing_refresh_token")
717
+
718
+ # Lookup refresh token row by hash
719
+ from sqlalchemy import select
720
+
721
+ from svc_infra.security.models import hash_refresh_token
722
+
723
+ token_hash = hash_refresh_token(raw_refresh)
724
+ found: RefreshToken | None = (
725
+ (
726
+ await session.execute(
727
+ select(RefreshToken).where(RefreshToken.token_hash == token_hash)
728
+ )
729
+ )
730
+ .scalars()
731
+ .first()
732
+ )
733
+ if (
734
+ not found
735
+ or found.revoked_at
736
+ or (found.expires_at and found.expires_at < datetime.now(timezone.utc))
737
+ ):
738
+ raise HTTPException(401, "invalid_refresh_token")
739
+
740
+ # Rotate refresh token
741
+ new_raw, _new_rt = await rotate_session_refresh(session, current=found)
742
+
743
+ # Write response (204) with new cookies
744
+ resp = Response(status_code=status.HTTP_204_NO_CONTENT)
745
+ await _set_cookie_on_response(resp, auth_backend, user, refresh_raw=new_raw)
746
+ return resp
747
+
748
+ # Dead code removed: MFA branch handled earlier in login flow, refresh returns 204 above.
708
749
  if hasattr(policy, "on_token_refresh"):
709
750
  try:
710
751
  await policy.on_token_refresh(user)
711
752
  except Exception:
712
753
  pass
713
754
 
714
- return resp
715
-
755
+ # Return router at end of factory
716
756
  return router