regstack 0.5.6__tar.gz → 0.6.0__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 (241) hide show
  1. {regstack-0.5.6 → regstack-0.6.0}/.github/workflows/publish.yml +18 -5
  2. {regstack-0.5.6 → regstack-0.6.0}/.github/workflows/test.yml +21 -7
  3. {regstack-0.5.6 → regstack-0.6.0}/.gitignore +12 -0
  4. regstack-0.6.0/CHANGELOG.md +463 -0
  5. {regstack-0.5.6 → regstack-0.6.0}/PKG-INFO +9 -6
  6. {regstack-0.5.6 → regstack-0.6.0}/docs/changelog.md +208 -0
  7. {regstack-0.5.6 → regstack-0.6.0}/docs/configuration.md +64 -2
  8. {regstack-0.5.6 → regstack-0.6.0}/docs/security.md +7 -4
  9. {regstack-0.5.6 → regstack-0.6.0}/pyproject.toml +22 -8
  10. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/blacklist_repo.py +4 -1
  11. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +11 -2
  12. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -15
  13. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/protocols.py +14 -1
  14. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/repositories/oauth_state_repo.py +11 -2
  15. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/cli/__main__.py +37 -7
  16. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/models/user.py +13 -0
  17. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/account.py +20 -7
  18. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/admin.py +15 -0
  19. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/login.py +17 -5
  20. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/oauth.py +199 -18
  21. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/verify.py +6 -2
  22. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/static/js/regstack.js +9 -0
  23. regstack-0.6.0/src/regstack/version.py +1 -0
  24. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_account_management.py +32 -12
  25. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_admin_router.py +31 -0
  26. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_oauth_google_router.py +119 -0
  27. regstack-0.6.0/tests/unit/test_cli_wizard_missing_extra.py +59 -0
  28. {regstack-0.5.6 → regstack-0.6.0}/uv.lock +16 -10
  29. regstack-0.5.6/CHANGELOG.md +0 -226
  30. regstack-0.5.6/src/regstack/version.py +0 -1
  31. {regstack-0.5.6 → regstack-0.6.0}/.python-version +0 -0
  32. {regstack-0.5.6 → regstack-0.6.0}/.readthedocs.yaml +0 -0
  33. {regstack-0.5.6 → regstack-0.6.0}/CLAUDE.md +0 -0
  34. {regstack-0.5.6 → regstack-0.6.0}/LICENSE +0 -0
  35. {regstack-0.5.6 → regstack-0.6.0}/NOTICE +0 -0
  36. {regstack-0.5.6 → regstack-0.6.0}/README.md +0 -0
  37. {regstack-0.5.6 → regstack-0.6.0}/SECURITY.md +0 -0
  38. {regstack-0.5.6 → regstack-0.6.0}/docs/_static/.gitkeep +0 -0
  39. {regstack-0.5.6 → regstack-0.6.0}/docs/_templates/.gitkeep +0 -0
  40. {regstack-0.5.6 → regstack-0.6.0}/docs/api.md +0 -0
  41. {regstack-0.5.6 → regstack-0.6.0}/docs/architecture.md +0 -0
  42. {regstack-0.5.6 → regstack-0.6.0}/docs/cli.md +0 -0
  43. {regstack-0.5.6 → regstack-0.6.0}/docs/conf.py +0 -0
  44. {regstack-0.5.6 → regstack-0.6.0}/docs/embedding.md +0 -0
  45. {regstack-0.5.6 → regstack-0.6.0}/docs/index.md +0 -0
  46. {regstack-0.5.6 → regstack-0.6.0}/docs/oauth.md +0 -0
  47. {regstack-0.5.6 → regstack-0.6.0}/docs/quickstart.md +0 -0
  48. {regstack-0.5.6 → regstack-0.6.0}/docs/security-reports/README.md +0 -0
  49. {regstack-0.5.6 → regstack-0.6.0}/docs/theming.md +0 -0
  50. {regstack-0.5.6 → regstack-0.6.0}/examples/_common/__init__.py +0 -0
  51. {regstack-0.5.6 → regstack-0.6.0}/examples/_common/app.py +0 -0
  52. {regstack-0.5.6 → regstack-0.6.0}/examples/mongo/README.md +0 -0
  53. {regstack-0.5.6 → regstack-0.6.0}/examples/mongo/branding/theme.css +0 -0
  54. {regstack-0.5.6 → regstack-0.6.0}/examples/mongo/main.py +0 -0
  55. {regstack-0.5.6 → regstack-0.6.0}/examples/mongo/regstack.toml +0 -0
  56. {regstack-0.5.6 → regstack-0.6.0}/examples/postgres/README.md +0 -0
  57. {regstack-0.5.6 → regstack-0.6.0}/examples/postgres/main.py +0 -0
  58. {regstack-0.5.6 → regstack-0.6.0}/examples/postgres/regstack.toml +0 -0
  59. {regstack-0.5.6 → regstack-0.6.0}/examples/sqlite/README.md +0 -0
  60. {regstack-0.5.6 → regstack-0.6.0}/examples/sqlite/main.py +0 -0
  61. {regstack-0.5.6 → regstack-0.6.0}/examples/sqlite/regstack.toml +0 -0
  62. {regstack-0.5.6 → regstack-0.6.0}/regstack.toml.example +0 -0
  63. {regstack-0.5.6 → regstack-0.6.0}/scripts/ccr_coverage_setup.py +0 -0
  64. {regstack-0.5.6 → regstack-0.6.0}/scripts/security-review-prompt.md +0 -0
  65. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/__init__.py +0 -0
  66. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/app.py +0 -0
  67. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/auth/__init__.py +0 -0
  68. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/auth/clock.py +0 -0
  69. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/auth/dependencies.py +0 -0
  70. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/auth/jwt.py +0 -0
  71. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/auth/lockout.py +0 -0
  72. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/auth/mfa.py +0 -0
  73. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/auth/password.py +0 -0
  74. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/auth/rate_limit.py +0 -0
  75. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/auth/tokens.py +0 -0
  76. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/__init__.py +0 -0
  77. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/base.py +0 -0
  78. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/factory.py +0 -0
  79. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/__init__.py +0 -0
  80. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/backend.py +0 -0
  81. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/client.py +0 -0
  82. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/indexes.py +0 -0
  83. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
  84. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
  85. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
  86. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +0 -0
  87. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
  88. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/__init__.py +0 -0
  89. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/backend.py +0 -0
  90. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/migrations/__init__.py +0 -0
  91. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/migrations/env.py +0 -0
  92. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  93. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  94. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
  95. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/repositories/__init__.py +0 -0
  96. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  97. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  98. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
  99. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +0 -0
  100. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
  101. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
  102. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/schema.py +0 -0
  103. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/backends/sql/types.py +0 -0
  104. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/cli/__init__.py +0 -0
  105. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/cli/_runtime.py +0 -0
  106. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/cli/admin.py +0 -0
  107. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/cli/doctor.py +0 -0
  108. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/cli/init.py +0 -0
  109. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/cli/migrate.py +0 -0
  110. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/config/__init__.py +0 -0
  111. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/config/loader.py +0 -0
  112. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/config/schema.py +0 -0
  113. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/config/secrets.py +0 -0
  114. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/__init__.py +0 -0
  115. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/base.py +0 -0
  116. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/composer.py +0 -0
  117. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/console.py +0 -0
  118. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/factory.py +0 -0
  119. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/ses.py +0 -0
  120. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/smtp.py +0 -0
  121. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/email_change.html +0 -0
  122. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/email_change.subject.txt +0 -0
  123. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/email_change.txt +0 -0
  124. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/password_reset.html +0 -0
  125. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  126. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/password_reset.txt +0 -0
  127. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  128. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  129. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/verification.html +0 -0
  130. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/verification.subject.txt +0 -0
  131. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/email/templates/verification.txt +0 -0
  132. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/hooks/__init__.py +0 -0
  133. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/hooks/events.py +0 -0
  134. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/models/__init__.py +0 -0
  135. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/models/_objectid.py +0 -0
  136. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/models/login_attempt.py +0 -0
  137. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/models/mfa_code.py +0 -0
  138. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/models/oauth_identity.py +0 -0
  139. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/models/oauth_state.py +0 -0
  140. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/models/pending_registration.py +0 -0
  141. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/oauth/__init__.py +0 -0
  142. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/oauth/base.py +0 -0
  143. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/oauth/errors.py +0 -0
  144. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/oauth/providers/__init__.py +0 -0
  145. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/oauth/providers/google.py +0 -0
  146. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/oauth/registry.py +0 -0
  147. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/__init__.py +0 -0
  148. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/_helpers.py +0 -0
  149. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/_schemas.py +0 -0
  150. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/logout.py +0 -0
  151. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/password.py +0 -0
  152. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/phone.py +0 -0
  153. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/routers/register.py +0 -0
  154. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/sms/__init__.py +0 -0
  155. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/sms/base.py +0 -0
  156. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/sms/factory.py +0 -0
  157. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/sms/null.py +0 -0
  158. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/sms/sns.py +0 -0
  159. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/sms/twilio.py +0 -0
  160. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/__init__.py +0 -0
  161. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/pages.py +0 -0
  162. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/static/css/core.css +0 -0
  163. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/static/css/theme.css +0 -0
  164. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  165. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/templates/auth/forgot.html +0 -0
  166. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/templates/auth/login.html +0 -0
  167. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/templates/auth/me.html +0 -0
  168. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  169. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
  170. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/templates/auth/register.html +0 -0
  171. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/templates/auth/reset.html +0 -0
  172. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/templates/auth/verify.html +0 -0
  173. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/ui/templates/base.html +0 -0
  174. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/__init__.py +0 -0
  175. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/oauth_google/__init__.py +0 -0
  176. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/oauth_google/cli.py +0 -0
  177. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/oauth_google/routes.py +0 -0
  178. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/oauth_google/server.py +0 -0
  179. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
  180. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
  181. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
  182. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/oauth_google/validators.py +0 -0
  183. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/oauth_google/window.py +0 -0
  184. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/oauth_google/writer.py +0 -0
  185. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/theme_designer/__init__.py +0 -0
  186. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/theme_designer/cli.py +0 -0
  187. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/theme_designer/routes.py +0 -0
  188. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/theme_designer/server.py +0 -0
  189. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/theme_designer/static/designer.css +0 -0
  190. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/theme_designer/static/designer.js +0 -0
  191. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/theme_designer/templates/designer.html +0 -0
  192. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/theme_designer/validators.py +0 -0
  193. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/theme_designer/window.py +0 -0
  194. {regstack-0.5.6 → regstack-0.6.0}/src/regstack/wizard/theme_designer/writer.py +0 -0
  195. {regstack-0.5.6 → regstack-0.6.0}/tasks/oauth-design.md +0 -0
  196. {regstack-0.5.6 → regstack-0.6.0}/tasks.py +0 -0
  197. {regstack-0.5.6 → regstack-0.6.0}/tests/__init__.py +0 -0
  198. {regstack-0.5.6 → regstack-0.6.0}/tests/_fake_google/__init__.py +0 -0
  199. {regstack-0.5.6 → regstack-0.6.0}/tests/_fake_google/provider.py +0 -0
  200. {regstack-0.5.6 → regstack-0.6.0}/tests/conftest.py +0 -0
  201. {regstack-0.5.6 → regstack-0.6.0}/tests/e2e/__init__.py +0 -0
  202. {regstack-0.5.6 → regstack-0.6.0}/tests/e2e/conftest.py +0 -0
  203. {regstack-0.5.6 → regstack-0.6.0}/tests/e2e/test_theme_designer.py +0 -0
  204. {regstack-0.5.6 → regstack-0.6.0}/tests/e2e/test_wizard_oauth_flow.py +0 -0
  205. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/__init__.py +0 -0
  206. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_happy_path.py +0 -0
  207. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_indexes.py +0 -0
  208. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_login_lockout.py +0 -0
  209. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_mfa.py +0 -0
  210. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_oauth_repos.py +0 -0
  211. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_oauth_ui.py +0 -0
  212. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_password_reset.py +0 -0
  213. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_rate_limits.py +0 -0
  214. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_sql_migrations.py +0 -0
  215. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_ui_router.py +0 -0
  216. {regstack-0.5.6 → regstack-0.6.0}/tests/integration/test_verification.py +0 -0
  217. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/__init__.py +0 -0
  218. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_base_install_imports.py +0 -0
  219. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_cli.py +0 -0
  220. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_cli_doctor.py +0 -0
  221. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_cli_init.py +0 -0
  222. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_cli_migrate.py +0 -0
  223. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_config_loader.py +0 -0
  224. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_jwt.py +0 -0
  225. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_lockout.py +0 -0
  226. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_mail_composer.py +0 -0
  227. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_mfa_code_repo.py +0 -0
  228. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_oauth_google.py +0 -0
  229. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_password.py +0 -0
  230. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_ses_backend.py +0 -0
  231. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_sms.py +0 -0
  232. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_smtp_backend.py +0 -0
  233. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_theme_designer_cli.py +0 -0
  234. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_theme_designer_routes.py +0 -0
  235. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_theme_designer_validators.py +0 -0
  236. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_theme_designer_writer.py +0 -0
  237. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_ui_env.py +0 -0
  238. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_wizard_oauth_cli.py +0 -0
  239. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_wizard_oauth_routes.py +0 -0
  240. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_wizard_oauth_validators.py +0 -0
  241. {regstack-0.5.6 → regstack-0.6.0}/tests/unit/test_wizard_oauth_writer.py +0 -0
