regstack 0.5.9__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 (240) hide show
  1. {regstack-0.5.9 → regstack-0.6.0}/.github/workflows/publish.yml +18 -5
  2. {regstack-0.5.9 → regstack-0.6.0}/.github/workflows/test.yml +21 -7
  3. {regstack-0.5.9 → regstack-0.6.0}/.gitignore +12 -0
  4. {regstack-0.5.9 → regstack-0.6.0}/CHANGELOG.md +89 -0
  5. {regstack-0.5.9 → regstack-0.6.0}/PKG-INFO +9 -6
  6. {regstack-0.5.9 → regstack-0.6.0}/docs/changelog.md +96 -0
  7. {regstack-0.5.9 → regstack-0.6.0}/pyproject.toml +22 -8
  8. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/__main__.py +37 -7
  9. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/account.py +19 -6
  10. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/admin.py +15 -0
  11. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/login.py +17 -5
  12. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/oauth.py +28 -3
  13. regstack-0.6.0/src/regstack/version.py +1 -0
  14. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_account_management.py +32 -12
  15. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_admin_router.py +31 -0
  16. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_oauth_google_router.py +26 -0
  17. regstack-0.6.0/tests/unit/test_cli_wizard_missing_extra.py +59 -0
  18. {regstack-0.5.9 → regstack-0.6.0}/uv.lock +16 -10
  19. regstack-0.5.9/src/regstack/version.py +0 -1
  20. {regstack-0.5.9 → regstack-0.6.0}/.python-version +0 -0
  21. {regstack-0.5.9 → regstack-0.6.0}/.readthedocs.yaml +0 -0
  22. {regstack-0.5.9 → regstack-0.6.0}/CLAUDE.md +0 -0
  23. {regstack-0.5.9 → regstack-0.6.0}/LICENSE +0 -0
  24. {regstack-0.5.9 → regstack-0.6.0}/NOTICE +0 -0
  25. {regstack-0.5.9 → regstack-0.6.0}/README.md +0 -0
  26. {regstack-0.5.9 → regstack-0.6.0}/SECURITY.md +0 -0
  27. {regstack-0.5.9 → regstack-0.6.0}/docs/_static/.gitkeep +0 -0
  28. {regstack-0.5.9 → regstack-0.6.0}/docs/_templates/.gitkeep +0 -0
  29. {regstack-0.5.9 → regstack-0.6.0}/docs/api.md +0 -0
  30. {regstack-0.5.9 → regstack-0.6.0}/docs/architecture.md +0 -0
  31. {regstack-0.5.9 → regstack-0.6.0}/docs/cli.md +0 -0
  32. {regstack-0.5.9 → regstack-0.6.0}/docs/conf.py +0 -0
  33. {regstack-0.5.9 → regstack-0.6.0}/docs/configuration.md +0 -0
  34. {regstack-0.5.9 → regstack-0.6.0}/docs/embedding.md +0 -0
  35. {regstack-0.5.9 → regstack-0.6.0}/docs/index.md +0 -0
  36. {regstack-0.5.9 → regstack-0.6.0}/docs/oauth.md +0 -0
  37. {regstack-0.5.9 → regstack-0.6.0}/docs/quickstart.md +0 -0
  38. {regstack-0.5.9 → regstack-0.6.0}/docs/security-reports/README.md +0 -0
  39. {regstack-0.5.9 → regstack-0.6.0}/docs/security.md +0 -0
  40. {regstack-0.5.9 → regstack-0.6.0}/docs/theming.md +0 -0
  41. {regstack-0.5.9 → regstack-0.6.0}/examples/_common/__init__.py +0 -0
  42. {regstack-0.5.9 → regstack-0.6.0}/examples/_common/app.py +0 -0
  43. {regstack-0.5.9 → regstack-0.6.0}/examples/mongo/README.md +0 -0
  44. {regstack-0.5.9 → regstack-0.6.0}/examples/mongo/branding/theme.css +0 -0
  45. {regstack-0.5.9 → regstack-0.6.0}/examples/mongo/main.py +0 -0
  46. {regstack-0.5.9 → regstack-0.6.0}/examples/mongo/regstack.toml +0 -0
  47. {regstack-0.5.9 → regstack-0.6.0}/examples/postgres/README.md +0 -0
  48. {regstack-0.5.9 → regstack-0.6.0}/examples/postgres/main.py +0 -0
  49. {regstack-0.5.9 → regstack-0.6.0}/examples/postgres/regstack.toml +0 -0
  50. {regstack-0.5.9 → regstack-0.6.0}/examples/sqlite/README.md +0 -0
  51. {regstack-0.5.9 → regstack-0.6.0}/examples/sqlite/main.py +0 -0
  52. {regstack-0.5.9 → regstack-0.6.0}/examples/sqlite/regstack.toml +0 -0
  53. {regstack-0.5.9 → regstack-0.6.0}/regstack.toml.example +0 -0
  54. {regstack-0.5.9 → regstack-0.6.0}/scripts/ccr_coverage_setup.py +0 -0
  55. {regstack-0.5.9 → regstack-0.6.0}/scripts/security-review-prompt.md +0 -0
  56. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/__init__.py +0 -0
  57. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/app.py +0 -0
  58. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/__init__.py +0 -0
  59. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/clock.py +0 -0
  60. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/dependencies.py +0 -0
  61. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/jwt.py +0 -0
  62. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/lockout.py +0 -0
  63. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/mfa.py +0 -0
  64. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/password.py +0 -0
  65. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/rate_limit.py +0 -0
  66. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/tokens.py +0 -0
  67. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/__init__.py +0 -0
  68. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/base.py +0 -0
  69. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/factory.py +0 -0
  70. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/__init__.py +0 -0
  71. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/backend.py +0 -0
  72. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/client.py +0 -0
  73. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/indexes.py +0 -0
  74. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
  75. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
  76. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
  77. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
  78. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +0 -0
  79. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +0 -0
  80. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
  81. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
  82. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/protocols.py +0 -0
  83. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/__init__.py +0 -0
  84. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/backend.py +0 -0
  85. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/migrations/__init__.py +0 -0
  86. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/migrations/env.py +0 -0
  87. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  88. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  89. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
  90. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/__init__.py +0 -0
  91. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  92. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  93. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
  94. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +0 -0
  95. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/oauth_state_repo.py +0 -0
  96. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
  97. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
  98. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/schema.py +0 -0
  99. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/types.py +0 -0
  100. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/__init__.py +0 -0
  101. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/_runtime.py +0 -0
  102. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/admin.py +0 -0
  103. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/doctor.py +0 -0
  104. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/init.py +0 -0
  105. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/migrate.py +0 -0
  106. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/config/__init__.py +0 -0
  107. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/config/loader.py +0 -0
  108. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/config/schema.py +0 -0
  109. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/config/secrets.py +0 -0
  110. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/__init__.py +0 -0
  111. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/base.py +0 -0
  112. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/composer.py +0 -0
  113. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/console.py +0 -0
  114. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/factory.py +0 -0
  115. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/ses.py +0 -0
  116. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/smtp.py +0 -0
  117. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/email_change.html +0 -0
  118. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/email_change.subject.txt +0 -0
  119. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/email_change.txt +0 -0
  120. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/password_reset.html +0 -0
  121. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  122. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/password_reset.txt +0 -0
  123. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  124. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  125. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/verification.html +0 -0
  126. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/verification.subject.txt +0 -0
  127. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/verification.txt +0 -0
  128. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/hooks/__init__.py +0 -0
  129. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/hooks/events.py +0 -0
  130. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/__init__.py +0 -0
  131. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/_objectid.py +0 -0
  132. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/login_attempt.py +0 -0
  133. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/mfa_code.py +0 -0
  134. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/oauth_identity.py +0 -0
  135. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/oauth_state.py +0 -0
  136. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/pending_registration.py +0 -0
  137. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/user.py +0 -0
  138. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/__init__.py +0 -0
  139. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/base.py +0 -0
  140. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/errors.py +0 -0
  141. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/providers/__init__.py +0 -0
  142. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/providers/google.py +0 -0
  143. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/registry.py +0 -0
  144. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/__init__.py +0 -0
  145. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/_helpers.py +0 -0
  146. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/_schemas.py +0 -0
  147. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/logout.py +0 -0
  148. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/password.py +0 -0
  149. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/phone.py +0 -0
  150. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/register.py +0 -0
  151. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/verify.py +0 -0
  152. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/__init__.py +0 -0
  153. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/base.py +0 -0
  154. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/factory.py +0 -0
  155. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/null.py +0 -0
  156. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/sns.py +0 -0
  157. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/twilio.py +0 -0
  158. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/__init__.py +0 -0
  159. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/pages.py +0 -0
  160. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/static/css/core.css +0 -0
  161. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/static/css/theme.css +0 -0
  162. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/static/js/regstack.js +0 -0
  163. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  164. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/forgot.html +0 -0
  165. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/login.html +0 -0
  166. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/me.html +0 -0
  167. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  168. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
  169. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/register.html +0 -0
  170. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/reset.html +0 -0
  171. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/verify.html +0 -0
  172. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/base.html +0 -0
  173. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/__init__.py +0 -0
  174. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/__init__.py +0 -0
  175. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/cli.py +0 -0
  176. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/routes.py +0 -0
  177. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/server.py +0 -0
  178. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
  179. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
  180. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
  181. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/validators.py +0 -0
  182. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/window.py +0 -0
  183. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/writer.py +0 -0
  184. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/__init__.py +0 -0
  185. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/cli.py +0 -0
  186. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/routes.py +0 -0
  187. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/server.py +0 -0
  188. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/static/designer.css +0 -0
  189. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/static/designer.js +0 -0
  190. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/templates/designer.html +0 -0
  191. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/validators.py +0 -0
  192. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/window.py +0 -0
  193. {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/writer.py +0 -0
  194. {regstack-0.5.9 → regstack-0.6.0}/tasks/oauth-design.md +0 -0
  195. {regstack-0.5.9 → regstack-0.6.0}/tasks.py +0 -0
  196. {regstack-0.5.9 → regstack-0.6.0}/tests/__init__.py +0 -0
  197. {regstack-0.5.9 → regstack-0.6.0}/tests/_fake_google/__init__.py +0 -0
  198. {regstack-0.5.9 → regstack-0.6.0}/tests/_fake_google/provider.py +0 -0
  199. {regstack-0.5.9 → regstack-0.6.0}/tests/conftest.py +0 -0
  200. {regstack-0.5.9 → regstack-0.6.0}/tests/e2e/__init__.py +0 -0
  201. {regstack-0.5.9 → regstack-0.6.0}/tests/e2e/conftest.py +0 -0
  202. {regstack-0.5.9 → regstack-0.6.0}/tests/e2e/test_theme_designer.py +0 -0
  203. {regstack-0.5.9 → regstack-0.6.0}/tests/e2e/test_wizard_oauth_flow.py +0 -0
  204. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/__init__.py +0 -0
  205. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_happy_path.py +0 -0
  206. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_indexes.py +0 -0
  207. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_login_lockout.py +0 -0
  208. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_mfa.py +0 -0
  209. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_oauth_repos.py +0 -0
  210. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_oauth_ui.py +0 -0
  211. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_password_reset.py +0 -0
  212. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_rate_limits.py +0 -0
  213. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_sql_migrations.py +0 -0
  214. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_ui_router.py +0 -0
  215. {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_verification.py +0 -0
  216. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/__init__.py +0 -0
  217. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_base_install_imports.py +0 -0
  218. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_cli.py +0 -0
  219. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_cli_doctor.py +0 -0
  220. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_cli_init.py +0 -0
  221. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_cli_migrate.py +0 -0
  222. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_config_loader.py +0 -0
  223. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_jwt.py +0 -0
  224. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_lockout.py +0 -0
  225. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_mail_composer.py +0 -0
  226. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_mfa_code_repo.py +0 -0
  227. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_oauth_google.py +0 -0
  228. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_password.py +0 -0
  229. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_ses_backend.py +0 -0
  230. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_sms.py +0 -0
  231. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_smtp_backend.py +0 -0
  232. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_theme_designer_cli.py +0 -0
  233. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_theme_designer_routes.py +0 -0
  234. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_theme_designer_validators.py +0 -0
  235. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_theme_designer_writer.py +0 -0
  236. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_ui_env.py +0 -0
  237. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_wizard_oauth_cli.py +0 -0
  238. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_wizard_oauth_routes.py +0 -0
  239. {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_wizard_oauth_validators.py +0 -0
  240. {regstack-0.5.9 → 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
@@ -5,6 +5,95 @@ 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.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
+
8
97
  ## 0.5.9 — 2026-05-13
9
98
 
10
99
  **`OAuthConfig.enforce_mfa_on_oauth_signin` is now wired.** The flag
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regstack
3
- Version: 0.5.9
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
@@ -3,6 +3,102 @@
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.6.0 — 2026-05-14
7
+
8
+ ### Changed (BREAKING — wizard install)
9
+
10
+ - **GUI setup wizards now require the optional `wizard` extra.**
11
+ `regstack oauth setup` and `regstack theme design` previously
12
+ worked from a bare `pip install regstack` because their deps —
13
+ `pywebview`, `tomlkit`, and `uvicorn[standard]` — were in the base
14
+ dependency list. That meant every library consumer (including pure
15
+ FastAPI host apps that never run a setup wizard) was paying for a
16
+ platform browser engine and an ASGI server at install time.
17
+ Those three packages now live in a new `wizard` extra; the base
18
+ install is significantly slimmer.
19
+
20
+ **Migration.**
21
+
22
+ - If you embed regstack but don't use the setup wizards: no
23
+ action needed. `import regstack`, `RegStack(...)`, and the
24
+ `regstack init` / `regstack doctor` / `regstack migrate` /
25
+ `regstack create-admin` CLIs all work on the bare install.
26
+ - If you use either setup wizard:
27
+ `pip install 'regstack[wizard]'` or
28
+ `uv sync --extra wizard`.
29
+ - The `dev` extra continues to pull in the wizard deps directly,
30
+ so `inv test-all` keeps working without an explicit `--extra
31
+ wizard`.
32
+
33
+ Running a wizard subcommand without the extra installed now
34
+ exits with a one-line install hint and a non-zero exit code,
35
+ rather than an `ImportError` traceback from deep inside the
36
+ wizard subtree.
37
+
38
+ Bumped to **0.6.0** rather than 0.5.12 because removing top-level
39
+ dependencies can surprise downstream callers; the version line
40
+ signals it's worth a glance at the migration note.
41
+
42
+ ## 0.5.11 — 2026-05-14
43
+
44
+ CI / workflow hygiene. No runtime code changes.
45
+
46
+ ### Security
47
+
48
+ - **All third-party GitHub Actions pinned to commit SHAs.**
49
+ `actions/checkout@v4`, `astral-sh/setup-uv@v3`,
50
+ `actions/upload-artifact@v4`, and `actions/download-artifact@v4`
51
+ now use commit SHAs across both workflows (`pypa/gh-action-pypi-publish`
52
+ was already SHA-pinned in 0.5.6). Tag swaps upstream can no longer
53
+ substitute a malicious version.
54
+ - **`permissions:` blocks declared on every workflow + job.** Both
55
+ workflows declare a `permissions: contents: read` default at the
56
+ workflow level and re-state it per job. The `publish` job
57
+ continues to add `id-token: write` for the OIDC trusted-publisher
58
+ exchange — that's the only scope above read-only anywhere in
59
+ the workflows.
60
+
61
+ ### Internal
62
+
63
+ - `.gitignore` gained `.env`, `.env.*`, and the common credential-file
64
+ patterns (`*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.crt`).
65
+ Belt-and-braces for misconfigured local dev envs; nothing tracked
66
+ today depends on these.
67
+
68
+ ## 0.5.10 — 2026-05-14
69
+
70
+ Security fixes from the 2026-05-13 / 2026-05-14 daily review reports.
71
+
72
+ ### Security
73
+
74
+ - **Open-redirect bypass in OAuth `redirect_to`.** `_validate_redirect`
75
+ was forwarding `urlsplit`'s judgment, but browsers normalize values
76
+ like `/\evil.com` and `////evil.com` into the protocol-relative
77
+ `//evil.com`. Both forms now rejected.
78
+ - **CVE-2025-62727 — `fastapi>=0.120.0`** (was `>=0.110`). Starlette
79
+ DoS via large request bodies after multipart processing.
80
+ - **CVE-2025-27516 — `jinja2>=3.1.6`** (was `>=3.1`). Sandbox breakout
81
+ via the `|attr` filter.
82
+ - **Login lockout coverage extended.** `POST /login` was returning
83
+ HTTP 403 for `is_active=False` and (when `require_verification=True`)
84
+ unverified accounts **without** recording a failure — an attacker
85
+ guessing passwords against either category had unbounded probing.
86
+ The endpoint now:
87
+ - Verifies the password **before** the active/verified checks (so
88
+ an unauthenticated attacker can't distinguish disabled vs active
89
+ accounts by HTTP code).
90
+ - Records a failure before raising 403 in either branch.
91
+ - **`POST /change-email` anti-enumeration.** An authenticated
92
+ attacker could walk the registered-email namespace via the 409 vs
93
+ 202 distinction. The endpoint now always returns 202; clashes are
94
+ logged server-side and the confirmation email is silently skipped.
95
+ Matches the existing stance on `/forgot-password` and
96
+ `/resend-verification`.
97
+ - **Admin resend-verification rejects OAuth-only users** with a clear
98
+ 400 instead of attempting to construct a `PendingRegistration` with
99
+ `hashed_password=None` (which corrupted the pending-registrations
100
+ row).
101
+
6
102
  ## 0.5.9 — 2026-05-13
7
103
 
8
104
  ### Security
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regstack"
3
- version = "0.5.9"
3
+ version = "0.6.0"
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"
@@ -17,13 +17,18 @@ classifiers = [
17
17
  "Topic :: Software Development :: Libraries :: Python Modules",
18
18
  ]
19
19
  dependencies = [
20
- "fastapi>=0.110",
20
+ # fastapi>=0.120.0 picks up CVE-2025-62727 (Starlette DoS via large
21
+ # request bodies after multipart processing).
22
+ "fastapi>=0.120.0",
21
23
  "pydantic>=2.6",
22
24
  "pydantic-settings>=2.2",
23
25
  "pwdlib[argon2]>=0.2.1",
24
26
  # pyjwt>=2.12.1 includes the fix for CVE-2026-32597 (`crit` header bypass).
25
27
  "pyjwt>=2.12.1",
26
- "jinja2>=3.1",
28
+ # jinja2>=3.1.6 picks up CVE-2025-27516 (sandbox breakout via the
29
+ # `|attr` filter; only relevant if hosts allow user-controlled
30
+ # templates, but we tighten the floor anyway).
31
+ "jinja2>=3.1.6",
27
32
  "click>=8.1",
28
33
  "dnspython>=2.6",
29
34
  # python-multipart>=0.0.26 picks up CVE-2026-40347 (DoS via oversized
@@ -35,11 +40,6 @@ dependencies = [
35
40
  "sqlalchemy[asyncio]>=2.0",
36
41
  "aiosqlite>=0.20",
37
42
  "alembic>=1.13",
38
- # OAuth setup wizard (regstack oauth setup) — needs a webview shell,
39
- # a small in-process FastAPI server, and round-trip-safe TOML editing.
40
- "pywebview>=5.0",
41
- "tomlkit>=0.13",
42
- "uvicorn[standard]>=0.29",
43
43
  ]
44
44
 
45
45
  [project.optional-dependencies]
@@ -51,6 +51,16 @@ twilio = ["twilio>=9.0"]
51
51
  # cryptography>=46.0.7 picks up CVE-2026-26007 (ECC subgroup attack on the
52
52
  # JWKS code path) plus CVE-2026-34073 and CVE-2026-39892.
53
53
  oauth = ["pyjwt[crypto]>=2.12.1", "cryptography>=46.0.7"]
54
+ # Interactive setup wizards (`regstack oauth setup`, `regstack theme
55
+ # design`). These open a native pywebview window over a local 127.0.0.1
56
+ # FastAPI server and use tomlkit for non-destructive TOML merging.
57
+ # Optional because library consumers who never run the GUI tools don't
58
+ # need a platform browser engine pulled in by pywebview.
59
+ wizard = [
60
+ "pywebview>=5.0",
61
+ "tomlkit>=0.13",
62
+ "uvicorn[standard]>=0.29",
63
+ ]
54
64
  # Per-route rate limiting on the auth router. The limiter itself can be
55
65
  # host-supplied (so hosts that already use slowapi share the same Limiter
56
66
  # state), otherwise regstack constructs an in-memory one.
@@ -86,6 +96,10 @@ dev = [
86
96
  "pytest-playwright>=0.5",
87
97
  # Rate-limit tests exercise the slowapi integration.
88
98
  "slowapi>=0.1.9",
99
+ # Wizard surface (now an optional `wizard` extra) — tests exercise
100
+ # the setup/theme-designer routes, writers, and Playwright e2e.
101
+ "pywebview>=5.0",
102
+ "tomlkit>=0.13",
89
103
  ]
90
104
 
91
105
  [project.scripts]
@@ -8,13 +8,39 @@ from regstack.cli.init import init as init_cmd
8
8
  from regstack.cli.migrate import migrate as migrate_cmd
9
9
  from regstack.version import __version__
10
10
 
11
+ _WIZARD_EXTRA_HINT = (
12
+ "The {subcommand} command requires the optional 'wizard' extra "
13
+ "(pywebview + tomlkit + uvicorn). Install with "
14
+ "`pip install regstack[wizard]` or `uv sync --extra wizard`."
15
+ )
16
+
17
+
18
+ def _missing_wizard_extra(subcommand: str) -> click.Command:
19
+ """Click command that prints a clear install hint and exits non-zero.
20
+
21
+ Used as the fallback when a wizard subcommand is invoked but the
22
+ ``wizard`` extra isn't installed — gives a one-line actionable
23
+ message instead of an ImportError traceback from deep inside the
24
+ wizard package.
25
+ """
26
+ name = subcommand.split()[-1]
27
+
28
+ @click.command(name=name, help="(needs `regstack[wizard]`)")
29
+ def _stub() -> None:
30
+ click.echo(_WIZARD_EXTRA_HINT.format(subcommand=f"`{subcommand}`"), err=True)
31
+ raise SystemExit(2)
32
+
33
+ return _stub
34
+
11
35
 
12
36
  class _LazyOauthGroup(click.Group):
13
37
  """Defer wizard imports until ``regstack oauth …`` is actually run.
14
38
 
15
- Importing the wizard pulls in pywebview, uvicorn, and tomlkit. We
16
- don't want to pay that on every ``regstack init`` / ``regstack doctor``
17
- invocation. Click's :meth:`Group.get_command` is the seam.
39
+ Importing the wizard pulls in pywebview, uvicorn, and tomlkit (the
40
+ ``wizard`` optional extra). Hosts who don't use the GUI tools
41
+ don't pay for those deps at install time; the cost is that
42
+ running a wizard subcommand without the extra exits with a clear
43
+ install hint instead of an ImportError.
18
44
  """
19
45
 
20
46
  def list_commands(self, ctx: click.Context) -> list[str]:
@@ -23,8 +49,10 @@ class _LazyOauthGroup(click.Group):
23
49
  def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
24
50
  if name != "setup":
25
51
  return None
26
- from regstack.wizard.oauth_google.cli import setup as setup_cmd
27
-
52
+ try:
53
+ from regstack.wizard.oauth_google.cli import setup as setup_cmd
54
+ except ImportError:
55
+ return _missing_wizard_extra("regstack oauth setup")
28
56
  return setup_cmd
29
57
 
30
58
 
@@ -37,8 +65,10 @@ class _LazyThemeGroup(click.Group):
37
65
  def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
38
66
  if name != "design":
39
67
  return None
40
- from regstack.wizard.theme_designer.cli import design as design_cmd
41
-
68
+ try:
69
+ from regstack.wizard.theme_designer.cli import design as design_cmd
70
+ except ImportError:
71
+ return _missing_wizard_extra("regstack theme design")
42
72
  return design_cmd
43
73
 
44
74
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from typing import TYPE_CHECKING, Annotated, Any
4
5
 
5
6
  import jwt as pyjwt
@@ -20,6 +21,8 @@ if TYPE_CHECKING:
20
21
  _EMAIL_CHANGE_PURPOSE = "email_change"
21
22
  _NEW_EMAIL_CLAIM = "new_email"
22
23
 
24
+ log = logging.getLogger("regstack.account")
25
+
23
26
 
24
27
  class ChangePasswordRequest(BaseModel):
25
28
  model_config = ConfigDict(extra="forbid")
@@ -124,12 +127,24 @@ def build_account_router(rs: RegStack) -> APIRouter:
124
127
  status_code=status.HTTP_400_BAD_REQUEST,
125
128
  detail="Current password is incorrect.",
126
129
  )
130
+
131
+ # Anti-enumeration: an authenticated attacker could otherwise
132
+ # walk the registered-email namespace by alternating 409 vs 202
133
+ # responses on this endpoint. Always return 202; if the address
134
+ # is already taken, log and skip the email send. The legitimate
135
+ # user finds out by not receiving the confirmation mail; the
136
+ # final uniqueness check still happens at /confirm-email-change
137
+ # via the users.update_email unique constraint.
127
138
  clash = await rs.users.get_by_email(payload.new_email)
139
+ accepted = MessageResponse(
140
+ message="If that email address is not already in use, a confirmation link has been sent.",
141
+ )
128
142
  if clash is not None:
129
- raise HTTPException(
130
- status_code=status.HTTP_409_CONFLICT,
131
- detail="That email address is already in use.",
143
+ log.info(
144
+ "change-email blocked by clash (anti-enumeration): user_id=%s",
145
+ user.id,
132
146
  )
147
+ return accepted
133
148
 
134
149
  assert user.id is not None
135
150
  ttl = rs.config.email_change_token_ttl_seconds
@@ -148,9 +163,7 @@ def build_account_router(rs: RegStack) -> APIRouter:
148
163
  new_email=payload.new_email,
149
164
  url=url,
150
165
  )
151
- return MessageResponse(
152
- message="A confirmation link has been sent to the new email address."
153
- )
166
+ return accepted
154
167
 
155
168
  @router.post(
156
169
  "/confirm-email-change",
@@ -154,6 +154,21 @@ def build_admin_router(rs: RegStack) -> APIRouter:
154
154
  status_code=status.HTTP_400_BAD_REQUEST,
155
155
  detail="User is already verified.",
156
156
  )
157
+ # OAuth-only users (no password) shouldn't be unverified — their
158
+ # OAuth identity was the verification. If somehow one ended up
159
+ # unverified, the password-bearing verification flow can't help:
160
+ # PendingRegistration requires a `hashed_password: str` and
161
+ # would store the literal string "None" (or fail validation,
162
+ # depending on the pydantic config). Surface this clearly
163
+ # instead of silently corrupting the pending-registrations row.
164
+ if user.hashed_password is None:
165
+ raise HTTPException(
166
+ status_code=status.HTTP_400_BAD_REQUEST,
167
+ detail=(
168
+ "User has no password (OAuth-only). Verify them directly "
169
+ "via the user record instead of resending verification email."
170
+ ),
171
+ )
157
172
 
158
173
  # Move the user to a pending registration row so the standard verify
159
174
  # endpoint completes the flow. Less special-case code, one path.
@@ -70,11 +70,11 @@ def build_login_router(rs: RegStack) -> APIRouter:
70
70
  if user is None or user.id is None:
71
71
  await rs.lockout.record_failure(payload.email)
72
72
  raise _INVALID
73
- if not user.is_active:
74
- raise HTTPException(
75
- status_code=status.HTTP_403_FORBIDDEN,
76
- detail="Account is disabled.",
77
- )
73
+ # Password verification runs first so the is_active and
74
+ # is_verified branches below are only reachable by an attacker
75
+ # who already knows the password. Without this ordering, an
76
+ # unauthenticated probe could distinguish disabled / unverified
77
+ # / non-existent accounts by HTTP code alone.
78
78
  # An OAuth-only user (hashed_password=None) returns the same
79
79
  # generic 401 as a wrong-password attempt — never reveal that
80
80
  # the account exists but has no password set, so an attacker
@@ -84,7 +84,19 @@ def build_login_router(rs: RegStack) -> APIRouter:
84
84
  ):
85
85
  await rs.lockout.record_failure(payload.email)
86
86
  raise _INVALID
87
+ # Even with the right password, disabled / unverified accounts
88
+ # must still increment the lockout counter — otherwise a
89
+ # password-stuffing attacker who happens to be holding the
90
+ # correct credentials for a disabled account gets unbounded
91
+ # probing.
92
+ if not user.is_active:
93
+ await rs.lockout.record_failure(payload.email)
94
+ raise HTTPException(
95
+ status_code=status.HTTP_403_FORBIDDEN,
96
+ detail="Account is disabled.",
97
+ )
87
98
  if rs.config.require_verification and not user.is_verified:
99
+ await rs.lockout.record_failure(payload.email)
88
100
  raise HTTPException(
89
101
  status_code=status.HTTP_403_FORBIDDEN,
90
102
  detail="Email address has not been verified.",