regstack 0.8.1__tar.gz → 0.8.2__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 (184) hide show
  1. {regstack-0.8.1 → regstack-0.8.2}/PKG-INFO +1 -1
  2. {regstack-0.8.1 → regstack-0.8.2}/pyproject.toml +1 -1
  3. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/_results.py +9 -0
  4. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/doctor.py +119 -4
  5. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/config/schema.py +9 -5
  6. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/oauth/providers/google.py +9 -1
  7. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/sms/null.py +6 -5
  8. regstack-0.8.2/src/regstack/version.py +1 -0
  9. regstack-0.8.1/src/regstack/version.py +0 -1
  10. {regstack-0.8.1 → regstack-0.8.2}/.gitignore +0 -0
  11. {regstack-0.8.1 → regstack-0.8.2}/CHANGELOG.md +0 -0
  12. {regstack-0.8.1 → regstack-0.8.2}/LICENSE +0 -0
  13. {regstack-0.8.1 → regstack-0.8.2}/NOTICE +0 -0
  14. {regstack-0.8.1 → regstack-0.8.2}/README.md +0 -0
  15. {regstack-0.8.1 → regstack-0.8.2}/SECURITY.md +0 -0
  16. {regstack-0.8.1 → regstack-0.8.2}/regstack.toml.example +0 -0
  17. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/__init__.py +0 -0
  18. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/app.py +0 -0
  19. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/auth/__init__.py +0 -0
  20. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/auth/clock.py +0 -0
  21. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/auth/dependencies.py +0 -0
  22. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/auth/jwt.py +0 -0
  23. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/auth/lockout.py +0 -0
  24. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/auth/mfa.py +0 -0
  25. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/auth/password.py +0 -0
  26. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/auth/rate_limit.py +0 -0
  27. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/auth/tokens.py +0 -0
  28. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/__init__.py +0 -0
  29. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/base.py +0 -0
  30. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/factory.py +0 -0
  31. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/__init__.py +0 -0
  32. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/backend.py +0 -0
  33. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/client.py +0 -0
  34. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/indexes.py +0 -0
  35. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
  36. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
  37. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
  38. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
  39. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +0 -0
  40. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +0 -0
  41. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
  42. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
  43. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/protocols.py +0 -0
  44. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/__init__.py +0 -0
  45. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/backend.py +0 -0
  46. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/migrations/__init__.py +0 -0
  47. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/migrations/env.py +0 -0
  48. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  49. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  50. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
  51. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/repositories/__init__.py +0 -0
  52. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  53. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  54. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
  55. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +0 -0
  56. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/repositories/oauth_state_repo.py +0 -0
  57. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
  58. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
  59. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/schema.py +0 -0
  60. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/backends/sql/types.py +0 -0
  61. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/__init__.py +0 -0
  62. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/__main__.py +0 -0
  63. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/_paths.py +0 -0
  64. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/_runtime.py +0 -0
  65. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/admin.py +0 -0
  66. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/init.py +0 -0
  67. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/migrate.py +0 -0
  68. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/__init__.py +0 -0
  69. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/capture.py +0 -0
  70. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/cli.py +0 -0
  71. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/http.py +0 -0
  72. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/logtail.py +0 -0
  73. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/phases/__init__.py +0 -0
  74. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/phases/account.py +0 -0
  75. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/phases/cleanup.py +0 -0
  76. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/phases/core_auth.py +0 -0
  77. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/phases/feature_discover.py +0 -0
  78. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/phases/oauth.py +0 -0
  79. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/phases/password_reset.py +0 -0
  80. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/phases/reachability.py +0 -0
  81. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/phases/sms_mfa.py +0 -0
  82. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/report.py +0 -0
  83. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/cli/validate/runner.py +0 -0
  84. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/config/__init__.py +0 -0
  85. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/config/loader.py +0 -0
  86. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/config/secrets.py +0 -0
  87. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/__init__.py +0 -0
  88. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/base.py +0 -0
  89. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/composer.py +0 -0
  90. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/console.py +0 -0
  91. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/factory.py +0 -0
  92. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/ses.py +0 -0
  93. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/smtp.py +0 -0
  94. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/email_change.html +0 -0
  95. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/email_change.subject.txt +0 -0
  96. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/email_change.txt +0 -0
  97. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/password_reset.html +0 -0
  98. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  99. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/password_reset.txt +0 -0
  100. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  101. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  102. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/verification.html +0 -0
  103. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/verification.subject.txt +0 -0
  104. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/email/templates/verification.txt +0 -0
  105. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/hooks/__init__.py +0 -0
  106. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/hooks/events.py +0 -0
  107. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/models/__init__.py +0 -0
  108. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/models/_objectid.py +0 -0
  109. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/models/login_attempt.py +0 -0
  110. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/models/mfa_code.py +0 -0
  111. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/models/oauth_identity.py +0 -0
  112. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/models/oauth_state.py +0 -0
  113. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/models/pending_registration.py +0 -0
  114. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/models/user.py +0 -0
  115. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/oauth/__init__.py +0 -0
  116. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/oauth/base.py +0 -0
  117. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/oauth/errors.py +0 -0
  118. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/oauth/providers/__init__.py +0 -0
  119. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/oauth/registry.py +0 -0
  120. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/__init__.py +0 -0
  121. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/_helpers.py +0 -0
  122. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/_schemas.py +0 -0
  123. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/account.py +0 -0
  124. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/admin.py +0 -0
  125. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/login.py +0 -0
  126. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/logout.py +0 -0
  127. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/oauth.py +0 -0
  128. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/password.py +0 -0
  129. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/phone.py +0 -0
  130. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/register.py +0 -0
  131. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/routers/verify.py +0 -0
  132. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/sms/__init__.py +0 -0
  133. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/sms/base.py +0 -0
  134. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/sms/factory.py +0 -0
  135. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/sms/sns.py +0 -0
  136. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/sms/twilio.py +0 -0
  137. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/__init__.py +0 -0
  138. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/pages.py +0 -0
  139. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/static/css/core.css +0 -0
  140. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/static/css/theme.css +0 -0
  141. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/static/js/regstack.js +0 -0
  142. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/auth/_token_handoff.html +0 -0
  143. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  144. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/auth/forgot.html +0 -0
  145. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/auth/login.html +0 -0
  146. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/auth/me.html +0 -0
  147. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  148. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
  149. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/auth/register.html +0 -0
  150. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/auth/reset.html +0 -0
  151. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/auth/verify.html +0 -0
  152. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/ui/templates/base.html +0 -0
  153. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/__init__.py +0 -0
  154. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/oauth_google/__init__.py +0 -0
  155. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/oauth_google/cli.py +0 -0
  156. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/oauth_google/routes.py +0 -0
  157. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/oauth_google/server.py +0 -0
  158. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
  159. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
  160. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
  161. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/oauth_google/validators.py +0 -0
  162. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/oauth_google/window.py +0 -0
  163. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/oauth_google/writer.py +0 -0
  164. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/__init__.py +0 -0
  165. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/_aws.py +0 -0
  166. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/cli.py +0 -0
  167. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/routes.py +0 -0
  168. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/server.py +0 -0
  169. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/static/wizard.css +0 -0
  170. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/static/wizard.js +0 -0
  171. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/templates/wizard.html +0 -0
  172. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/validators.py +0 -0
  173. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/window.py +0 -0
  174. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/ses/writer.py +0 -0
  175. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/theme_designer/__init__.py +0 -0
  176. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/theme_designer/cli.py +0 -0
  177. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/theme_designer/routes.py +0 -0
  178. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/theme_designer/server.py +0 -0
  179. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/theme_designer/static/designer.css +0 -0
  180. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/theme_designer/static/designer.js +0 -0
  181. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/theme_designer/templates/designer.html +0 -0
  182. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/theme_designer/validators.py +0 -0
  183. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/theme_designer/window.py +0 -0
  184. {regstack-0.8.1 → regstack-0.8.2}/src/regstack/wizard/theme_designer/writer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regstack
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB.
5
5
  Project-URL: Homepage, https://github.com/jdrumgoole/regstack