@@ -4,6 +4,11 @@ name: publish
4
4
  # Configure the PyPI project (https://pypi.org/manage/account/publishing/)
5
5
  # with the publisher: this repository, environment name `pypi`, workflow
6
6
  # `publish.yml`. No PyPI tokens live in GitHub Actions secrets.
7
+ #
8
+ # Third-party actions are pinned to commit SHAs (not mutable tags) so a
9
+ # tag swap upstream can't substitute a malicious version. Update by
10
+ # resolving the latest SHA for the version you want and bumping the
11
+ # trailing `# vN` comment to match.
7
12
 
8
13
  on:
9
14
  push:
@@ -11,15 +16,22 @@ on:
11
16
  - "v*"
12
17
  workflow_dispatch:
13
18
 
19
+ # Workflow-level default. Individual jobs override where they need
20
+ # extra scopes (the `publish` job adds `id-token: write` for OIDC).
21
+ permissions:
22
+ contents: read
23
+
14
24
  jobs:
15
25
  build:
16
26
  runs-on: ubuntu-latest
27
+ permissions:
28
+ contents: read
17
29
  steps:
18
- - uses: actions/checkout@v4
30
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
19
31
  with:
20
32
  fetch-depth: 0
21
33
 
22
- - uses: astral-sh/setup-uv@v3
34
+ - uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
23
35
  with:
