regstack 0.5.0__tar.gz → 0.5.6__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 (239) hide show
  1. {regstack-0.5.0 → regstack-0.5.6}/.github/workflows/publish.yml +6 -1
  2. {regstack-0.5.0 → regstack-0.5.6}/CHANGELOG.md +81 -0
  3. {regstack-0.5.0 → regstack-0.5.6}/PKG-INFO +10 -5
  4. {regstack-0.5.0 → regstack-0.5.6}/docs/architecture.md +7 -3
  5. {regstack-0.5.0 → regstack-0.5.6}/docs/changelog.md +121 -0
  6. {regstack-0.5.0 → regstack-0.5.6}/docs/configuration.md +38 -2
  7. {regstack-0.5.0 → regstack-0.5.6}/docs/quickstart.md +8 -3
  8. {regstack-0.5.0 → regstack-0.5.6}/docs/security.md +50 -3
  9. {regstack-0.5.0 → regstack-0.5.6}/pyproject.toml +19 -5
  10. regstack-0.5.6/scripts/ccr_coverage_setup.py +242 -0
  11. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/app.py +55 -2
  12. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/dependencies.py +12 -4
  13. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/password.py +2 -20
  14. regstack-0.5.6/src/regstack/auth/rate_limit.py +141 -0
  15. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/backend.py +5 -6
  16. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/client.py +9 -3
  17. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/indexes.py +2 -1
  18. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/blacklist_repo.py +14 -2
  19. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +3 -1
  20. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +2 -1
  21. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +3 -1
  22. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +3 -1
  23. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/pending_repo.py +3 -1
  24. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/user_repo.py +3 -1
  25. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/__init__.py +3 -1
  26. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/env.py +5 -1
  27. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -2
  28. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +4 -6
  29. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/oauth_state_repo.py +3 -3
  30. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/pending_repo.py +4 -6
  31. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/user_repo.py +4 -4
  32. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/types.py +4 -2
  33. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/doctor.py +9 -5
  34. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/config/schema.py +18 -1
  35. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/composer.py +9 -2
  36. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/ses.py +1 -1
  37. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/hooks/events.py +7 -0
  38. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/_objectid.py +1 -1
  39. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/user.py +1 -1
  40. regstack-0.5.6/src/regstack/routers/_helpers.py +28 -0
  41. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/account.py +5 -23
  42. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/admin.py +4 -0
  43. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/login.py +12 -5
  44. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/logout.py +3 -1
  45. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/oauth.py +6 -1
  46. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/phone.py +12 -3
  47. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/sns.py +1 -1
  48. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/twilio.py +1 -1
  49. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/pages.py +9 -2
  50. regstack-0.5.6/src/regstack/version.py +1 -0
  51. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/writer.py +1 -3
  52. {regstack-0.5.0 → regstack-0.5.6}/tasks.py +16 -1
  53. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_admin_router.py +39 -0
  54. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_happy_path.py +40 -0
  55. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_mfa.py +110 -0
  56. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_oauth_google_router.py +33 -0
  57. regstack-0.5.6/tests/integration/test_rate_limits.py +215 -0
  58. {regstack-0.5.0 → regstack-0.5.6}/uv.lock +54 -6
  59. regstack-0.5.0/src/regstack/version.py +0 -1
  60. {regstack-0.5.0 → regstack-0.5.6}/.github/workflows/test.yml +0 -0
  61. {regstack-0.5.0 → regstack-0.5.6}/.gitignore +0 -0
  62. {regstack-0.5.0 → regstack-0.5.6}/.python-version +0 -0
  63. {regstack-0.5.0 → regstack-0.5.6}/.readthedocs.yaml +0 -0
  64. {regstack-0.5.0 → regstack-0.5.6}/CLAUDE.md +0 -0
  65. {regstack-0.5.0 → regstack-0.5.6}/LICENSE +0 -0
  66. {regstack-0.5.0 → regstack-0.5.6}/NOTICE +0 -0
  67. {regstack-0.5.0 → regstack-0.5.6}/README.md +0 -0
  68. {regstack-0.5.0 → regstack-0.5.6}/SECURITY.md +0 -0
  69. {regstack-0.5.0 → regstack-0.5.6}/docs/_static/.gitkeep +0 -0
  70. {regstack-0.5.0 → regstack-0.5.6}/docs/_templates/.gitkeep +0 -0
  71. {regstack-0.5.0 → regstack-0.5.6}/docs/api.md +0 -0
  72. {regstack-0.5.0 → regstack-0.5.6}/docs/cli.md +0 -0
  73. {regstack-0.5.0 → regstack-0.5.6}/docs/conf.py +0 -0
  74. {regstack-0.5.0 → regstack-0.5.6}/docs/embedding.md +0 -0
  75. {regstack-0.5.0 → regstack-0.5.6}/docs/index.md +0 -0
  76. {regstack-0.5.0 → regstack-0.5.6}/docs/oauth.md +0 -0
  77. {regstack-0.5.0 → regstack-0.5.6}/docs/security-reports/README.md +0 -0
  78. {regstack-0.5.0 → regstack-0.5.6}/docs/theming.md +0 -0
  79. {regstack-0.5.0 → regstack-0.5.6}/examples/_common/__init__.py +0 -0
  80. {regstack-0.5.0 → regstack-0.5.6}/examples/_common/app.py +0 -0
  81. {regstack-0.5.0 → regstack-0.5.6}/examples/mongo/README.md +0 -0
  82. {regstack-0.5.0 → regstack-0.5.6}/examples/mongo/branding/theme.css +0 -0
  83. {regstack-0.5.0 → regstack-0.5.6}/examples/mongo/main.py +0 -0
  84. {regstack-0.5.0 → regstack-0.5.6}/examples/mongo/regstack.toml +0 -0
  85. {regstack-0.5.0 → regstack-0.5.6}/examples/postgres/README.md +0 -0
  86. {regstack-0.5.0 → regstack-0.5.6}/examples/postgres/main.py +0 -0
  87. {regstack-0.5.0 → regstack-0.5.6}/examples/postgres/regstack.toml +0 -0
  88. {regstack-0.5.0 → regstack-0.5.6}/examples/sqlite/README.md +0 -0
  89. {regstack-0.5.0 → regstack-0.5.6}/examples/sqlite/main.py +0 -0
  90. {regstack-0.5.0 → regstack-0.5.6}/examples/sqlite/regstack.toml +0 -0
  91. {regstack-0.5.0 → regstack-0.5.6}/regstack.toml.example +0 -0
  92. {regstack-0.5.0 → regstack-0.5.6}/scripts/security-review-prompt.md +0 -0
  93. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/__init__.py +0 -0
  94. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/__init__.py +0 -0
  95. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/clock.py +0 -0
  96. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/jwt.py +0 -0
  97. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/lockout.py +0 -0
  98. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/mfa.py +0 -0
  99. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/tokens.py +0 -0
  100. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/__init__.py +0 -0
  101. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/base.py +0 -0
  102. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/factory.py +0 -0
  103. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/__init__.py +0 -0
  104. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
  105. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/protocols.py +0 -0
  106. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/__init__.py +0 -0
  107. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/backend.py +0 -0
  108. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  109. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  110. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
  111. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/__init__.py +0 -0
  112. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  113. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  114. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/schema.py +0 -0
  115. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/__init__.py +0 -0
  116. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/__main__.py +0 -0
  117. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/_runtime.py +0 -0
  118. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/admin.py +0 -0
  119. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/init.py +0 -0
  120. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/migrate.py +0 -0
  121. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/config/__init__.py +0 -0
  122. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/config/loader.py +0 -0
  123. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/config/secrets.py +0 -0
  124. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/__init__.py +0 -0
  125. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/base.py +0 -0
  126. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/console.py +0 -0
  127. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/factory.py +0 -0
  128. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/smtp.py +0 -0
  129. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.html +0 -0
  130. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.subject.txt +0 -0
  131. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.txt +0 -0
  132. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.html +0 -0
  133. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  134. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.txt +0 -0
  135. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  136. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  137. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/verification.html +0 -0
  138. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/verification.subject.txt +0 -0
  139. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/verification.txt +0 -0
  140. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/hooks/__init__.py +0 -0
  141. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/__init__.py +0 -0
  142. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/login_attempt.py +0 -0
  143. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/mfa_code.py +0 -0
  144. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/oauth_identity.py +0 -0
  145. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/oauth_state.py +0 -0
  146. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/pending_registration.py +0 -0
  147. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/__init__.py +0 -0
  148. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/base.py +0 -0
  149. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/errors.py +0 -0
  150. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/providers/__init__.py +0 -0
  151. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/providers/google.py +0 -0
  152. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/registry.py +0 -0
  153. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/__init__.py +0 -0
  154. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/_schemas.py +0 -0
  155. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/password.py +0 -0
  156. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/register.py +0 -0
  157. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/verify.py +0 -0
  158. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/__init__.py +0 -0
  159. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/base.py +0 -0
  160. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/factory.py +0 -0
  161. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/null.py +0 -0
  162. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/__init__.py +0 -0
  163. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/static/css/core.css +0 -0
  164. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/static/css/theme.css +0 -0
  165. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/static/js/regstack.js +0 -0
  166. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  167. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/forgot.html +0 -0
  168. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/login.html +0 -0
  169. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/me.html +0 -0
  170. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  171. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
  172. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/register.html +0 -0
  173. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/reset.html +0 -0
  174. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/verify.html +0 -0
  175. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/base.html +0 -0
  176. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/__init__.py +0 -0
  177. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/__init__.py +0 -0
  178. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/cli.py +0 -0
  179. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/routes.py +0 -0
  180. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/server.py +0 -0
  181. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
  182. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
  183. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
  184. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/validators.py +0 -0
  185. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/window.py +0 -0
  186. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/writer.py +0 -0
  187. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/__init__.py +0 -0
  188. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/cli.py +0 -0
  189. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/routes.py +0 -0
  190. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/server.py +0 -0
  191. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/static/designer.css +0 -0
  192. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/static/designer.js +0 -0
  193. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/templates/designer.html +0 -0
  194. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/validators.py +0 -0
  195. {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/window.py +0 -0
  196. {regstack-0.5.0 → regstack-0.5.6}/tasks/oauth-design.md +0 -0
  197. {regstack-0.5.0 → regstack-0.5.6}/tests/__init__.py +0 -0
  198. {regstack-0.5.0 → regstack-0.5.6}/tests/_fake_google/__init__.py +0 -0
  199. {regstack-0.5.0 → regstack-0.5.6}/tests/_fake_google/provider.py +0 -0
  200. {regstack-0.5.0 → regstack-0.5.6}/tests/conftest.py +0 -0
  201. {regstack-0.5.0 → regstack-0.5.6}/tests/e2e/__init__.py +0 -0
  202. {regstack-0.5.0 → regstack-0.5.6}/tests/e2e/conftest.py +0 -0
  203. {regstack-0.5.0 → regstack-0.5.6}/tests/e2e/test_theme_designer.py +0 -0
  204. {regstack-0.5.0 → regstack-0.5.6}/tests/e2e/test_wizard_oauth_flow.py +0 -0
  205. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/__init__.py +0 -0
  206. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_account_management.py +0 -0
  207. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_indexes.py +0 -0
  208. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_login_lockout.py +0 -0
  209. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_oauth_repos.py +0 -0
  210. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_oauth_ui.py +0 -0
  211. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_password_reset.py +0 -0
  212. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_sql_migrations.py +0 -0
  213. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_ui_router.py +0 -0
  214. {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_verification.py +0 -0
  215. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/__init__.py +0 -0
  216. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_base_install_imports.py +0 -0
  217. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_cli.py +0 -0
  218. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_cli_doctor.py +0 -0
  219. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_cli_init.py +0 -0
  220. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_cli_migrate.py +0 -0
  221. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_config_loader.py +0 -0
  222. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_jwt.py +0 -0
  223. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_lockout.py +0 -0
  224. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_mail_composer.py +0 -0
  225. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_mfa_code_repo.py +0 -0
  226. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_oauth_google.py +0 -0
  227. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_password.py +0 -0
  228. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_ses_backend.py +0 -0
  229. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_sms.py +0 -0
  230. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_smtp_backend.py +0 -0
  231. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_theme_designer_cli.py +0 -0
  232. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_theme_designer_routes.py +0 -0
  233. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_theme_designer_validators.py +0 -0
  234. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_theme_designer_writer.py +0 -0
  235. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_ui_env.py +0 -0
  236. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_cli.py +0 -0
  237. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_routes.py +0 -0
  238. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_validators.py +0 -0
  239. {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_writer.py +0 -0
@@ -61,4 +61,9 @@ jobs:
61
61
  with:
62
62
  name: dist
63
63
  path: dist/
64
- - uses: pypa/gh-action-pypi-publish@release/v1
64
+ # Pinned to a commit SHA, not the mutable `release/v1` branch — the
65
+ # publish job has `id-token: write`, so a tag/branch swap in the
66
+ # action repo would let an attacker push a malicious wheel under our
67
+ # OIDC identity. Update by resolving the latest SHA on `release/v1`
68
+ # and bumping the trailing comment to match.
69
+ - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
@@ -5,6 +5,87 @@ authoritative copy lives at
5
5
  [`docs/changelog.md`](docs/changelog.md) and is rendered into the
6
6
  Sphinx docs.
7
7
 
8
+ ## 0.5.6 — 2026-05-13
9
+
10
+ Eleven days of security-review remediation, supply-chain hardening,
11
+ a full `mypy --strict` cleanup pass, and the per-route rate-limits
12
+ feature rolled up into a single release.
13
+
14
+ **Per-route IP rate limits.** Opt-in via the new `rate_limit` extra
15
+ (or a host-supplied `slowapi.Limiter`) plus any of the new
16
+ `RegStackConfig.*_rate_limit` fields (`login_rate_limit`,
17
+ `register_rate_limit`, `forgot_password_rate_limit`,
18
+ `reset_password_rate_limit`, `verify_rate_limit`,
19
+ `resend_verification_rate_limit`, `change_password_rate_limit`,
20
+ `change_email_rate_limit`, `confirm_email_change_rate_limit`,
21
+ `delete_account_rate_limit`). Each accepts a slowapi-syntax string
22
+ (`"5/minute"`, `"5/minute;20/hour"`). Empty / unset means no limit
23
+ on that route — `LockoutService` still defends `/login` against
24
+ credential stuffing per-account. When `*_rate_limit` strings are
25
+ configured but neither a `rate_limiter=` argument is passed nor
26
+ the `rate_limit` extra is installed, `RegStack.router` raises
27
+ `RuntimeError` on first access — failing closed beats silently
28
+ disabling the protection. Hosts remain responsible for
29
+ `app.state.limiter` and `app.add_exception_handler(RateLimitExceeded, ...)`;
30
+ slowapi owns the 429 response shape. The previously-reserved
31
+ `login_max_per_minute` / `login_max_per_hour` fields are kept for
32
+ back-compat but unwired.
33
+
34
+ **Security fixes.**
35
+
36
+ - JWT 401 detail now returns a static `"Invalid or expired token."`;
37
+ no longer leaks the pyjwt failure reason (signature mismatch /
38
+ expired / malformed / audience mismatch).
39
+ - OAuth sign-in now honours `allow_registration=False`. Previously,
40
+ `/register` respected the flag but the OAuth `_resolve_user`
41
+ "brand-new account" branch did not, creating accounts even when
42
+ self-service signup was disabled.
43
+ - Admin `DELETE /admin/users/{id}` now cascades `oauth_identities`,
44
+ matching the user-initiated `DELETE /account` path. Previously
45
+ left orphan rows that blocked re-registration of the same Google
46
+ subject.
47
+ - `POST /phone/start` and `DELETE /phone` now return 400 (not crash
48
+ with HTTP 500) for OAuth-only users who have no `hashed_password`.
49
+
50
+ **Breaking change — hook contracts.** `mfa_login_started` and
51
+ `phone_setup_started` no longer include the raw OTP code in their
52
+ kwargs. Hooks are best-effort observability and are the documented
53
+ integration surface for analytics / logging / Slack notifications,
54
+ so a plaintext OTP in `**kwargs` is a leak waiting to happen.
55
+ Hosts that subscribed to either event to take over SMS delivery
56
+ should migrate to a custom `SmsService` subclass — the supported
57
+ delivery override.
58
+
59
+ **Dependency floors raised for CVEs.**
60
+
61
+ - `pyjwt>=2.12.1` for CVE-2026-32597 (`crit` header bypass, CVSS 7.5).
62
+ - `cryptography>=46.0.7` added explicitly to the `oauth` extra for
63
+ CVE-2026-26007 (ECC subgroup attack on the JWKS code path, CVSS
64
+ 8.2) plus CVE-2026-34073 and CVE-2026-39892.
65
+ - `python-multipart>=0.0.26` for CVE-2026-40347 (DoS via oversized
66
+ multipart preamble).
67
+
68
+ **Supply chain.** `pypa/gh-action-pypi-publish` in `publish.yml`
69
+ pinned to a commit SHA instead of the mutable `release/v1` branch.
70
+ The publish job holds `id-token: write`, so a tag/branch swap
71
+ upstream would let an attacker push a malicious wheel under our
72
+ OIDC identity.
73
+
74
+ **Removed.** `PasswordHasher.needs_rehash` — called pwdlib's
75
+ non-existent `check_needs_rehash` and would `AttributeError` if
76
+ anyone invoked it. No callers in src or tests. If you were planning
77
+ to use it, call `pwdlib.PasswordHash.verify_and_update` directly.
78
+
79
+ **Internal.** 72 `mypy --strict` errors cleared across 35 files;
80
+ `inv lint` is now green end-to-end. Mongo
81
+ `BlacklistRepo.purge_expired` added (protocol parity with SQL).
82
+ `KNOWN_EVENTS` reconciled — 7 previously-undeclared events added
83
+ (`verification_requested`, `email_change_requested`, `email_changed`,
84
+ `phone_setup_started`, `mfa_login_started`, `mfa_enabled`,
85
+ `mfa_disabled`). `user_logged_out` now actually fires from
86
+ `routers/logout.py` (was listed in `KNOWN_EVENTS` but no router
87
+ emitted it).
88
+
8
89
  ## 0.3.0 — 2026-04-30
9
90
 
10
91
  **OAuth — Sign in with Google.** Opt-in via the new `oauth` extra
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regstack
3
- Version: 0.5.0
3
+ Version: 0.5.6
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
@@ -27,8 +27,8 @@ Requires-Dist: jinja2>=3.1
27
27
  Requires-Dist: pwdlib[argon2]>=0.2.1
28
28
  Requires-Dist: pydantic-settings>=2.2
29
29
  Requires-Dist: pydantic>=2.6
30
- Requires-Dist: pyjwt>=2.8
31
- Requires-Dist: python-multipart>=0.0.9
30
+ Requires-Dist: pyjwt>=2.12.1
31
+ Requires-Dist: python-multipart>=0.0.26
32
32
  Requires-Dist: pywebview>=5.0
33
33
  Requires-Dist: sqlalchemy[asyncio]>=2.0
34
34
  Requires-Dist: tomlkit>=0.13
@@ -36,10 +36,11 @@ Requires-Dist: uvicorn[standard]>=0.29
36
36
  Provides-Extra: dev
37
37
  Requires-Dist: anyio>=4.3; extra == 'dev'
38
38
  Requires-Dist: asyncpg>=0.29; extra == 'dev'
39
+ Requires-Dist: cryptography>=46.0.7; extra == 'dev'
39
40
  Requires-Dist: httpx>=0.27; extra == 'dev'
40
41
  Requires-Dist: invoke>=2.2; extra == 'dev'
41
42
  Requires-Dist: mypy>=1.10; extra == 'dev'
42
- Requires-Dist: pyjwt[crypto]>=2.8; extra == 'dev'
43
+ Requires-Dist: pyjwt[crypto]>=2.12.1; extra == 'dev'
43
44
  Requires-Dist: pymongo>=4.9; extra == 'dev'
44
45
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
45
46
  Requires-Dist: pytest-cov>=5.0; extra == 'dev'
@@ -47,6 +48,7 @@ Requires-Dist: pytest-playwright>=0.5; extra == 'dev'
47
48
  Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
48
49
  Requires-Dist: pytest>=8.0; extra == 'dev'
49
50
  Requires-Dist: ruff>=0.4; extra == 'dev'
51
+ Requires-Dist: slowapi>=0.1.9; extra == 'dev'
50
52
  Requires-Dist: uvicorn[standard]>=0.29; extra == 'dev'
51
53
  Provides-Extra: docs
52
54
  Requires-Dist: furo>=2024.1; extra == 'docs'
@@ -58,9 +60,12 @@ Requires-Dist: sphinx>=7.3; extra == 'docs'
58
60
  Provides-Extra: mongo
59
61
  Requires-Dist: pymongo>=4.9; extra == 'mongo'
60
62
  Provides-Extra: oauth
61
- Requires-Dist: pyjwt[crypto]>=2.8; extra == 'oauth'
63
+ Requires-Dist: cryptography>=46.0.7; extra == 'oauth'
64
+ Requires-Dist: pyjwt[crypto]>=2.12.1; extra == 'oauth'
62
65
  Provides-Extra: postgres
63
66
  Requires-Dist: asyncpg>=0.29; extra == 'postgres'
67
+ Provides-Extra: rate-limit
68
+ Requires-Dist: slowapi>=0.1.9; extra == 'rate-limit'
64
69
  Provides-Extra: ses
65
70
  Requires-Dist: aioboto3>=12.3; extra == 'ses'
66
71
  Provides-Extra: sns
@@ -54,9 +54,13 @@ multi-tenant deployments where a single FastAPI app serves multiple
54
54
  The backend is auto-built from `config.database_url` if not supplied
55
55
  explicitly. URL scheme decides:
56
56
 
57
- - `sqlite+aiosqlite://` → SQLAlchemy backend in SQLite mode.
58
- - `postgresql+asyncpg://` SQLAlchemy backend in Postgres mode.
59
- - `mongodb://` / `mongodb+srv://` Mongo backend.
57
+ - `sqlite+aiosqlite:///PATH` → SQLAlchemy backend in SQLite mode.
58
+ `PATH` is `./dbname.db` for a relative file, `/var/lib/app/dbname.db`
59
+ for an absolute file, or `:memory:` for an ephemeral in-process DB.
60
+ - `postgresql+asyncpg://<username>:<password>@dbhost.example.com:5432/dbname`
61
+ → SQLAlchemy backend in Postgres mode.
62
+ - `mongodb://<username>:<password>@dbhost.example.com:27017/dbname`
63
+ (or `mongodb+srv://…`) → Mongo backend.
60
64
 
61
65
  The façade exposes:
62
66
 
@@ -3,6 +3,127 @@
3
3
  All notable changes to this project are documented here. Versions follow
4
4
  [Semantic Versioning](https://semver.org/) once `1.0.0` ships.
5
5
 
6
+ ## 0.5.6 — 2026-05-13
7
+
8
+ A rollup release that consolidates 11 days of security-review
9
+ remediation, supply-chain hardening, a full `mypy --strict` pass,
10
+ and the per-route rate-limits feature.
11
+
12
+ ### Added
13
+
14
+ - **Per-route IP rate limits.** Opt-in via the new `rate_limit` extra
15
+ (or a host-supplied `slowapi.Limiter`) plus any of the new
16
+ `RegStackConfig.*_rate_limit` fields (`login_rate_limit`,
17
+ `register_rate_limit`, `forgot_password_rate_limit`,
18
+ `reset_password_rate_limit`, `verify_rate_limit`,
19
+ `resend_verification_rate_limit`, `change_password_rate_limit`,
20
+ `change_email_rate_limit`, `confirm_email_change_rate_limit`,
21
+ `delete_account_rate_limit`). Each accepts a slowapi-syntax string
22
+ (`"5/minute"`, `"5/minute;20/hour"`).
23
+ - New constructor argument `RegStack(rate_limiter=...)`. When at
24
+ least one `*_rate_limit` field is set, regstack expects either
25
+ this argument or the `rate_limit` extra; failure to provide one
26
+ raises `RuntimeError` on first access to `regstack.router` —
27
+ failing closed beats silently disabling the protection. Hosts
28
+ remain responsible for `app.state.limiter` and the
29
+ `RateLimitExceeded` exception handler; slowapi owns the 429
30
+ response shape.
31
+ - **`user_logged_out` hook now fires.** The event was listed in
32
+ `KNOWN_EVENTS` since M1 but no router ever emitted it.
33
+ `routers/logout.py` now fires `user_logged_out` (with a `user=`
34
+ kwarg) immediately after the bearer token is revoked.
35
+
36
+ ### Changed (security)
37
+
38
+ - **JWT 401 responses no longer leak the pyjwt error reason.**
39
+ Replaced `f"Invalid token: {exc}"` with the static `"Invalid or
40
+ expired token."`. The pyjwt error text disclosed *why* a token was
41
+ rejected (signature mismatch, expired, malformed, audience
42
+ mismatch) — useful signal for an attacker probing the auth
43
+ surface.
44
+ - **OAuth sign-in honours `allow_registration=False`.** `/register`
45
+ already did; the OAuth `_resolve_user` "brand-new account" branch
46
+ did not, so an operator who disabled self-service signup still got
47
+ accounts created via "Sign in with Google". The OAuth callback now
48
+ redirects with `?error=registration_disabled` if no existing
49
+ account matches and registration is disabled.
50
+ - **Admin `DELETE /admin/users/{id}` now cascades
51
+ `oauth_identities`.** Matches the user-initiated `DELETE
52
+ /account` flow; previously left orphan rows that blocked the
53
+ Google subject from re-registering.
54
+ - **`POST /phone/start` and `DELETE /phone` guard against OAuth-only
55
+ users.** Both endpoints previously crashed with HTTP 500 for
56
+ users with `hashed_password=None`. Both now return 400 with a
57
+ message pointing to forgot-password (which doubles as a "set
58
+ initial password" path).
59
+
60
+ ### Changed (BREAKING — hook contracts)
61
+
62
+ - **`mfa_login_started` and `phone_setup_started` no longer include
63
+ the raw OTP code in their kwargs.** Hooks are best-effort
64
+ observability and are the documented integration surface for
65
+ analytics / logging / Slack notifications, so a plaintext OTP in
66
+ `**kwargs` is a leak waiting to happen — a host adding
67
+ `logger.info(kw)` to a hook handler is enough to put OTPs in a
68
+ log stream. Hosts that subscribed to either event to take over
69
+ SMS delivery should migrate to a custom `SmsService` subclass
70
+ (the supported delivery override). The other kwargs (`user`,
71
+ `phone`) remain.
72
+
73
+ ### Changed (deps)
74
+
75
+ - `pyjwt>=2.12.1` (was `>=2.8`). Picks up CVE-2026-32597 (`crit`
76
+ header bypass, CVSS 7.5).
77
+ - `cryptography>=46.0.7` added explicitly to the `oauth` extra
78
+ (was pulled transitively, unbounded). Picks up CVE-2026-26007
79
+ (ECC subgroup attack on the JWKS code path, CVSS 8.2) plus
80
+ CVE-2026-34073 and CVE-2026-39892.
81
+ - `python-multipart>=0.0.26` (was `>=0.0.9`). Picks up
82
+ CVE-2026-40347 (DoS via oversized multipart preamble).
83
+ - `pypa/gh-action-pypi-publish` in `publish.yml` pinned to a commit
84
+ SHA instead of the mutable `release/v1` branch. The publish job
85
+ holds `id-token: write`, so a tag/branch swap upstream would let
86
+ an attacker push a malicious wheel under our OIDC identity.
87
+
88
+ ### Removed
89
+
90
+ - `PasswordHasher.needs_rehash` — called pwdlib's non-existent
91
+ `check_needs_rehash` and would `AttributeError` if anyone invoked
92
+ it. No callers in src or tests. If you were planning to use it,
93
+ call `pwdlib.PasswordHash.verify_and_update` directly.
94
+
95
+ ### Internal
96
+
97
+ - 72 `mypy --strict` errors cleared across 35 files. `inv lint` is
98
+ green end-to-end (ruff + mypy). Still local-only — not yet a CI
99
+ gate.
100
+ - Mongo `BlacklistRepo.purge_expired` added (was missing from the
101
+ Mongo impl; SQL impl already had it). Mongo's TTL index still
102
+ reaps automatically; the explicit `delete_many` is for protocol
103
+ parity and for tests that can't wait for the 60-second TTL
104
+ monitor.
105
+ - `KNOWN_EVENTS` reconciled with reality: 7 previously-undeclared
106
+ events added (`verification_requested`, `email_change_requested`,
107
+ `email_changed`, `phone_setup_started`, `mfa_login_started`,
108
+ `mfa_enabled`, `mfa_disabled`).
109
+ - `routers/_helpers.require_password_set` factored out of
110
+ `routers/account.py` and reused in `routers/phone.py`.
111
+ - `AsyncDatabase[MongoDoc]` / `AsyncMongoClient[MongoDoc]`
112
+ parameterized across the Mongo backend so pymongo's typed stubs
113
+ are satisfied.
114
+
115
+ ### Notes
116
+
117
+ - `LockoutService` (per-account, sliding-window failure counter) is
118
+ unchanged and continues to defend `/login` against
119
+ credential-stuffing against a single account. Per-route IP limits
120
+ are orthogonal: they defend each endpoint against a single source
121
+ IP spamming requests across many accounts.
122
+ - The previously-reserved `login_max_per_minute` /
123
+ `login_max_per_hour` config fields are kept for back-compat but
124
+ no longer have any effect. Switch to the per-route fields when
125
+ you next touch your config.
126
+
6
127
  ## 0.5.0 — 2026-05-02
7
128
 
8
129
  ### Added
@@ -85,9 +85,10 @@ regstack picks a backend at construction time from the URL scheme of
85
85
  - Notes
86
86
 
87
87
  * - SQLite
88
- - `sqlite+aiosqlite:///./dbname.db`
88
+ - Relative file: `sqlite+aiosqlite:///./dbname.db`
89
89
  - Default. Bundled in the base install — no extras needed.
90
- `:memory:` works too (per-test).
90
+ See [SQLite URL forms](#sqlite-url-forms) below for absolute-path
91
+ and in-memory variants.
91
92
  * - Postgres
92
93
  - `postgresql+asyncpg://<username>:<password>@dbhost.example.com:5432/dbname`
93
94
  - Requires the `postgres` extra (pulls in `asyncpg`). The driver is
@@ -103,6 +104,41 @@ The active backend exposes the same five repository protocols on
103
104
  ``RegStack.users``, ``.pending``, ``.blacklist``, ``.attempts``,
104
105
  ``.mfa_codes``. Routers / hooks never branch on backend kind.
105
106
 
107
+ ### SQLite URL forms
108
+
109
+ SQLite is the default backend and the only one whose URL points at a
110
+ file rather than a network host. The shape is always:
111
+
112
+ ```
113
+ sqlite+aiosqlite:///PATH
114
+ ```
115
+
116
+ …where the prefix is fixed and `PATH` is whatever you want SQLAlchemy
117
+ to open. Three useful values for `PATH`:
118
+
119
+ | `PATH` | Resolves to | When to use it |
120
+ |---|---|---|
121
+ | `./dbname.db` | `dbname.db` in the process working directory | Local dev and the bundled `examples/` apps. |
122
+ | `/var/lib/app/dbname.db` | the absolute file at that path | Production. Point it at the host's persistent volume. |
123
+ | `:memory:` | per-process in-memory DB | Per-test fixtures only — contents vanish at process exit. |
124
+
125
+ So the three full URLs are:
126
+
127
+ ```bash
128
+ sqlite+aiosqlite:///./dbname.db
129
+ sqlite+aiosqlite:////var/lib/app/dbname.db
130
+ sqlite+aiosqlite:///:memory:
131
+ ```
132
+
133
+ The absolute form looks like it has four slashes, but it's the same
134
+ three-slash prefix as the others — the fourth slash is the leading
135
+ `/` of the absolute path. This is the single most common SQLite-URL
136
+ paper-cut.
137
+
138
+ Both file forms create the file on first connection, so a fresh
139
+ checkout running `uv run regstack init && uv run uvicorn …` works
140
+ with no `mkdir` or `touch` step.
141
+
106
142
  ## JWT
107
143
 
108
144
  ```{list-table}
@@ -91,9 +91,14 @@ app.include_router(regstack.router, prefix=config.api_prefix)
91
91
  `RegStack` picks the right backend automatically from the URL scheme of
92
92
  `config.database_url`:
93
93
 
94
- - `sqlite+aiosqlite://` → SQLite via SQLAlchemy
95
- - `postgresql+asyncpg://` Postgres via SQLAlchemy
96
- - `mongodb://` (or `mongodb+srv://`) MongoDB
94
+ - `sqlite+aiosqlite:///PATH` → SQLite via SQLAlchemy. `PATH` is the
95
+ filename (e.g. `./dbname.db`) or the literal `:memory:`. See
96
+ [SQLite URL forms](configuration.md#sqlite-url-forms) for the
97
+ absolute-path variant.
98
+ - `postgresql+asyncpg://<username>:<password>@dbhost.example.com:5432/dbname`
99
+ → Postgres via SQLAlchemy
100
+ - `mongodb://<username>:<password>@dbhost.example.com:27017/dbname`
101
+ (or `mongodb+srv://…`) → MongoDB
97
102
 
98
103
  `install_schema()` is idempotent. On SQL backends it runs Alembic
99
104
  migrations to head; on MongoDB it ensures the indexes exist. Calling
@@ -104,6 +104,51 @@ visible to logged-out users only.
104
104
  failures.
105
105
  - Disabled in tests via `rate_limit_disabled=True`.
106
106
 
107
+ ## Per-route IP rate limits
108
+
109
+ Lockout defends each *account* against credential-stuffing. It does
110
+ nothing for an IP that hammers `/forgot-password`, `/register`, or
111
+ `/verify` against many accounts. For that, regstack supports
112
+ slowapi-backed per-route IP rate limits:
113
+
114
+ - Opt in by installing the `rate_limit` extra (`pip install
115
+ regstack[rate_limit]`) **or** by passing a host-built
116
+ `slowapi.Limiter` to `RegStack(rate_limiter=...)`. Hosts already
117
+ using slowapi should pass their own Limiter so it shares state
118
+ with the rest of the app.
119
+ - Set any of the `*_rate_limit` config fields to a slowapi-syntax
120
+ string. Each empty / unset field means "no limit on this route":
121
+
122
+ ```toml
123
+ login_rate_limit = "30/minute;200/hour"
124
+ register_rate_limit = "10/minute;50/hour"
125
+ forgot_password_rate_limit = "5/minute;20/hour"
126
+ reset_password_rate_limit = "5/minute;20/hour"
127
+ verify_rate_limit = "10/minute;60/hour"
128
+ resend_verification_rate_limit = "5/minute;30/hour"
129
+ change_password_rate_limit = "5/minute;20/hour"
130
+ ```
131
+
132
+ - Hosts still own slowapi's app-level wiring:
133
+
134
+ ```python
135
+ from slowapi import Limiter, _rate_limit_exceeded_handler
136
+ from slowapi.errors import RateLimitExceeded
137
+ from slowapi.util import get_remote_address
138
+
139
+ limiter = Limiter(key_func=get_remote_address)
140
+ app.state.limiter = limiter
141
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
142
+
143
+ rs = RegStack(config=cfg, rate_limiter=limiter)
144
+ app.include_router(rs.router, prefix="/api/auth")
145
+ ```
146
+
147
+ - Failing closed: if `*_rate_limit` is set but neither a Limiter
148
+ nor the extra is available, `regstack.router` raises
149
+ `RuntimeError` on first access. We never silently disable a
150
+ configured protection.
151
+
107
152
  ## Email verification (durable, hashed token)
108
153
 
109
154
  - Random 32-byte URL-safe token, SHA-256 hashed in
@@ -267,9 +312,11 @@ avoids that:
267
312
  - **Content Security Policy headers.** regstack's SSR layer is
268
313
  CSP-friendly but the host emits the `Content-Security-Policy`
269
314
  response header.
270
- - **Rate-limiting beyond the per-account login lockout.** A future
271
- milestone may add a `slowapi`-style middleware; for now host-level
272
- rate limits are the right place to push back broad attack traffic.
315
+ - **Rate-limiting beyond the per-account login lockout.** Per-route
316
+ IP limits ship as the optional `rate_limit` extra (see *Per-route
317
+ IP rate limits* above). Host-level rate limiting (nginx, Cloudfront,
318
+ …) is still the right place to push back broad attack traffic that
319
+ isn't worth letting hit Python at all.
273
320
  - **Backups, MongoDB user permissions, network-level isolation** between
274
321
  the app and the database.
275
322
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regstack"
3
- version = "0.5.0"
3
+ version = "0.5.6"
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"
@@ -21,11 +21,14 @@ dependencies = [
21
21
  "pydantic>=2.6",
22
22
  "pydantic-settings>=2.2",
23
23
  "pwdlib[argon2]>=0.2.1",
24
- "pyjwt>=2.8",
24
+ # pyjwt>=2.12.1 includes the fix for CVE-2026-32597 (`crit` header bypass).
25
+ "pyjwt>=2.12.1",
25
26
  "jinja2>=3.1",
26
27
  "click>=8.1",
27
28
  "dnspython>=2.6",
28
- "python-multipart>=0.0.9",
29
+ # python-multipart>=0.0.26 picks up CVE-2026-40347 (DoS via oversized
30
+ # multipart preamble).
31
+ "python-multipart>=0.0.26",
29
32
  "email-validator>=2.1",
30
33
  "aiosmtplib>=3.0",
31
34
  # SQL stack — bundled by default because SQLite is the default backend.
@@ -45,7 +48,15 @@ mongo = ["pymongo>=4.9"]
45
48
  ses = ["aioboto3>=12.3"]
46
49
  sns = ["aioboto3>=12.3"]
47
50
  twilio = ["twilio>=9.0"]
48
- oauth = ["pyjwt[crypto]>=2.8"]
51
+ # cryptography>=46.0.7 picks up CVE-2026-26007 (ECC subgroup attack on the
52
+ # JWKS code path) plus CVE-2026-34073 and CVE-2026-39892.
53
+ oauth = ["pyjwt[crypto]>=2.12.1", "cryptography>=46.0.7"]
54
+ # Per-route rate limiting on the auth router. The limiter itself can be
55
+ # host-supplied (so hosts that already use slowapi share the same Limiter
56
+ # state), otherwise regstack constructs an in-memory one.
57
+ rate_limit = [
58
+ "slowapi>=0.1.9",
59
+ ]
49
60
  docs = [
50
61
  "sphinx>=7.3",
51
62
  "myst-parser>=3.0",
@@ -69,9 +80,12 @@ dev = [
69
80
  "pymongo>=4.9",
70
81
  "asyncpg>=0.29",
71
82
  # OAuth provider tests need the crypto bits to verify ID tokens.
72
- "pyjwt[crypto]>=2.8",
83
+ "pyjwt[crypto]>=2.12.1",
84
+ "cryptography>=46.0.7",
73
85
  # E2E tests for the OAuth setup wizard (drives the SPA in headless Chromium).
74
86
  "pytest-playwright>=0.5",
87
+ # Rate-limit tests exercise the slowapi integration.
88
+ "slowapi>=0.1.9",
75
89
  ]
76
90
 
77
91
  [project.scripts]