6
6
  Project-URL: Repository, https://github.com/jdrumgoole/regstack
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regstack"
3
- version = "0.8.1"
3
+ version = "0.8.2"
4
4
  description = "Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -15,6 +15,7 @@ class CheckResult:
15
15
  ok: bool
16
16
  detail: str
17
17
  skipped: bool = False
18
+ warn: bool = False
18
19
 
19
20
  @classmethod
20
21
  def passed(cls, name: str, detail: str) -> CheckResult:
@@ -27,3 +28,11 @@ class CheckResult:
27
28
  @classmethod
28
29
  def skip(cls, name: str, detail: str) -> CheckResult:
29
30
  return cls(name=name, ok=True, detail=detail, skipped=True)
31
+
32
+ @classmethod
33
+ def warned(cls, name: str, detail: str) -> CheckResult:
34
+ """An advisory finding: not a hard failure (``ok=True`` so it
35
+ doesn't fail the command), but surfaced distinctly so operators
36
+ notice it. Used for things outside regstack's control that the
37
+ operator should act on — e.g. an out-of-date database server."""
38
+ return cls(name=name, ok=True, detail=detail, warn=True)
@@ -49,14 +49,23 @@ def doctor(config_path_in: Path | None, check_dns: bool, test_recipient: str | N
49
49
  _run(toml_path=toml_path, check_dns=check_dns, test_recipient=test_recipient)
50
50
  )