24
36
  enable-cache: true
25
37
 
@@ -42,7 +54,7 @@ jobs:
42
54
  - name: Build sdist + wheel
43
55
  run: uv build
44
56
 
45
- - uses: actions/upload-artifact@v4
57
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
46
58
  with:
47
59
  name: dist
48
60
  path: dist/
@@ -55,9 +67,10 @@ jobs:
55
67
  name: pypi
56
68
  url: https://pypi.org/p/regstack
57
69
  permissions:
58
- id-token: write # OIDC trusted-publisher
70
+ id-token: write # OIDC trusted-publisher exchange
71
+ contents: read
59
72
  steps:
60
- - uses: actions/download-artifact@v4
73
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
61
74
  with:
62
75
  name: dist
63
76
  path: dist/
@@ -10,9 +10,19 @@ concurrency:
10
10
  group: test-${{ github.ref }}
11
11
  cancel-in-progress: true
12
12
 
13
+ # Workflow-level default — every job in this workflow gets read-only
14
+ # permissions unless it overrides explicitly. Third-party actions are
15
+ # pinned to commit SHAs so a tag swap upstream can't substitute a
16
+ # malicious version; update by resolving the latest SHA for the
17
+ # version you want and bumping the trailing `# vN` comment.
18
+ permissions:
19
+ contents: read
20
+
13
21
  jobs:
14
22
  pytest:
15
23
  runs-on: ubuntu-latest
24
+ permissions:
25
+ contents: read
16
26
  strategy:
17
27
  fail-fast: false
18
28
  matrix:
@@ -47,9 +57,9 @@ jobs:
47
57
  # Connect as superuser so the per-test fixture can CREATE/DROP DATABASE.
48
58
  REGSTACK_TEST_POSTGRES_URL: postgresql+asyncpg://regstack:regstack@localhost:5432
49
59
  steps:
50
- - uses: actions/checkout@v4
60
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
51
61
 
52
- - uses: astral-sh/setup-uv@v3
62
+ - uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
53
63
  with:
54
64
  enable-cache: true
55
65
  cache-dependency-glob: "uv.lock"
@@ -71,16 +81,18 @@ jobs:
71
81
 
72
82
  docs:
73
83
  runs-on: ubuntu-latest
84
+ permissions:
85
+ contents: read
74
86
  steps:
75
- - uses: actions/checkout@v4
76
- - uses: astral-sh/setup-uv@v3
87
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
88
+ - uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
77
89
  with:
78
90
  enable-cache: true
79
91
  cache-dependency-glob: "uv.lock"
80
92
  - run: uv python pin 3.11
81
93
  - run: uv sync --extra docs --extra dev
82
94
  - run: uv run sphinx-build -b html -W --keep-going docs docs/_build/html
83
- - uses: actions/upload-artifact@v4
95
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
84
96
  with:
85
97
  name: docs-html
86
98
  path: docs/_build/html
@@ -93,9 +105,11 @@ jobs:
93
105
  # equivalent in-process via meta_path blocking; this job verifies the
94
106
  # actual built wheel installs and imports against a clean Python.
95
107
  runs-on: ubuntu-latest
108
+ permissions:
109
+ contents: read
96
110
  steps:
97
- - uses: actions/checkout@v4
98
- - uses: astral-sh/setup-uv@v3
111
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
112
+ - uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
99
113
  with:
100
114
  enable-cache: true
101
115
  cache-dependency-glob: "uv.lock"
@@ -32,3 +32,15 @@ docs/_autosummary/
32
32
  /regstack-bootstrap.json
33
33
  **/regstack.secrets.env
34
34
  **/regstack-bootstrap.json
35
+
36
+ # Defensive: keep credential material out of any commit, even from a
37
+ # misconfigured local dev env. None of these are checked in today, but
38
+ # the .env / *.pem / etc. patterns are recurring audit recommendations.
39
+ .env
40
+ .env.*
41
+ *.pem
42
+ *.key
43
+ *.p12
44
+ *.pfx
45
+ *.jks
46
+ *.crt
@@ -0,0 +1,463 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The
4
+ authoritative copy lives at
5
+ [`docs/changelog.md`](docs/changelog.md) and is rendered into the
6
+ Sphinx docs.
7
+
8
+ ## 0.6.0 — 2026-05-14
9
+
10
+ **Breaking change for wizard users.** The GUI setup wizards
11
+ (`regstack oauth setup`, `regstack theme design`) are now behind a
12
+ new optional `wizard` extra. `pip install regstack` no longer pulls
13
+ in `pywebview`, `tomlkit`, or `uvicorn[standard]` — three heavy
14
+ wizard-only dependencies that every library consumer was paying for,
15
+ including a platform browser engine on every fresh install. A
16
+ recurring audit recommendation since 0.5.0.
17
+
18
+ **Migration.**
19
+
20
+ - If you only embed regstack in a FastAPI app (no `regstack oauth
21
+ setup` or `regstack theme design`): no action needed. The base
22
+ install is now significantly slimmer.
23
+ - If you use either setup wizard: install the new extra —
24
+ `pip install 'regstack[wizard]'` or `uv sync --extra wizard`.
25
+ Running a wizard subcommand without the extra now exits with a
26
+ one-line install hint (no ImportError traceback).
27
+ - The `dev` extra continues to pull in the wizard deps directly so
28
+ `inv test-all` keeps working without an explicit `--extra wizard`.
29
+
30
+ Bumped to **0.6.0** (not 0.5.12) because removing top-level deps is
31
+ the kind of change that can surprise downstream `pip install
32
+ regstack` callers — even though "the GUI wizard CLIs need an extra
33
+ now" is the only observable effect.
34
+
35
+ ## 0.5.11 — 2026-05-14
36
+
37
+ CI / workflow hygiene. No runtime code changes.
38
+
39
+ - **All third-party GitHub Actions pinned to commit SHAs.**
40
+ `actions/checkout@v4`, `astral-sh/setup-uv@v3`,
41
+ `actions/upload-artifact@v4`, and `actions/download-artifact@v4` now
42
+ use commit SHAs in `.github/workflows/publish.yml` and
43
+ `.github/workflows/test.yml`, with `# v4` / `# v3` trailing comments
44
+ so future operators can resolve and bump. `pypa/gh-action-pypi-publish`
45
+ was already SHA-pinned (#37). A tag swap upstream can no longer
46
+ substitute a malicious version.
47
+ - **`permissions:` blocks added to every workflow + job.** Both
48
+ workflows now declare a `permissions: contents: read` default at
49
+ the workflow level and re-state it per job (so a future addition of
50
+ a write-needing action doesn't silently inherit elevated scopes).
51
+ The `publish` job continues to declare `id-token: write` (OIDC
52
+ trusted-publisher exchange) — that's the only scope above
53
+ read-only anywhere in the workflows.
54
+ - **`.gitignore` defensive additions.** `.env`, `.env.*`, and the
55
+ common credential-file patterns (`*.pem`, `*.key`, `*.p12`,
56
+ `*.pfx`, `*.jks`, `*.crt`) are now ignored at the repo root. None
57
+ are present today; this is belt-and-braces for misconfigured local
58
+ dev environments. A recurring audit recommendation.
59
+
60
+ ## 0.5.10 — 2026-05-14
61
+
62
+ Security fixes from the 2026-05-13 / 2026-05-14 daily reviews. All
63
+ warnings, no criticals — but several are real exploitable issues.
64
+
65
+ **Security.**
66
+
67
+ - **Open-redirect bypass in OAuth `redirect_to`.** `_validate_redirect`
68
+ was forwarding `urlsplit`'s judgment, but browsers normalize values
69
+ like `/\evil.com` and `////evil.com` into the protocol-relative
70
+ `//evil.com` — both of which `urlsplit` reports as same-origin
71
+ paths. The validator now rejects any backslash plus any value that
72
+ doesn't start with a single `/` followed by a non-slash character.
73
+ - **CVE-2025-62727 — `fastapi` floor raised to `>=0.120.0`.** Starlette
74
+ DoS via large request bodies after multipart processing.
75
+ - **CVE-2025-27516 — `jinja2` floor raised to `>=3.1.6`.** Sandbox
76
+ breakout via the `|attr` filter (only relevant if hosts allow
77
+ user-controlled templates; tightening the floor regardless).
78
+ - **Login lockout no longer skips disabled / unverified accounts.**
79
+ `POST /login` now records a failure before raising HTTP 403 for
80
+ `is_active=False` and (when `require_verification=True`)
81
+ `is_verified=False` users. Password verification was also re-ordered
82
+ to run **before** those checks, so an attacker without the password
83
+ can't distinguish disabled vs active accounts by HTTP code alone.
84
+ - **`POST /change-email` no longer enumerates registered addresses.**
85
+ An authenticated attacker could previously iterate the email
86
+ namespace via the 409 vs 202 response distinction. The endpoint now
87
+ always returns 202; if the candidate is already registered, no
88
+ confirmation email is sent (the legitimate user finds out by not
89
+ receiving it). Matches the existing anti-enumeration stance on
90
+ `/forgot-password` and `/resend-verification`.
91
+ - **Admin resend-verification rejects OAuth-only users.** Previously
92
+ attempted to construct a `PendingRegistration` from a user with
93
+ `hashed_password=None`, which either failed validation or stored
94
+ the literal string `"None"` in the pending row. Now returns 400
95
+ with a clear message.
96
+
97
+ ## 0.5.9 — 2026-05-13
98
+
99
+ **`OAuthConfig.enforce_mfa_on_oauth_signin` is now wired.** The flag
100
+ has been on the config since the OAuth router shipped (0.3.0) and the
101
+ wizard surfaced it, but the callback never read it — operators who
102
+ flipped it on still got OAuth sign-ins that bypassed the SMS second
103
+ factor. The High #1 finding from the post-0.5.6 consistency audit.
104
+
105
+ Now, when the flag is `true` and the resolved user has SMS MFA set
106
+ up (`is_mfa_enabled=True` plus a `phone_number`):
107
+
108
+ - The OAuth callback sends the SMS code and stashes a short-lived
109
+ `login_mfa` pending JWT in the state row (instead of a session
110
+ token).
111
+ - `POST /oauth/exchange` returns `mfa_required=True` and
112
+ `mfa_pending_token=...` (with no `access_token`) so the SPA knows
113
+ to redirect to `/account/mfa-confirm`.
114
+ - The SPA's bundled `regstack.js` `oauth-complete` handler stashes
115
+ the pending token under `regstack.mfa_pending` (same key the
116
+ password-login MFA flow uses) and redirects.
117
+ - The user enters the SMS code and hits the existing
118
+ `POST /login/mfa-confirm` endpoint — same downstream path as the
119
+ password-login second factor.
120
+
121
+ Link flows (`mode="link"`) are exempt: the user was already
122
+ authenticated when they kicked off the link, so adding SMS friction
123
+ on top of an already-authenticated link operation has no
124
+ threat-model win.
125
+
126
+ The `ExchangeResponse` model grew two optional fields
127
+ (`mfa_required: bool = False`, `mfa_pending_token: str | None = None`)
128
+ and `access_token` is now defaulted to `""` so the MFA branch can
129
+ return cleanly. Existing handlers reading `access_token` keep
130
+ working — they just need to check `mfa_required` first.
131
+
132
+ ## 0.5.8 — 2026-05-13
133
+
134
+ Audit-driven consistency cleanup — small fixes across the API surface
135
+ flagged by the post-0.5.6 consistency review.
136
+
137
+ **Security**
138
+
139
+ - **`oauth.completion_ttl_seconds` is finally enforced.** The flag has
140
+ been on `OAuthConfig` since the OAuth router shipped, but the
141
+ callback never used it: a state row stayed valid for the full
142
+ `state_ttl_seconds` (300s default) between callback completion and
143
+ `/oauth/exchange`. Now `set_result_token(...)` bumps the row's
144
+ expiry down to `now + completion_ttl_seconds` (30s default), so the
145
+ blast radius of a stolen state_id post-callback is the documented
146
+ 30-second window. `OAuthStateRepoProtocol.set_result_token` grew an
147
+ optional `new_expires_at=` kwarg to make this atomic with the
148
+ token write.
149
+
150
+ **Changed (UserPublic surface)**
151
+
152
+ - `UserPublic` now serialises `updated_at` and
153
+ `tokens_invalidated_after`. SPAs comparing the latter against their
154
+ cached session JWT's `iat` can detect a forced sign-out after a
155
+ password / email change without an extra round-trip.
156
+
157
+ **Changed (hook payloads)**
158
+
159
+ - `oauth_signin_started` in `mode="link"` now carries the
160
+ authenticated `user=` kwarg, matching `oauth_signin_completed` and
161
+ `oauth_account_linked`. The `mode="signin"` call site stays without
162
+ `user=` (there isn't one yet — sign-in is what produces it).
163
+
164
+ **Internal**
165
+
166
+ - `OAuthConfig.completion_ttl_seconds` config field is now load-bearing
167
+ (was previously declared-but-unread).
168
+ - `MessageResponse` in `routers/oauth.py` deleted; the router now uses
169
+ the shared one from `routers/_schemas.py`. OpenAPI no longer carries
170
+ two identically-named schemas.
171
+ - `MongoOAuthStateRepo` / `SqlOAuthStateRepo` `set_result_token` grew
172
+ a `new_expires_at` parameter (default `None`, so existing callers
173
+ see no change).
174
+ - `MongoBlacklistRepo.purge_expired` switched from `$lte` to `$lt` to
175
+ match the rest of the `purge_expired` family across both backends.
176
+ Edge-instant tokens get one more microsecond of life — the
177
+ bulk-revoke check (which DOES use `<=`) is unchanged.
178
+ - Dead `create()` and `delete_by_id()` methods removed from
179
+ `MongoPendingRepo` — neither was in the protocol or the SQL impl,
180
+ and nothing in src or tests called them.
181
+ - OAuth `start` and `callback` endpoints now declare
182
+ `response_class=RedirectResponse` and `status_code=302`. OpenAPI
183
+ surfaces the redirect intent properly.
184
+ - Custom-claim JWT encoder in `routers/account.py` (email-change
185
+ token) now emits `iat` as a float instead of `int`, matching the
186
+ three other custom-claim encoders and the bulk-revoke contract.
187
+ - `routers/verify.py` `created_at` for resent pending registrations
188
+ now goes through `rs.clock.now()` instead of wall-clock
189
+ `datetime.now(UTC)` — keeps `FrozenClock`-driven tests
190
+ deterministic.
191
+ - `BaseUser.model_config = ConfigDict(extra="allow")` got a
192
+ comment explaining why it's the only model in the package that
193
+ doesn't `extra="forbid"`.
194
+
195
+ ## 0.5.7 — 2026-05-13
196
+
197
+ Documentation-only follow-up to 0.5.6.
198
+
199
+ - `docs/configuration.md` now documents the per-route `*_rate_limit`
200
+ family (added in 0.5.4) instead of pointing at
201
+ `login_max_per_minute` / `login_max_per_hour` as reserved future
202
+ fields.
203
+ - `docs/security.md` no longer references
204
+ `PasswordHasher.needs_rehash` (removed in 0.5.6). Replacement
205
+ guidance points hosts at `pwdlib.PasswordHash.verify_and_update`.
206
+ - Root `CHANGELOG.md` backfilled with 0.4.0 and 0.5.0 entries so it
207
+ matches `docs/changelog.md`.
208
+
209
+ ## 0.5.6 — 2026-05-13
210
+
211
+ Eleven days of security-review remediation, supply-chain hardening,
212
+ a full `mypy --strict` cleanup pass, and the per-route rate-limits
213
+ feature rolled up into a single release.
214
+
215
+ **Per-route IP rate limits.** Opt-in via the new `rate_limit` extra
216
+ (or a host-supplied `slowapi.Limiter`) plus any of the new
217
+ `RegStackConfig.*_rate_limit` fields (`login_rate_limit`,
218
+ `register_rate_limit`, `forgot_password_rate_limit`,
219
+ `reset_password_rate_limit`, `verify_rate_limit`,
220
+ `resend_verification_rate_limit`, `change_password_rate_limit`,
221
+ `change_email_rate_limit`, `confirm_email_change_rate_limit`,
222
+ `delete_account_rate_limit`). Each accepts a slowapi-syntax string
223
+ (`"5/minute"`, `"5/minute;20/hour"`). Empty / unset means no limit
224
+ on that route — `LockoutService` still defends `/login` against
225
+ credential stuffing per-account. When `*_rate_limit` strings are
226
+ configured but neither a `rate_limiter=` argument is passed nor
227
+ the `rate_limit` extra is installed, `RegStack.router` raises
228
+ `RuntimeError` on first access — failing closed beats silently
229
+ disabling the protection. Hosts remain responsible for
230
+ `app.state.limiter` and `app.add_exception_handler(RateLimitExceeded, ...)`;
231
+ slowapi owns the 429 response shape. The previously-reserved
232
+ `login_max_per_minute` / `login_max_per_hour` fields are kept for
233
+ back-compat but unwired.
234
+
235
+ **Security fixes.**
236
+
237
+ - JWT 401 detail now returns a static `"Invalid or expired token."`;
238
+ no longer leaks the pyjwt failure reason (signature mismatch /
239
+ expired / malformed / audience mismatch).
240
+ - OAuth sign-in now honours `allow_registration=False`. Previously,
241
+ `/register` respected the flag but the OAuth `_resolve_user`
242
+ "brand-new account" branch did not, creating accounts even when
243
+ self-service signup was disabled.
244
+ - Admin `DELETE /admin/users/{id}` now cascades `oauth_identities`,
245
+ matching the user-initiated `DELETE /account` path. Previously
246
+ left orphan rows that blocked re-registration of the same Google
247
+ subject.
248
+ - `POST /phone/start` and `DELETE /phone` now return 400 (not crash
249
+ with HTTP 500) for OAuth-only users who have no `hashed_password`.
250
+
251
+ **Breaking change — hook contracts.** `mfa_login_started` and
252
+ `phone_setup_started` no longer include the raw OTP code in their
253
+ kwargs. Hooks are best-effort observability and are the documented
254
+ integration surface for analytics / logging / Slack notifications,
255
+ so a plaintext OTP in `**kwargs` is a leak waiting to happen.
256
+ Hosts that subscribed to either event to take over SMS delivery
257
+ should migrate to a custom `SmsService` subclass — the supported
258
+ delivery override.
259
+
260
+ **Dependency floors raised for CVEs.**
261
+
262
+ - `pyjwt>=2.12.1` for CVE-2026-32597 (`crit` header bypass, CVSS 7.5).
263
+ - `cryptography>=46.0.7` added explicitly to the `oauth` extra for
264
+ CVE-2026-26007 (ECC subgroup attack on the JWKS code path, CVSS
265
+ 8.2) plus CVE-2026-34073 and CVE-2026-39892.
266
+ - `python-multipart>=0.0.26` for CVE-2026-40347 (DoS via oversized
267
+ multipart preamble).
268
+
269
+ **Supply chain.** `pypa/gh-action-pypi-publish` in `publish.yml`
270
+ pinned to a commit SHA instead of the mutable `release/v1` branch.
271
+ The publish job holds `id-token: write`, so a tag/branch swap
272
+ upstream would let an attacker push a malicious wheel under our
273
+ OIDC identity.
274
+
275
+ **Removed.** `PasswordHasher.needs_rehash` — called pwdlib's
276
+ non-existent `check_needs_rehash` and would `AttributeError` if
277
+ anyone invoked it. No callers in src or tests. If you were planning
278
+ to use it, call `pwdlib.PasswordHash.verify_and_update` directly.
279
+
280
+ **Internal.** 72 `mypy --strict` errors cleared across 35 files;
281
+ `inv lint` is now green end-to-end. Mongo
282
+ `BlacklistRepo.purge_expired` added (protocol parity with SQL).
283
+ `KNOWN_EVENTS` reconciled — 7 previously-undeclared events added
284
+ (`verification_requested`, `email_change_requested`, `email_changed`,
285
+ `phone_setup_started`, `mfa_login_started`, `mfa_enabled`,
286
+ `mfa_disabled`). `user_logged_out` now actually fires from
287
+ `routers/logout.py` (was listed in `KNOWN_EVENTS` but no router
288
+ emitted it).
289
+
290
+ ## 0.5.0 — 2026-05-02
291
+
292
+ **Theme designer.** `regstack theme design` opens a native pywebview
293
+ window with controls for every `--rs-*` CSS custom property and a
294
+ real-time preview of the bundled SSR widgets (sign-in form, success /
295
+ error banners, danger-zone button). Saving writes `regstack-theme.css`;
296
+ the designer round-trips values back into the form on next launch so
297
+ iteration is non-destructive. `--print-only` mode takes repeatable
298
+ `--var NAME=VALUE` pairs (with a `dark:` prefix for dark-scheme
299
+ overrides) and writes the file headlessly. Lives in
300
+ `regstack.wizard.theme_designer`; registered as a lazy Click subgroup
301
+ so `regstack init` / `doctor` don't pay the pywebview/uvicorn import
302
+ cost.
303
+
304
+ **Docs.** New "About the examples" convention block at the top of
305
+ `docs/index.md`. Every URL, email, smtp host, and admin command across
306
+ the docs now extrapolates from the same fictional app at
307
+ `app.example.com` with `<username>` / `<password>` placeholders.
308
+
309
+ ## 0.4.0 — 2026-05-02
310
+
311
+ **OAuth setup wizard.** `regstack oauth setup` opens a native webview
312
+ window that walks an operator through registering a Google OAuth 2.0
313
+ client and merges the credentials into `regstack.toml` +
314
+ `regstack.secrets.env` non-destructively (preserves comments, other
315
+ tables, unrelated keys). 12-step SPA inside a local-only 127.0.0.1
316
+ FastAPI server, gated by a per-launch random token. Each "Next" click
317
+ hits a server-side validator so the Write step can never be reached
318
+ with bad data. `--print-only` mode skips the GUI for headless / CI
319
+ use.
320
+
321
+ Three new base dependencies — `pywebview>=5.0`, `tomlkit>=0.13`,
322
+ `uvicorn[standard]>=0.29` — for the wizard's local server.
323
+ `pytest-playwright` added to the `dev` extra; new `inv test-e2e` task
324
+ chained into `inv test-all`.
325
+
326
+ ## 0.3.0 — 2026-04-30
327
+
328
+ **OAuth — Sign in with Google.** Opt-in via the new `oauth` extra
329
+ and `enable_oauth=True`. Five JSON endpoints, an SSR
330
+ `/account/oauth-complete` page, "Sign in with Google" button on the
331
+ login page, and a Connected-accounts panel on `/account/me`.
332
+
333
+ Schema migration `0002_oauth.py` creates `oauth_identities` +
334
+ `oauth_states` and makes `users.hashed_password` nullable
335
+ (OAuth-only users have no password). Roll forward via
336
+ `regstack migrate` or first-boot `install_schema()` — no manual
337
+ intervention.
338
+
339
+ Account-linking policy defaults to **refuse**: if a Google sign-in
340
+ arrives carrying an email that already belongs to a password-
341
+ registered user, the callback returns `?error=email_in_use` and the
342
+ user must sign in then explicitly link from `/account/me`. Hosts
343
+ that consciously accept the email-recycling threat for UX can flip
344
+ `oauth.auto_link_verified_emails = true`. See
345
+ [`docs/oauth.md`](https://regstack.readthedocs.io/en/latest/oauth.html)
346
+ and [`tasks/oauth-design.md`](https://github.com/jdrumgoole/regstack/blob/main/tasks/oauth-design.md)
347
+ for the full threat model.
348
+
349
+ **Migration**
350
+
351
+ - Install the new extra: `uv add 'regstack[oauth]'`.
352
+ - Set `enable_oauth = true` and provide `oauth.google_client_id` +
353
+ `oauth.google_client_secret`.
354
+ - Run `regstack migrate` (SQL backends only) or rely on
355
+ `install_schema()` at first boot.
356
+
357
+ `BaseUser.hashed_password` is now `str | None`. Code that imported
358
+ the field type explicitly will need to widen it.
359
+
360
+ ## 0.2.6 — 2026-04-28
361
+
362
+ Bug fix.
363
+
364
+ - **Fix:** `/admin/stats` reported `pending_registrations: 0` on
365
+ every SQL backend. The route reached into the Mongo repo's private
366
+ `_collection` attribute and silently fell back to `0` when the
367
+ attribute was absent. Added `count_unexpired(now=None)` to
368
+ `PendingRepoProtocol` with Mongo + SQL implementations and routed
369
+ through `rs.clock.now()` so the count respects the injected clock.
370
+ New parametrized integration test exercises the count on every
371
+ backend.
372
+
373
+ ## 0.2.5 — 2026-04-28
374
+
375
+ Bug fix + tooling.
376
+
377
+ - **Fix:** `regstack doctor` against a SQL backend crashed with
378
+ `asyncio.run() cannot be called from a running event loop`. The
379
+ schema check called `regstack.backends.sql.migrations.current()`,
380
+ which used `asyncio.run()` internally — invalid inside doctor's own
381
+ `asyncio.run`. Added `current_async()` and switched the doctor
382
+ command to use it. Sync `current()` is preserved for the migrate
383
+ CLI.
384
+ - **New:** `inv coverage [--no-html] [--fail-under=N]` runs the full
385
+ three-backend matrix under coverage and writes term + HTML reports.
386
+ Branch coverage is on by default.
387
+ - Test coverage uplift on the CLI: `cli/init.py` 14% → 88%,
388
+ `cli/doctor.py` 61% → 87%. Total: **85% → 87.1%**.
389
+
390
+ ## 0.2.4 — 2026-04-28
391
+
392
+ **Breaking** — back-compat shims removed:
393
+
394
+ - `RegStack.install_indexes()` (alias for `install_schema()`).
395
+ - `ObjectIdStr` alias for `IdStr` in `regstack.models._objectid`.
396
+ - Re-exports of `UserAlreadyExistsError`,
397
+ `PendingAlreadyExistsError`, `MfaVerifyOutcome`, and
398
+ `MfaVerifyResult` from `regstack.backends.mongo.repositories.*`.
399
+ Their canonical home is `regstack.backends.protocols`.
400
+
401
+ If you import any of these from the old paths, switch to:
402
+ - `RegStack.install_schema()`
403
+ - `from regstack.models._objectid import IdStr`
404
+ - `from regstack.backends.protocols import UserAlreadyExistsError`
405
+ (and friends).
406
+
407
+ The internal mongo `install_indexes(db, config)` function is unchanged.
408
+
409
+ ## 0.2.3 — 2026-04-28
410
+
411
+ Docs-only release. Restructured the API reference around the current
412
+ package layout (post multi-backend refactor) and added Google-style
413
+ docstrings (Args / Returns / Raises) to the public surface — RegStack,
414
+ JwtCodec, PasswordHasher, LockoutService, AuthDependencies,
415
+ HookRegistry, EmailService, SmsService, the router builders, and the
416
+ Clock implementations. Dataclass field docs moved to PEP 258
417
+ attribute docstrings. Sphinx builds clean under `-W` again.
418
+
419
+ ## 0.2.2 — 2026-04-28
420
+
421
+ Docs-only release. The README and Sphinx docs landing page now lead
422
+ with the same pitch (problem framing, "Why not just use…?" comparison
423
+ vs Auth0 / Clerk / Keycloak / fastapi-users) before diving into
424
+ architecture. Hyperlink density trimmed back: only major external
425
+ packages, products, and JWT (RFC 7519) are linked — Wikipedia trivia,
426
+ MDN basics, OWASP article links, and deep-dependency helper-class
427
+ docs were removed.
428
+
429
+ ## 0.2.1 — 2026-04-28
430
+
431
+ Hotfix for 0.2.0: `import regstack` failed on a base install because
432
+ several modules in the import path (`models/_objectid.py`,
433
+ `backends/protocols.py`, four routers, and the SQL `mfa_code_repo`)
434
+ had unconditional `from bson …` / `from regstack.backends.mongo …`
435
+ imports — but `pymongo` became an optional `mongo` extra in 0.2.0.
436
+ Added a CI smoketest that builds the wheel and imports it in a
437
+ no-extras venv, plus an in-process regression test that blocks `bson`
438
+ / `pymongo` via `sys.meta_path`.
439
+
440
+ ## 0.2.0 — 2026-04-28
441
+
442
+ Multi-backend support — SQLite (default), Postgres, MongoDB — switched
443
+ by `database_url` URL scheme. Bundled Alembic migrations for SQL
444
+ backends. Embedding API change: `RegStack(config=, db=)` →
445
+ `RegStack(config=, backend=None)`. README + core docs rewritten for
446
+ less-expert readers (problem framing, hyperlinks to external
447
+ standards, comparison vs Auth0/Clerk/Keycloak/fastapi-users).
448
+
449
+ See [`docs/changelog.md`](docs/changelog.md) for the full per-feature
450
+ breakdown.
451
+
452
+ ## 0.1.1 — 2026-04-27
453
+
454
+ - Rewrite README relative links as absolute URLs so they resolve on the
455
+ PyPI project page. README-only release.
456
+
457
+ ## 0.1.0 — 2026-04-27
458
+
459
+ First tagged release. Bundles M1–M6 from the development plan into a
460
+ single Apache-2.0 package on PyPI.
461
+
462
+ See [`docs/changelog.md`](docs/changelog.md) for the per-milestone
463
+ breakdown of M1 through M6.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regstack
3
- Version: 0.5.6
3
+ Version: 0.6.0
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
@@ -22,17 +22,14 @@ Requires-Dist: alembic>=1.13
22
22
  Requires-Dist: click>=8.1
23
23
  Requires-Dist: dnspython>=2.6
24
24
  Requires-Dist: email-validator>=2.1
25
- Requires-Dist: fastapi>=0.110
26
- Requires-Dist: jinja2>=3.1
25
+ Requires-Dist: fastapi>=0.120.0
26
+ Requires-Dist: jinja2>=3.1.6
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
30
  Requires-Dist: pyjwt>=2.12.1
31
31
  Requires-Dist: python-multipart>=0.0.26
32
- Requires-Dist: pywebview>=5.0
33
32
  Requires-Dist: sqlalchemy[asyncio]>=2.0
34
- Requires-Dist: tomlkit>=0.13
35
- Requires-Dist: uvicorn[standard]>=0.29
36
33
  Provides-Extra: dev
37
34
  Requires-Dist: anyio>=4.3; extra == 'dev'
38
35
  Requires-Dist: asyncpg>=0.29; extra == 'dev'
@@ -47,8 +44,10 @@ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
47
44
  Requires-Dist: pytest-playwright>=0.5; extra == 'dev'
48
45
  Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
49
46
  Requires-Dist: pytest>=8.0; extra == 'dev'
47
+ Requires-Dist: pywebview>=5.0; extra == 'dev'
50
48
  Requires-Dist: ruff>=0.4; extra == 'dev'
51
49
  Requires-Dist: slowapi>=0.1.9; extra == 'dev'
50
+ Requires-Dist: tomlkit>=0.13; extra == 'dev'
52
51
  Requires-Dist: uvicorn[standard]>=0.29; extra == 'dev'
53
52
  Provides-Extra: docs
54
53
  Requires-Dist: furo>=2024.1; extra == 'docs'
@@ -72,6 +71,10 @@ Provides-Extra: sns
72
71
  Requires-Dist: aioboto3>=12.3; extra == 'sns'
73
72
  Provides-Extra: twilio
74
73
  Requires-Dist: twilio>=9.0; extra == 'twilio'
74
+ Provides-Extra: wizard
75
+ Requires-Dist: pywebview>=5.0; extra == 'wizard'
76
+ Requires-Dist: tomlkit>=0.13; extra == 'wizard'
77
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'wizard'
75
78
  Description-Content-Type: text/markdown
76
79
 
77
80
  # regstack