51
51
  failed = sum(1 for r in results if not r.ok)
52
+ warned = sum(1 for r in results if r.warn)
52
53
  for r in results:
53
- symbol = click.style("✔", fg="green") if r.ok else click.style("✘", fg="red")
54
+ if r.warn:
55
+ symbol = click.style("⚠", fg="yellow")
56
+ elif r.ok:
57
+ symbol = click.style("✔", fg="green")
58
+ else:
59
+ symbol = click.style("✘", fg="red")
54
60
  click.echo(f"{symbol} {r.name}: {r.detail}")
61
+ if warned:
62
+ click.echo(click.style(f"\n{warned} advisory warning(s).", fg="yellow"), err=True)
55
63
  if failed:
56
- click.echo(click.style(f"\n{failed} check(s) failed.", fg="red"), err=True)
64
+ click.echo(click.style(f"{failed} check(s) failed.", fg="red"), err=True)
57
65
  # Clamp to 0/1 so a shell `regstack doctor && deploy` is predictable;
58
- # the failure count appears on the stderr line above for operators
59
- # who want it. (Review #4.)
66
+ # advisory warnings (e.g. an out-of-date DB server) do NOT fail the
67
+ # command they're surfaced but exit 0. The failure count appears on
68
+ # the stderr line above for operators who want it. (Review #4.)
60
69
  sys.exit(1 if failed else 0)
61
70
 
62
71
 
@@ -79,6 +88,9 @@ async def _run(
79
88
 
80
89
  out.append(await _check_backend(config))
81
90
  out.append(await _check_schema(config))
91
+ mongo_version = await _check_mongo_server_version(config)
92
+ if mongo_version is not None:
93
+ out.append(mongo_version)
82
94
  out.append(_check_email_factory(config))
83
95
 
84
96
  if check_dns:
@@ -156,6 +168,109 @@ async def _check_schema(config: RegStackConfig) -> CheckResult:
156
168
  await backend.aclose()
157
169
 
158
170
 
171
+ # CVE-2025-14847 ("MongoBleed", CVSS 8.7): an unauthenticated network
172
+ # attacker can leak server memory via crafted zlib-compressed messages.
173
+ # The pymongo driver is not the vulnerable component — the server binary
174
+ # is. Patched server releases by LTS track. Keyed (major, minor) →
175
+ # minimum safe (major, minor, patch). See docs/security-reports/2026-05-20.md.
176
+ _MONGO_PATCHED_BASELINE: dict[tuple[int, int], tuple[int, int, int]] = {
177
+ (8, 2): (8, 2, 3),
178
+ (8, 0): (8, 0, 17),
179
+ (7, 0): (7, 0, 28),
180
+ (6, 0): (6, 0, 27),
181
+ (5, 0): (5, 0, 32),
182
+ (4, 4): (4, 4, 30),
183
+ }
184
+
185
+ # The newest LTS track we have a baseline for. A server on a track newer
186
+ # than this (e.g. 8.3+, 9.x) post-dates the advisory and is treated as safe.
187
+ _MONGO_NEWEST_KNOWN_TRACK = max(_MONGO_PATCHED_BASELINE)
188
+ # The oldest LTS track the advisory lists. Anything below it (3.6, 4.0,
189
+ # 4.2, …) is EOL and below every patched release — warn.
190
+ _MONGO_OLDEST_KNOWN_TRACK = min(_MONGO_PATCHED_BASELINE)
191
+
192
+
193
+ def _parse_mongo_version(version: str) -> tuple[int, int, int] | None:
194
+ """Parse a MongoDB version string like ``"7.0.5"`` into ``(7, 0, 5)``.
195
+
196
+ Tolerates a release-candidate / pre-release suffix (``"8.0.0-rc1"``)
197
+ by splitting on the first ``-``. Returns ``None`` if the leading
198
+ three dotted components aren't all integers.
199
+ """
200
+ core = version.split("-", 1)[0]
201
+ parts = core.split(".")
202
+ if len(parts) < 3:
203
+ return None
204
+ try:
205
+ return (int(parts[0]), int(parts[1]), int(parts[2]))
206
+ except ValueError:
207
+ return None
208
+
209
+
210
+ def _assess_mongo_server_version(version: str) -> tuple[bool, str]:
211
+ """Decide whether ``version`` is safe against CVE-2025-14847.
212
+
213
+ Returns ``(ok, detail)``. ``ok`` is False only when the server is on
214
+ a known-affected LTS track *below* its patched baseline — that's the
215
+ case worth a WARNING. Newer-than-advisory and patched servers return
216
+ True; an unrecognised/older track returns True with a "verify
217
+ manually" note rather than crying wolf on every odd build string.
218
+ """
219
+ parsed = _parse_mongo_version(version)
220
+ if parsed is None:
221
+ return True, f"server {version} (could not parse version; verify against CVE-2025-14847)"
222
+ track = (parsed[0], parsed[1])
223
+ baseline = _MONGO_PATCHED_BASELINE.get(track)
224
+ if baseline is not None:
225
+ if parsed < baseline:
226
+ patched = ".".join(str(n) for n in baseline)
227
+ return False, (
228
+ f"server {version} is vulnerable to CVE-2025-14847 (MongoBleed); "
229
+ f"upgrade to ≥ {patched}, or disable zlib compression in mongod.conf"
230
+ )
231
+ return True, f"server {version} (≥ {'.'.join(str(n) for n in baseline)}, patched)"
232
+ if track > _MONGO_NEWEST_KNOWN_TRACK:
233
+ return True, f"server {version} (newer than CVE-2025-14847 advisory tracks)"
234
+ if track < _MONGO_OLDEST_KNOWN_TRACK:
235
+ return False, (
236
+ f"server {version} is end-of-life and below every CVE-2025-14847 "
237
+ "patched release; upgrade to a supported, patched MongoDB version"
238
+ )
239
+ return True, (
240
+ f"server {version} on an unrecognised release track; verify against CVE-2025-14847 manually"
241
+ )
242
+
243
+
244
+ async def _check_mongo_server_version(config: RegStackConfig) -> CheckResult | None:
245
+ """Advisory check: warn if the MongoDB *server* is below the
246
+ CVE-2025-14847 patched baseline. Returns None for non-Mongo backends
247
+ (the check doesn't apply).
248
+ """
249
+ from regstack.backends.base import BackendKind
250
+
251
+ backend = build_backend(config)
252
+ try:
253
+ if backend.kind is not BackendKind.MONGO:
254
+ return None
255
+ from regstack.backends.mongo import MongoBackend
256
+
257
+ assert isinstance(backend, MongoBackend)
258
+ info = await backend.database.command("buildInfo")
259
+ version = str(info.get("version", ""))
260
+ if not version:
261
+ return CheckResult.warned(
262
+ "mongo server", "buildInfo returned no version; verify against CVE-2025-14847"
263
+ )
264
+ ok, detail = _assess_mongo_server_version(version)
265
+ if ok:
266
+ return CheckResult.passed("mongo server", detail)
267
+ return CheckResult.warned("mongo server", detail)
268
+ except Exception as exc:
269
+ return CheckResult.warned("mongo server", f"version check failed: {exc}")
270
+ finally:
271
+ await backend.aclose()
272
+
273
+
159
274
  def _check_email_factory(config: RegStackConfig) -> CheckResult:
160
275
  try:
161
276
  service = build_email_service(config.email)
@@ -98,11 +98,15 @@ class SmsConfig(BaseModel):
98
98
  twilio_account_sid: str | None = None
99
99
  twilio_auth_token: SecretStr | None = None
100
100
 
101
- log_bodies: bool = True
102
- """When True (default), the ``null`` backend logs the SMS body
103
- (including the 6-digit code) at INFO. Set to False to silence
104
- code logging in shared environments. Other backends ignore this
105
- flag they never log message bodies."""
101
+ log_bodies: bool = False
102
+ """When True, the ``null`` backend logs the SMS body (including the
103
+ 6-digit MFA code) at INFO. Defaults to False so a misconfigured
104
+ deployment can't leak codes into shared logs symmetric with
105
+ ``email.log_bodies``. Set it True for local dev when you want to
106
+ read the code out of stdout (the bundled examples instead surface
107
+ it via an ``mfa_login_started`` hook, so they don't need this).
108
+ Other backends ignore this flag — they never log message bodies.
109
+ (Security review 2026-05-19.)"""
106
110
 
107
111
 
108
112
  class OAuthConfig(BaseModel):
@@ -16,6 +16,7 @@ installed and turns ``enable_oauth`` on.
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import asyncio
19
20
  from typing import TYPE_CHECKING, Any
20
21
  from urllib.parse import urlencode
21
22
 
@@ -162,7 +163,14 @@ class GoogleProvider(OAuthProvider):
162
163
  expected_nonce: str,
163
164
  ) -> OAuthUserInfo:
164
165
  try:
165
- signing_key = self._jwks_client.get_signing_key_from_jwt(id_token).key
166
+ # PyJWKClient's fetch is synchronous urllib; on a cache miss it
167
+ # would block the event loop for the round-trip to Google's JWKS
168
+ # endpoint. Push it to a worker thread so concurrent requests
169
+ # aren't stalled. (Security review 2026-05-20 · I-2.)
170
+ signing_key_obj = await asyncio.to_thread(
171
+ self._jwks_client.get_signing_key_from_jwt, id_token
172
+ )
173
+ signing_key = signing_key_obj.key
166
174
  except Exception as exc: # PyJWKClient raises a grab-bag — collapse to ours
167
175
  raise OAuthIdTokenError(f"jwks lookup failed: {exc}") from exc
168
176
  try:
@@ -10,14 +10,15 @@ log = logging.getLogger("regstack.sms.null")
10
10
  class NullSmsService(SmsService):
11
11
  """Default backend. Records messages in ``self.outbox`` so tests and dev
12
12
  runs can inspect them without contacting a real SMS gateway. Logs each
13
- send at INFO so the demo can grep the code out of stdout.
13
+ send at INFO.
14
14
 
15
- ``log_bodies`` (default True) controls whether the message body
16
- (containing the 6-digit code) is included in the log line. Operators
17
- in shared environments who want call-only audit lines can flip it off.
15
+ ``log_bodies`` (default False) controls whether the message body
16
+ (containing the 6-digit code) is included in the log line. It defaults
17
+ off so a misconfigured deployment can't leak codes into shared logs;
18
+ flip it on for local dev when you want to read the code out of stdout.
18
19
  """
19
20
 
20
- def __init__(self, *, log_bodies: bool = True) -> None:
21
+ def __init__(self, *, log_bodies: bool = False) -> None:
21
22
  self.outbox: list[SmsMessage] = []
22
23
  self._log_bodies = log_bodies
23
24
 
@@ -0,0 +1 @@
1
+ __version__ = "0.8.2"
@@ -1 +0,0 @@
1
- __version__ = "0.8.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes