regstack 0.4.0__tar.gz → 0.5.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. {regstack-0.4.0 → regstack-0.5.6}/.github/workflows/publish.yml +6 -1
  2. {regstack-0.4.0 → regstack-0.5.6}/CHANGELOG.md +81 -0
  3. {regstack-0.4.0 → regstack-0.5.6}/CLAUDE.md +14 -0
  4. {regstack-0.4.0 → regstack-0.5.6}/PKG-INFO +14 -9
  5. {regstack-0.4.0 → regstack-0.5.6}/README.md +4 -4
  6. {regstack-0.4.0 → regstack-0.5.6}/docs/architecture.md +7 -3
  7. {regstack-0.4.0 → regstack-0.5.6}/docs/changelog.md +148 -0
  8. regstack-0.5.6/docs/cli.md +187 -0
  9. {regstack-0.4.0 → regstack-0.5.6}/docs/configuration.md +45 -8
  10. {regstack-0.4.0 → regstack-0.5.6}/docs/embedding.md +5 -5
  11. {regstack-0.4.0 → regstack-0.5.6}/docs/index.md +53 -2
  12. {regstack-0.4.0 → regstack-0.5.6}/docs/quickstart.md +9 -4
  13. {regstack-0.4.0 → regstack-0.5.6}/docs/security.md +50 -3
  14. {regstack-0.4.0 → regstack-0.5.6}/docs/theming.md +46 -1
  15. {regstack-0.4.0 → regstack-0.5.6}/pyproject.toml +19 -5
  16. regstack-0.5.6/scripts/ccr_coverage_setup.py +242 -0
  17. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/app.py +55 -2
  18. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/dependencies.py +12 -4
  19. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/password.py +2 -20
  20. regstack-0.5.6/src/regstack/auth/rate_limit.py +141 -0
  21. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/backend.py +5 -6
  22. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/client.py +9 -3
  23. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/indexes.py +2 -1
  24. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/blacklist_repo.py +14 -2
  25. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +3 -1
  26. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +2 -1
  27. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +3 -1
  28. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +3 -1
  29. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/pending_repo.py +3 -1
  30. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/user_repo.py +3 -1
  31. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/__init__.py +3 -1
  32. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/env.py +5 -1
  33. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -2
  34. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +4 -6
  35. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/oauth_state_repo.py +3 -3
  36. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/pending_repo.py +4 -6
  37. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/user_repo.py +4 -4
  38. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/types.py +4 -2
  39. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/__main__.py +15 -0
  40. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/doctor.py +9 -5
  41. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/config/schema.py +18 -1
  42. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/composer.py +9 -2
  43. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/ses.py +1 -1
  44. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/hooks/events.py +7 -0
  45. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/_objectid.py +1 -1
  46. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/user.py +1 -1
  47. regstack-0.5.6/src/regstack/routers/_helpers.py +28 -0
  48. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/account.py +5 -23
  49. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/admin.py +4 -0
  50. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/login.py +12 -5
  51. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/logout.py +3 -1
  52. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/oauth.py +6 -1
  53. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/phone.py +12 -3
  54. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/sns.py +1 -1
  55. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/twilio.py +1 -1
  56. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/pages.py +9 -2
  57. regstack-0.5.6/src/regstack/version.py +1 -0
  58. regstack-0.5.6/src/regstack/wizard/theme_designer/__init__.py +8 -0
  59. regstack-0.5.6/src/regstack/wizard/theme_designer/cli.py +181 -0
  60. regstack-0.5.6/src/regstack/wizard/theme_designer/routes.py +278 -0
  61. regstack-0.5.6/src/regstack/wizard/theme_designer/server.py +87 -0
  62. regstack-0.5.6/src/regstack/wizard/theme_designer/static/designer.css +348 -0
  63. regstack-0.5.6/src/regstack/wizard/theme_designer/static/designer.js +272 -0
  64. regstack-0.5.6/src/regstack/wizard/theme_designer/templates/designer.html +89 -0
  65. regstack-0.5.6/src/regstack/wizard/theme_designer/validators.py +145 -0
  66. regstack-0.5.6/src/regstack/wizard/theme_designer/window.py +63 -0
  67. regstack-0.5.6/src/regstack/wizard/theme_designer/writer.py +194 -0
  68. {regstack-0.4.0 → regstack-0.5.6}/tasks.py +16 -1
  69. regstack-0.5.6/tests/e2e/conftest.py +121 -0
  70. regstack-0.5.6/tests/e2e/test_theme_designer.py +87 -0
  71. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_admin_router.py +39 -0
  72. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_happy_path.py +40 -0
  73. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_mfa.py +110 -0
  74. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_oauth_google_router.py +33 -0
  75. regstack-0.5.6/tests/integration/test_rate_limits.py +215 -0
  76. regstack-0.5.6/tests/unit/test_cli_migrate.py +151 -0
  77. regstack-0.5.6/tests/unit/test_theme_designer_cli.py +190 -0
  78. regstack-0.5.6/tests/unit/test_theme_designer_routes.py +179 -0
  79. regstack-0.5.6/tests/unit/test_theme_designer_validators.py +154 -0
  80. regstack-0.5.6/tests/unit/test_theme_designer_writer.py +156 -0
  81. regstack-0.5.6/tests/unit/test_wizard_oauth_cli.py +230 -0
  82. {regstack-0.4.0 → regstack-0.5.6}/uv.lock +54 -6
  83. regstack-0.4.0/docs/cli.md +0 -87
  84. regstack-0.4.0/src/regstack/version.py +0 -1
  85. regstack-0.4.0/tests/e2e/conftest.py +0 -82
  86. regstack-0.4.0/tests/unit/test_wizard_oauth_cli.py +0 -93
  87. {regstack-0.4.0 → regstack-0.5.6}/.github/workflows/test.yml +0 -0
  88. {regstack-0.4.0 → regstack-0.5.6}/.gitignore +0 -0
  89. {regstack-0.4.0 → regstack-0.5.6}/.python-version +0 -0
  90. {regstack-0.4.0 → regstack-0.5.6}/.readthedocs.yaml +0 -0
  91. {regstack-0.4.0 → regstack-0.5.6}/LICENSE +0 -0
  92. {regstack-0.4.0 → regstack-0.5.6}/NOTICE +0 -0
  93. {regstack-0.4.0 → regstack-0.5.6}/SECURITY.md +0 -0
  94. {regstack-0.4.0 → regstack-0.5.6}/docs/_static/.gitkeep +0 -0
  95. {regstack-0.4.0 → regstack-0.5.6}/docs/_templates/.gitkeep +0 -0
  96. {regstack-0.4.0 → regstack-0.5.6}/docs/api.md +0 -0
  97. {regstack-0.4.0 → regstack-0.5.6}/docs/conf.py +0 -0
  98. {regstack-0.4.0 → regstack-0.5.6}/docs/oauth.md +0 -0
  99. {regstack-0.4.0 → regstack-0.5.6}/docs/security-reports/README.md +0 -0
  100. {regstack-0.4.0 → regstack-0.5.6}/examples/_common/__init__.py +0 -0
  101. {regstack-0.4.0 → regstack-0.5.6}/examples/_common/app.py +0 -0
  102. {regstack-0.4.0 → regstack-0.5.6}/examples/mongo/README.md +0 -0
  103. {regstack-0.4.0 → regstack-0.5.6}/examples/mongo/branding/theme.css +0 -0
  104. {regstack-0.4.0 → regstack-0.5.6}/examples/mongo/main.py +0 -0
  105. {regstack-0.4.0 → regstack-0.5.6}/examples/mongo/regstack.toml +0 -0
  106. {regstack-0.4.0 → regstack-0.5.6}/examples/postgres/README.md +0 -0
  107. {regstack-0.4.0 → regstack-0.5.6}/examples/postgres/main.py +0 -0
  108. {regstack-0.4.0 → regstack-0.5.6}/examples/postgres/regstack.toml +0 -0
  109. {regstack-0.4.0 → regstack-0.5.6}/examples/sqlite/README.md +0 -0
  110. {regstack-0.4.0 → regstack-0.5.6}/examples/sqlite/main.py +0 -0
  111. {regstack-0.4.0 → regstack-0.5.6}/examples/sqlite/regstack.toml +0 -0
  112. {regstack-0.4.0 → regstack-0.5.6}/regstack.toml.example +0 -0
  113. {regstack-0.4.0 → regstack-0.5.6}/scripts/security-review-prompt.md +0 -0
  114. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/__init__.py +0 -0
  115. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/__init__.py +0 -0
  116. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/clock.py +0 -0
  117. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/jwt.py +0 -0
  118. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/lockout.py +0 -0
  119. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/mfa.py +0 -0
  120. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/tokens.py +0 -0
  121. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/__init__.py +0 -0
  122. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/base.py +0 -0
  123. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/factory.py +0 -0
  124. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/__init__.py +0 -0
  125. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
  126. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/protocols.py +0 -0
  127. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/__init__.py +0 -0
  128. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/backend.py +0 -0
  129. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  130. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  131. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
  132. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/__init__.py +0 -0
  133. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  134. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  135. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/schema.py +0 -0
  136. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/__init__.py +0 -0
  137. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/_runtime.py +0 -0
  138. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/admin.py +0 -0
  139. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/init.py +0 -0
  140. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/migrate.py +0 -0
  141. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/config/__init__.py +0 -0
  142. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/config/loader.py +0 -0
  143. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/config/secrets.py +0 -0
  144. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/__init__.py +0 -0
  145. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/base.py +0 -0
  146. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/console.py +0 -0
  147. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/factory.py +0 -0
  148. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/smtp.py +0 -0
  149. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.html +0 -0
  150. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.subject.txt +0 -0
  151. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.txt +0 -0
  152. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.html +0 -0
  153. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  154. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.txt +0 -0
  155. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  156. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  157. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/verification.html +0 -0
  158. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/verification.subject.txt +0 -0
  159. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/verification.txt +0 -0
  160. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/hooks/__init__.py +0 -0
  161. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/__init__.py +0 -0
  162. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/login_attempt.py +0 -0
  163. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/mfa_code.py +0 -0
  164. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/oauth_identity.py +0 -0
  165. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/oauth_state.py +0 -0
  166. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/pending_registration.py +0 -0
  167. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/__init__.py +0 -0
  168. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/base.py +0 -0
  169. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/errors.py +0 -0
  170. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/providers/__init__.py +0 -0
  171. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/providers/google.py +0 -0
  172. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/registry.py +0 -0
  173. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/__init__.py +0 -0
  174. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/_schemas.py +0 -0
  175. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/password.py +0 -0
  176. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/register.py +0 -0
  177. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/verify.py +0 -0
  178. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/__init__.py +0 -0
  179. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/base.py +0 -0
  180. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/factory.py +0 -0
  181. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/null.py +0 -0
  182. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/__init__.py +0 -0
  183. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/static/css/core.css +0 -0
  184. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/static/css/theme.css +0 -0
  185. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/static/js/regstack.js +0 -0
  186. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  187. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/forgot.html +0 -0
  188. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/login.html +0 -0
  189. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/me.html +0 -0
  190. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  191. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
  192. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/register.html +0 -0
  193. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/reset.html +0 -0
  194. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/verify.html +0 -0
  195. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/base.html +0 -0
  196. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/__init__.py +0 -0
  197. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/__init__.py +0 -0
  198. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/cli.py +0 -0
  199. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/routes.py +0 -0
  200. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/server.py +0 -0
  201. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
  202. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
  203. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
  204. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/validators.py +0 -0
  205. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/window.py +0 -0
  206. {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/writer.py +0 -0
  207. {regstack-0.4.0 → regstack-0.5.6}/tasks/oauth-design.md +0 -0
  208. {regstack-0.4.0 → regstack-0.5.6}/tests/__init__.py +0 -0
  209. {regstack-0.4.0 → regstack-0.5.6}/tests/_fake_google/__init__.py +0 -0
  210. {regstack-0.4.0 → regstack-0.5.6}/tests/_fake_google/provider.py +0 -0
  211. {regstack-0.4.0 → regstack-0.5.6}/tests/conftest.py +0 -0
  212. {regstack-0.4.0 → regstack-0.5.6}/tests/e2e/__init__.py +0 -0
  213. {regstack-0.4.0 → regstack-0.5.6}/tests/e2e/test_wizard_oauth_flow.py +0 -0
  214. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/__init__.py +0 -0
  215. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_account_management.py +0 -0
  216. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_indexes.py +0 -0
  217. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_login_lockout.py +0 -0
  218. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_oauth_repos.py +0 -0
  219. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_oauth_ui.py +0 -0
  220. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_password_reset.py +0 -0
  221. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_sql_migrations.py +0 -0
  222. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_ui_router.py +0 -0
  223. {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_verification.py +0 -0
  224. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/__init__.py +0 -0
  225. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_base_install_imports.py +0 -0
  226. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_cli.py +0 -0
  227. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_cli_doctor.py +0 -0
  228. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_cli_init.py +0 -0
  229. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_config_loader.py +0 -0
  230. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_jwt.py +0 -0
  231. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_lockout.py +0 -0
  232. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_mail_composer.py +0 -0
  233. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_mfa_code_repo.py +0 -0
  234. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_oauth_google.py +0 -0
  235. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_password.py +0 -0
  236. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_ses_backend.py +0 -0
  237. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_sms.py +0 -0
  238. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_smtp_backend.py +0 -0
  239. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_ui_env.py +0 -0
  240. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_routes.py +0 -0
  241. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_validators.py +0 -0
  242. {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_writer.py +0 -0
@@ -61,4 +61,9 @@ jobs:
61
61
  with:
62
62
  name: dist
63
63
  path: dist/
64
- - uses: pypa/gh-action-pypi-publish@release/v1
64
+ # Pinned to a commit SHA, not the mutable `release/v1` branch — the
65
+ # publish job has `id-token: write`, so a tag/branch swap in the
66
+ # action repo would let an attacker push a malicious wheel under our
67
+ # OIDC identity. Update by resolving the latest SHA on `release/v1`
68
+ # and bumping the trailing comment to match.
69
+ - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
@@ -5,6 +5,87 @@ authoritative copy lives at
5
5
  [`docs/changelog.md`](docs/changelog.md) and is rendered into the
6
6
  Sphinx docs.
7
7
 
8
+ ## 0.5.6 — 2026-05-13
9
+
10
+ Eleven days of security-review remediation, supply-chain hardening,
11
+ a full `mypy --strict` cleanup pass, and the per-route rate-limits
12
+ feature rolled up into a single release.
13
+
14
+ **Per-route IP rate limits.** Opt-in via the new `rate_limit` extra
15
+ (or a host-supplied `slowapi.Limiter`) plus any of the new
16
+ `RegStackConfig.*_rate_limit` fields (`login_rate_limit`,
17
+ `register_rate_limit`, `forgot_password_rate_limit`,
18
+ `reset_password_rate_limit`, `verify_rate_limit`,
19
+ `resend_verification_rate_limit`, `change_password_rate_limit`,
20
+ `change_email_rate_limit`, `confirm_email_change_rate_limit`,
21
+ `delete_account_rate_limit`). Each accepts a slowapi-syntax string
22
+ (`"5/minute"`, `"5/minute;20/hour"`). Empty / unset means no limit
23
+ on that route — `LockoutService` still defends `/login` against
24
+ credential stuffing per-account. When `*_rate_limit` strings are
25
+ configured but neither a `rate_limiter=` argument is passed nor
26
+ the `rate_limit` extra is installed, `RegStack.router` raises
27
+ `RuntimeError` on first access — failing closed beats silently
28
+ disabling the protection. Hosts remain responsible for
29
+ `app.state.limiter` and `app.add_exception_handler(RateLimitExceeded, ...)`;
30
+ slowapi owns the 429 response shape. The previously-reserved
31
+ `login_max_per_minute` / `login_max_per_hour` fields are kept for
32
+ back-compat but unwired.
33
+
34
+ **Security fixes.**
35
+
36
+ - JWT 401 detail now returns a static `"Invalid or expired token."`;
37
+ no longer leaks the pyjwt failure reason (signature mismatch /
38
+ expired / malformed / audience mismatch).
39
+ - OAuth sign-in now honours `allow_registration=False`. Previously,
40
+ `/register` respected the flag but the OAuth `_resolve_user`
41
+ "brand-new account" branch did not, creating accounts even when
42
+ self-service signup was disabled.
43
+ - Admin `DELETE /admin/users/{id}` now cascades `oauth_identities`,
44
+ matching the user-initiated `DELETE /account` path. Previously
45
+ left orphan rows that blocked re-registration of the same Google
46
+ subject.
47
+ - `POST /phone/start` and `DELETE /phone` now return 400 (not crash
48
+ with HTTP 500) for OAuth-only users who have no `hashed_password`.
49
+
50
+ **Breaking change — hook contracts.** `mfa_login_started` and
51
+ `phone_setup_started` no longer include the raw OTP code in their
52
+ kwargs. Hooks are best-effort observability and are the documented
53
+ integration surface for analytics / logging / Slack notifications,
54
+ so a plaintext OTP in `**kwargs` is a leak waiting to happen.
55
+ Hosts that subscribed to either event to take over SMS delivery
56
+ should migrate to a custom `SmsService` subclass — the supported
57
+ delivery override.
58
+
59
+ **Dependency floors raised for CVEs.**
60
+
61
+ - `pyjwt>=2.12.1` for CVE-2026-32597 (`crit` header bypass, CVSS 7.5).
62
+ - `cryptography>=46.0.7` added explicitly to the `oauth` extra for
63
+ CVE-2026-26007 (ECC subgroup attack on the JWKS code path, CVSS
64
+ 8.2) plus CVE-2026-34073 and CVE-2026-39892.
65
+ - `python-multipart>=0.0.26` for CVE-2026-40347 (DoS via oversized
66
+ multipart preamble).
67
+
68
+ **Supply chain.** `pypa/gh-action-pypi-publish` in `publish.yml`
69
+ pinned to a commit SHA instead of the mutable `release/v1` branch.
70
+ The publish job holds `id-token: write`, so a tag/branch swap
71
+ upstream would let an attacker push a malicious wheel under our
72
+ OIDC identity.
73
+
74
+ **Removed.** `PasswordHasher.needs_rehash` — called pwdlib's
75
+ non-existent `check_needs_rehash` and would `AttributeError` if
76
+ anyone invoked it. No callers in src or tests. If you were planning
77
+ to use it, call `pwdlib.PasswordHash.verify_and_update` directly.
78
+
79
+ **Internal.** 72 `mypy --strict` errors cleared across 35 files;
80
+ `inv lint` is now green end-to-end. Mongo
81
+ `BlacklistRepo.purge_expired` added (protocol parity with SQL).
82
+ `KNOWN_EVENTS` reconciled — 7 previously-undeclared events added
83
+ (`verification_requested`, `email_change_requested`, `email_changed`,
84
+ `phone_setup_started`, `mfa_login_started`, `mfa_enabled`,
85
+ `mfa_disabled`). `user_logged_out` now actually fires from
86
+ `routers/logout.py` (was listed in `KNOWN_EVENTS` but no router
87
+ emitted it).
88
+
8
89
  ## 0.3.0 — 2026-04-30
9
90
 
10
91
  **OAuth — Sign in with Google.** Opt-in via the new `oauth` extra
@@ -77,6 +77,20 @@ The full plan, including milestone scope and deferred items, lives at
77
77
  four layers: validators (unit), writer (golden-file), routes
78
78
  (TestClient), full SPA flow (Playwright e2e). Run e2e with
79
79
  `inv test-e2e`; `inv test-all` chains it after the backend matrix.
80
+ - **Theme designer — done.** `regstack theme design` opens a sibling
81
+ pywebview window with controls for every `--rs-*` variable and a
82
+ real-time preview of the bundled SSR widgets (renders the same
83
+ `.rs-*` classes the SSR pages render, so what you see is what
84
+ you'll ship). Save writes `regstack-theme.css`; the designer
85
+ round-trips values back into the form on next launch so iteration
86
+ is non-destructive. Lives in `src/regstack/wizard/theme_designer/`,
87
+ registered via a `_LazyThemeGroup` mirroring the OAuth pattern.
88
+ Same four-layer test stack (validators / writer / routes /
89
+ Playwright e2e). The shared scaffold (FastAPI app, uvicorn launcher,
90
+ pywebview shim, e2e fixture pattern) is duplicated rather than
91
+ abstracted into a base class — the two tools have meaningfully
92
+ different inputs and an early shared base would calcify the wrong
93
+ decisions. Refactor only when a third pywebview tool lands.
80
94
 
81
95
  ## Three kinds of single-use proof
82
96
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regstack
3
- Version: 0.4.0
3
+ Version: 0.5.6
4
4
  Summary: Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB.
5
5
  Project-URL: Homepage, https://github.com/jdrumgoole/regstack
6
6
  Project-URL: Repository, https://github.com/jdrumgoole/regstack
@@ -27,8 +27,8 @@ Requires-Dist: jinja2>=3.1
27
27
  Requires-Dist: pwdlib[argon2]>=0.2.1
28
28
  Requires-Dist: pydantic-settings>=2.2
29
29
  Requires-Dist: pydantic>=2.6
30
- Requires-Dist: pyjwt>=2.8
31
- Requires-Dist: python-multipart>=0.0.9
30
+ Requires-Dist: pyjwt>=2.12.1
31
+ Requires-Dist: python-multipart>=0.0.26
32
32
  Requires-Dist: pywebview>=5.0
33
33
  Requires-Dist: sqlalchemy[asyncio]>=2.0
34
34
  Requires-Dist: tomlkit>=0.13
@@ -36,10 +36,11 @@ Requires-Dist: uvicorn[standard]>=0.29
36
36
  Provides-Extra: dev
37
37
  Requires-Dist: anyio>=4.3; extra == 'dev'
38
38
  Requires-Dist: asyncpg>=0.29; extra == 'dev'
39
+ Requires-Dist: cryptography>=46.0.7; extra == 'dev'
39
40
  Requires-Dist: httpx>=0.27; extra == 'dev'
40
41
  Requires-Dist: invoke>=2.2; extra == 'dev'
41
42
  Requires-Dist: mypy>=1.10; extra == 'dev'
42
- Requires-Dist: pyjwt[crypto]>=2.8; extra == 'dev'
43
+ Requires-Dist: pyjwt[crypto]>=2.12.1; extra == 'dev'
43
44
  Requires-Dist: pymongo>=4.9; extra == 'dev'
44
45
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
45
46
  Requires-Dist: pytest-cov>=5.0; extra == 'dev'
@@ -47,6 +48,7 @@ Requires-Dist: pytest-playwright>=0.5; extra == 'dev'
47
48
  Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
48
49
  Requires-Dist: pytest>=8.0; extra == 'dev'
49
50
  Requires-Dist: ruff>=0.4; extra == 'dev'
51
+ Requires-Dist: slowapi>=0.1.9; extra == 'dev'
50
52
  Requires-Dist: uvicorn[standard]>=0.29; extra == 'dev'
51
53
  Provides-Extra: docs
52
54
  Requires-Dist: furo>=2024.1; extra == 'docs'
@@ -58,9 +60,12 @@ Requires-Dist: sphinx>=7.3; extra == 'docs'
58
60
  Provides-Extra: mongo
59
61
  Requires-Dist: pymongo>=4.9; extra == 'mongo'
60
62
  Provides-Extra: oauth
61
- Requires-Dist: pyjwt[crypto]>=2.8; extra == 'oauth'
63
+ Requires-Dist: cryptography>=46.0.7; extra == 'oauth'
64
+ Requires-Dist: pyjwt[crypto]>=2.12.1; extra == 'oauth'
62
65
  Provides-Extra: postgres
63
66
  Requires-Dist: asyncpg>=0.29; extra == 'postgres'
67
+ Provides-Extra: rate-limit
68
+ Requires-Dist: slowapi>=0.1.9; extra == 'rate-limit'
64
69
  Provides-Extra: ses
65
70
  Requires-Dist: aioboto3>=12.3; extra == 'ses'
66
71
  Provides-Extra: sns
@@ -143,7 +148,7 @@ result everywhere is what regstack is for.
143
148
  ✔ Server-rendered HTML pages, theme with one CSS file
144
149
  ✔ Pluggable email (console / SMTP / Amazon SES) and SMS (Amazon SNS / Twilio)
145
150
  ✔ Argon2 password hashing, CSP-friendly templates
146
- ✔ Setup wizard (`regstack init`) and config validator (`regstack doctor`)
151
+ ✔ Setup wizards (live in their own pywebview windows): `regstack init` (project bootstrap), `regstack oauth setup` (guided Google OAuth client config), `regstack theme design` (live theme designer with preview), and `regstack doctor` (config validator)
147
152
  ✔ Three storage backends: SQLite, PostgreSQL, MongoDB — chosen by URL
148
153
  ```
149
154
 
@@ -186,7 +191,7 @@ register from the command line:
186
191
  ```bash
187
192
  curl -X POST http://localhost:8000/api/auth/register \
188
193
  -H 'content-type: application/json' \
189
- -d '{"email":"alice@example.com","password":"hunter2hunter2","full_name":"Alice"}'
194
+ -d '{"email":"alice@app.example.com","password":"<password>","full_name":"Alice"}'
190
195
  ```
191
196
 
192
197
  The bundled example serves themed SSR pages at `/account/*`, prints
@@ -251,8 +256,8 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
251
256
  Alpha. Single-file SQLite is the default and runs with no infrastructure;
252
257
  PostgreSQL and MongoDB backends pass the same parametrized integration
253
258
  suite. OAuth (Google) shipped in `v0.3.0`; the `regstack oauth setup`
254
- guided wizard shipped in `v0.4.0`. Latest tagged release: `v0.4.0`.
255
- See the
259
+ guided wizard in `v0.4.0`; the live `regstack theme design` tool in
260
+ `v0.5.0`. Latest tagged release: `v0.5.0`. See the
256
261
  [changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
257
262
  for the per-release breakdown.
258
263
 
@@ -72,7 +72,7 @@ result everywhere is what regstack is for.
72
72
  ✔ Server-rendered HTML pages, theme with one CSS file
73
73
  ✔ Pluggable email (console / SMTP / Amazon SES) and SMS (Amazon SNS / Twilio)
74
74
  ✔ Argon2 password hashing, CSP-friendly templates
75
- ✔ Setup wizard (`regstack init`) and config validator (`regstack doctor`)
75
+ ✔ Setup wizards (live in their own pywebview windows): `regstack init` (project bootstrap), `regstack oauth setup` (guided Google OAuth client config), `regstack theme design` (live theme designer with preview), and `regstack doctor` (config validator)
76
76
  ✔ Three storage backends: SQLite, PostgreSQL, MongoDB — chosen by URL
77
77
  ```
78
78
 
@@ -115,7 +115,7 @@ register from the command line:
115
115
  ```bash
116
116
  curl -X POST http://localhost:8000/api/auth/register \
117
117
  -H 'content-type: application/json' \
118
- -d '{"email":"alice@example.com","password":"hunter2hunter2","full_name":"Alice"}'
118
+ -d '{"email":"alice@app.example.com","password":"<password>","full_name":"Alice"}'
119
119
  ```
120
120
 
121
121
  The bundled example serves themed SSR pages at `/account/*`, prints
@@ -180,8 +180,8 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
180
180
  Alpha. Single-file SQLite is the default and runs with no infrastructure;
181
181
  PostgreSQL and MongoDB backends pass the same parametrized integration
182
182
  suite. OAuth (Google) shipped in `v0.3.0`; the `regstack oauth setup`
183
- guided wizard shipped in `v0.4.0`. Latest tagged release: `v0.4.0`.
184
- See the
183
+ guided wizard in `v0.4.0`; the live `regstack theme design` tool in
184
+ `v0.5.0`. Latest tagged release: `v0.5.0`. See the
185
185
  [changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
186
186
  for the per-release breakdown.
187
187
 
@@ -54,9 +54,13 @@ multi-tenant deployments where a single FastAPI app serves multiple
54
54
  The backend is auto-built from `config.database_url` if not supplied
55
55
  explicitly. URL scheme decides:
56
56
 
57
- - `sqlite+aiosqlite://` → SQLAlchemy backend in SQLite mode.
58
- - `postgresql+asyncpg://` SQLAlchemy backend in Postgres mode.
59
- - `mongodb://` / `mongodb+srv://` Mongo backend.
57
+ - `sqlite+aiosqlite:///PATH` → SQLAlchemy backend in SQLite mode.
58
+ `PATH` is `./dbname.db` for a relative file, `/var/lib/app/dbname.db`
59
+ for an absolute file, or `:memory:` for an ephemeral in-process DB.
60
+ - `postgresql+asyncpg://<username>:<password>@dbhost.example.com:5432/dbname`
61
+ → SQLAlchemy backend in Postgres mode.
62
+ - `mongodb://<username>:<password>@dbhost.example.com:27017/dbname`
63
+ (or `mongodb+srv://…`) → Mongo backend.
60
64
 
61
65
  The façade exposes:
62
66
 
@@ -3,6 +3,154 @@
3
3
  All notable changes to this project are documented here. Versions follow
4
4
  [Semantic Versioning](https://semver.org/) once `1.0.0` ships.
5
5
 
6
+ ## 0.5.6 — 2026-05-13
7
+
8
+ A rollup release that consolidates 11 days of security-review
9
+ remediation, supply-chain hardening, a full `mypy --strict` pass,
10
+ and the per-route rate-limits feature.
11
+
12
+ ### Added
13
+
14
+ - **Per-route IP rate limits.** Opt-in via the new `rate_limit` extra
15
+ (or a host-supplied `slowapi.Limiter`) plus any of the new
16
+ `RegStackConfig.*_rate_limit` fields (`login_rate_limit`,
17
+ `register_rate_limit`, `forgot_password_rate_limit`,
18
+ `reset_password_rate_limit`, `verify_rate_limit`,
19
+ `resend_verification_rate_limit`, `change_password_rate_limit`,
20
+ `change_email_rate_limit`, `confirm_email_change_rate_limit`,
21
+ `delete_account_rate_limit`). Each accepts a slowapi-syntax string
22
+ (`"5/minute"`, `"5/minute;20/hour"`).
23
+ - New constructor argument `RegStack(rate_limiter=...)`. When at
24
+ least one `*_rate_limit` field is set, regstack expects either
25
+ this argument or the `rate_limit` extra; failure to provide one
26
+ raises `RuntimeError` on first access to `regstack.router` —
27
+ failing closed beats silently disabling the protection. Hosts
28
+ remain responsible for `app.state.limiter` and the
29
+ `RateLimitExceeded` exception handler; slowapi owns the 429
30
+ response shape.
31
+ - **`user_logged_out` hook now fires.** The event was listed in
32
+ `KNOWN_EVENTS` since M1 but no router ever emitted it.
33
+ `routers/logout.py` now fires `user_logged_out` (with a `user=`
34
+ kwarg) immediately after the bearer token is revoked.
35
+
36
+ ### Changed (security)
37
+
38
+ - **JWT 401 responses no longer leak the pyjwt error reason.**
39
+ Replaced `f"Invalid token: {exc}"` with the static `"Invalid or
40
+ expired token."`. The pyjwt error text disclosed *why* a token was
41
+ rejected (signature mismatch, expired, malformed, audience
42
+ mismatch) — useful signal for an attacker probing the auth
43
+ surface.
44
+ - **OAuth sign-in honours `allow_registration=False`.** `/register`
45
+ already did; the OAuth `_resolve_user` "brand-new account" branch
46
+ did not, so an operator who disabled self-service signup still got
47
+ accounts created via "Sign in with Google". The OAuth callback now
48
+ redirects with `?error=registration_disabled` if no existing
49
+ account matches and registration is disabled.
50
+ - **Admin `DELETE /admin/users/{id}` now cascades
51
+ `oauth_identities`.** Matches the user-initiated `DELETE
52
+ /account` flow; previously left orphan rows that blocked the
53
+ Google subject from re-registering.
54
+ - **`POST /phone/start` and `DELETE /phone` guard against OAuth-only
55
+ users.** Both endpoints previously crashed with HTTP 500 for
56
+ users with `hashed_password=None`. Both now return 400 with a
57
+ message pointing to forgot-password (which doubles as a "set
58
+ initial password" path).
59
+
60
+ ### Changed (BREAKING — hook contracts)
61
+
62
+ - **`mfa_login_started` and `phone_setup_started` no longer include
63
+ the raw OTP code in their kwargs.** Hooks are best-effort
64
+ observability and are the documented integration surface for
65
+ analytics / logging / Slack notifications, so a plaintext OTP in
66
+ `**kwargs` is a leak waiting to happen — a host adding
67
+ `logger.info(kw)` to a hook handler is enough to put OTPs in a
68
+ log stream. Hosts that subscribed to either event to take over
69
+ SMS delivery should migrate to a custom `SmsService` subclass
70
+ (the supported delivery override). The other kwargs (`user`,
71
+ `phone`) remain.
72
+
73
+ ### Changed (deps)
74
+
75
+ - `pyjwt>=2.12.1` (was `>=2.8`). Picks up CVE-2026-32597 (`crit`
76
+ header bypass, CVSS 7.5).
77
+ - `cryptography>=46.0.7` added explicitly to the `oauth` extra
78
+ (was pulled transitively, unbounded). Picks up CVE-2026-26007
79
+ (ECC subgroup attack on the JWKS code path, CVSS 8.2) plus
80
+ CVE-2026-34073 and CVE-2026-39892.
81
+ - `python-multipart>=0.0.26` (was `>=0.0.9`). Picks up
82
+ CVE-2026-40347 (DoS via oversized multipart preamble).
83
+ - `pypa/gh-action-pypi-publish` in `publish.yml` pinned to a commit
84
+ SHA instead of the mutable `release/v1` branch. The publish job
85
+ holds `id-token: write`, so a tag/branch swap upstream would let
86
+ an attacker push a malicious wheel under our OIDC identity.
87
+
88
+ ### Removed
89
+
90
+ - `PasswordHasher.needs_rehash` — called pwdlib's non-existent
91
+ `check_needs_rehash` and would `AttributeError` if anyone invoked
92
+ it. No callers in src or tests. If you were planning to use it,
93
+ call `pwdlib.PasswordHash.verify_and_update` directly.
94
+
95
+ ### Internal
96
+
97
+ - 72 `mypy --strict` errors cleared across 35 files. `inv lint` is
98
+ green end-to-end (ruff + mypy). Still local-only — not yet a CI
99
+ gate.
100
+ - Mongo `BlacklistRepo.purge_expired` added (was missing from the
101
+ Mongo impl; SQL impl already had it). Mongo's TTL index still
102
+ reaps automatically; the explicit `delete_many` is for protocol
103
+ parity and for tests that can't wait for the 60-second TTL
104
+ monitor.
105
+ - `KNOWN_EVENTS` reconciled with reality: 7 previously-undeclared
106
+ events added (`verification_requested`, `email_change_requested`,
107
+ `email_changed`, `phone_setup_started`, `mfa_login_started`,
108
+ `mfa_enabled`, `mfa_disabled`).
109
+ - `routers/_helpers.require_password_set` factored out of
110
+ `routers/account.py` and reused in `routers/phone.py`.
111
+ - `AsyncDatabase[MongoDoc]` / `AsyncMongoClient[MongoDoc]`
112
+ parameterized across the Mongo backend so pymongo's typed stubs
113
+ are satisfied.
114
+
115
+ ### Notes
116
+
117
+ - `LockoutService` (per-account, sliding-window failure counter) is
118
+ unchanged and continues to defend `/login` against
119
+ credential-stuffing against a single account. Per-route IP limits
120
+ are orthogonal: they defend each endpoint against a single source
121
+ IP spamming requests across many accounts.
122
+ - The previously-reserved `login_max_per_minute` /
123
+ `login_max_per_hour` config fields are kept for back-compat but
124
+ no longer have any effect. Switch to the per-route fields when
125
+ you next touch your config.
126
+
127
+ ## 0.5.0 — 2026-05-02
128
+
129
+ ### Added
130
+
131
+ - **Theme designer.** `regstack theme design` opens a native pywebview
132
+ window with controls for every `--rs-*` CSS custom property and a
133
+ real-time preview of the bundled SSR widgets (sign-in form, success
134
+ / error banners, danger-zone button). Saving writes
135
+ `regstack-theme.css`; the designer round-trips values back into the
136
+ form on next launch so iteration is non-destructive. `--print-only`
137
+ mode takes repeatable `--var NAME=VALUE` pairs (with a `dark:`
138
+ prefix for dark-scheme overrides) and writes the file headlessly.
139
+ Lives in `regstack.wizard.theme_designer`; registered as a lazy
140
+ Click subgroup so `regstack init` / `doctor` don't pay the
141
+ pywebview/uvicorn import cost.
142
+ - "Why use regstack" pitch in `docs/index.md` updated to surface the
143
+ two pywebview tools (`oauth setup` + `theme design`) as a
144
+ distinguishing feature vs. fastapi-users / Auth0 / Keycloak.
145
+
146
+ ### Docs
147
+
148
+ - New "About the examples" convention block at the top of
149
+ `docs/index.md`. Every URL, email, smtp host, and admin command
150
+ across the docs now extrapolates from the same fictional app at
151
+ `app.example.com` with `<username>` / `<password>` placeholders —
152
+ no more `user:pw@host/dbname` / `db.internal/myapp` mishmash.
153
+
6
154
  ## 0.4.0 — 2026-05-02
7
155
 
8
156
  ### Added
@@ -0,0 +1,187 @@
1
+ # CLI reference
2
+
3
+ `regstack` is the entry point installed by the package. All sub-commands
4
+ share a config-loading model: programmatic kwargs > env vars >
5
+ `regstack.secrets.env` > `regstack.toml` > defaults. The `--config <path>`
6
+ flag overrides where the TOML file is found.
7
+
8
+ ## `regstack init`
9
+
10
+ Interactive wizard that writes `regstack.toml` and `regstack.secrets.env`
11
+ in the current directory. Asks which backend to use (SQLite default →
12
+ Postgres → MongoDB), generates a 64-byte JWT secret, runs DNS sanity
13
+ checks if asked, never provisions infrastructure.
14
+
15
+ ```bash
16
+ uv run regstack init
17
+ uv run regstack init --target /etc/app --force
18
+ ```
19
+
20
+ Options:
21
+
22
+ - `--target DIR` — directory to write the config files (default cwd).
23
+ - `--force` — overwrite without confirming.
24
+
25
+ Re-running the wizard prompts before overwriting unless `--force` is
26
+ passed; pre-existing answers aren't kept (the wizard is intentionally
27
+ stateless).
28
+
29
+ ## `regstack oauth setup`
30
+
31
+ Opens a guided 12-step wizard in a native webview window that walks you
32
+ through registering a Google OAuth 2.0 client (project selection,
33
+ consent screen, redirect URI, credentials) and merges the result into
34
+ your existing `regstack.toml` and `regstack.secrets.env`. The merge is
35
+ **non-clobbering** — comments, unrelated tables (`[email]`, `[sms]`,
36
+ …), and unrelated top-level keys are preserved. Re-run any time to
37
+ rotate credentials or change the linking policy.
38
+
39
+ ```bash
40
+ uv run regstack oauth setup
41
+ uv run regstack oauth setup --target /etc/app
42
+ ```
43
+
44
+ Options:
45
+
46
+ - `--target DIR` — directory containing (or to receive) `regstack.toml`
47
+ (default cwd).
48
+ - `--api-prefix PREFIX` — router prefix the host mounts regstack under
49
+ (default `/api/auth`). Used to compute the suggested redirect URI.
50
+ - `--port N` — pin the wizard server's TCP port (default: random free
51
+ port on `127.0.0.1`).
52
+ - `--print-only` — skip the GUI; print the TOML + secrets diff that
53
+ *would* be written to stdout, then exit. Useful for headless hosts
54
+ (CI, servers without a webview backend) and dry-run smoke tests.
55
+ Pair with `--client-id`, `--client-secret`, `--base-url`,
56
+ `--auto-link/--no-auto-link`, `--mfa/--no-mfa`.
57
+
58
+ The interactive mode requires a desktop environment with a webview
59
+ backend (WebKit on macOS, GTK / QtWebEngine on Linux, Edge WebView2 on
60
+ Windows). On a headless host it exits with a clear error pointing at
61
+ `--print-only`.
62
+
63
+ The wizard binds to `127.0.0.1` only and authenticates every API call
64
+ with a per-launch random token, so a hostile process on the same host
65
+ can't drive the write endpoint.
66
+
67
+ ## `regstack theme design`
68
+
69
+ Opens a live designer for `regstack-theme.css` in a native pywebview
70
+ window. The left pane has controls for every `--rs-*` CSS custom
71
+ property; the right pane renders the bundled SSR widgets (sign-in
72
+ form, success / error messages, danger-zone button) with your changes
73
+ applied in real time. Click **Save** to write the file; **Reset to
74
+ defaults** to start over; **Copy CSS** to put the generated
75
+ stylesheet on the clipboard without writing.
76
+
77
+ ```bash
78
+ uv run regstack theme design
79
+ uv run regstack theme design --target /var/www/static
80
+ ```
81
+
82
+ Options:
83
+
84
+ - `--target DIR` — directory to write `regstack-theme.css` into
85
+ (default cwd).
86
+ - `--filename NAME` — output filename
87
+ (default `regstack-theme.css`).
88
+ - `--port N` — pin the designer's TCP port (default: random free
89
+ port on `127.0.0.1`).
90
+ - `--print-only` — skip the GUI; write the file from `--var` pairs
91
+ and emit a JSON summary. For headless / CI use.
92
+ - `--var NAME=VALUE` — repeatable. Used with `--print-only`. Prefix
93
+ with `dark:` to set the dark-scheme value, e.g.
94
+ `--var dark:--rs-accent=#2dd4bf`.
95
+
96
+ Re-running the designer reloads the previous values out of the file,
97
+ so iterating on a theme is non-destructive. Only the `:root` and
98
+ `@media (prefers-color-scheme: dark)` blocks are managed by the
99
+ designer — anything else in the file is left alone.
100
+
101
+ Same security shape as `regstack oauth setup`: binds `127.0.0.1`
102
+ only, every API call requires a per-launch random token.
103
+
104
+ ## `regstack create-admin`
105
+
106
+ Create or promote a superuser. Idempotent.
107
+
108
+ ```bash
109
+ uv run regstack create-admin --email admin@app.example.com
110
+ uv run regstack create-admin --email admin@app.example.com --password 'long-strong-password'
111
+ uv run regstack create-admin --email admin@app.example.com --config /etc/app/regstack.toml
112
+ ```
113
+
114
+ Options:
115
+
116
+ - `--email EMAIL` *(required)*.
117
+ - `--password PW` — if omitted, prompts (with confirmation).
118
+ - `--config PATH` — TOML file to load (default: env or cwd).
119
+
120
+ If the user already exists, the command sets `is_superuser=True` and
121
+ keeps the existing password. If they don't exist, it creates the user
122
+ with `is_active=True`, `is_verified=True`, `is_superuser=True` and the
123
+ provided password.
124
+
125
+ ## `regstack migrate`
126
+
127
+ Runs the bundled Alembic migrations against the configured
128
+ `database_url`. Idempotent — re-running on a DB already at the target
129
+ revision is a no-op. Use this on SQL backends (SQLite / PostgreSQL)
130
+ to roll the schema forward to a new regstack release.
131
+
132
+ ```bash
133
+ uv run regstack migrate
134
+ uv run regstack migrate --target head
135
+ uv run regstack migrate --config /etc/app/regstack.toml --target 0001
136
+ ```
137
+
138
+ Options:
139
+
140
+ - `--config PATH` — TOML file to load (default: cwd / `$REGSTACK_CONFIG`).
141
+ - `--target REV` — revision to upgrade to (default `head`). Accepts
142
+ any Alembic revision spec: a revision id (`0001`), a relative step
143
+ (`+1`), or `head`.
144
+
145
+ Mongo backends are silently skipped: TTL indexes are installed by
146
+ `RegStack.install_schema()` on every app start, so there's no separate
147
+ migration story to run. Output prints the before / after revision and
148
+ exits non-zero if Alembic raises.
149
+
150
+ ## `regstack doctor`
151
+
152
+ Runs read-only validation against the loaded config and reports each
153
+ check on a green/red line. Exit code is the number of failed checks —
154
+ suitable for use in container health checks.
155
+
156
+ ```bash
157
+ uv run regstack doctor
158
+ uv run regstack doctor --config /etc/app/regstack.toml
159
+ uv run regstack doctor --check-dns
160
+ uv run regstack doctor --send-test-email alice@app.example.com
161
+ ```
162
+
163
+ Options:
164
+
165
+ - `--config PATH` — TOML file to load.
166
+ - `--check-dns` — run SPF / DMARC / MX `dig`s on the sender domain.
167
+ Internet-dependent; off by default.
168
+ - `--send-test-email TO` — actually send a probe email through the
169
+ configured backend. Costs real money on SES; off by default.
170
+
171
+ Default checks:
172
+
173
+ | Check | Pass criterion |
174
+ |-------|----------------|
175
+ | `jwt secret` | Present, ≥ 32 chars |
176
+ | `backend` | `Backend.ping()` succeeds (works for any backend) |
177
+ | `schema` | Mongo: required indexes present. SQL: `users` table responds to `count(*)` |
178
+ | `email backend` | `build_email_service(config.email)` instantiates |
179
+
180
+ Optional checks:
181
+
182
+ | Check | Pass criterion |
183
+ |-------|----------------|
184
+ | `dns mx` | At least one MX record on the sender domain |
185
+ | `dns spf` | A TXT record containing `v=spf1` |
186
+ | `dns dmarc` | A TXT record at `_dmarc.<domain>` containing `v=DMARC1` |
187
+ | `email send` | The configured backend's `send()` returned without raising |
@@ -85,15 +85,17 @@ regstack picks a backend at construction time from the URL scheme of
85
85
  - Notes
86
86
 
87
87
  * - SQLite
88
- - `sqlite+aiosqlite:///./path.db`
88
+ - Relative file: `sqlite+aiosqlite:///./dbname.db`
89
89
  - Default. Bundled in the base install — no extras needed.
90
- `:memory:` works too (per-test).
90
+ See [SQLite URL forms](#sqlite-url-forms) below for absolute-path
91
+ and in-memory variants.
91
92
  * - Postgres
92
- - `postgresql+asyncpg://user:pw@host/dbname`
93
+ - `postgresql+asyncpg://<username>:<password>@dbhost.example.com:5432/dbname`
93
94
  - Requires the `postgres` extra (pulls in `asyncpg`). The driver is
94
95
  pinned to `+asyncpg` — sync drivers won't work.
95
96
  * - MongoDB
96
- - `mongodb://host:port/dbname` (or `mongodb+srv://`)
97
+ - `mongodb://<username>:<password>@dbhost.example.com:27017/dbname`
98
+ (or `mongodb+srv://<username>:<password>@app.abc123.mongodb.net/dbname`)
97
99
  - Requires the `mongo` extra (pulls in `pymongo`). Database is taken
98
100
  from the URL path; falls back to ``mongodb_database`` if absent.
99
101
  ```
@@ -102,6 +104,41 @@ The active backend exposes the same five repository protocols on
102
104
  ``RegStack.users``, ``.pending``, ``.blacklist``, ``.attempts``,
103
105
  ``.mfa_codes``. Routers / hooks never branch on backend kind.
104
106
 
107
+ ### SQLite URL forms
108
+
109
+ SQLite is the default backend and the only one whose URL points at a
110
+ file rather than a network host. The shape is always:
111
+
112
+ ```
113
+ sqlite+aiosqlite:///PATH
114
+ ```
115
+
116
+ …where the prefix is fixed and `PATH` is whatever you want SQLAlchemy
117
+ to open. Three useful values for `PATH`:
118
+
119
+ | `PATH` | Resolves to | When to use it |
120
+ |---|---|---|
121
+ | `./dbname.db` | `dbname.db` in the process working directory | Local dev and the bundled `examples/` apps. |
122
+ | `/var/lib/app/dbname.db` | the absolute file at that path | Production. Point it at the host's persistent volume. |
123
+ | `:memory:` | per-process in-memory DB | Per-test fixtures only — contents vanish at process exit. |
124
+
125
+ So the three full URLs are:
126
+
127
+ ```bash
128
+ sqlite+aiosqlite:///./dbname.db
129
+ sqlite+aiosqlite:////var/lib/app/dbname.db
130
+ sqlite+aiosqlite:///:memory:
131
+ ```
132
+
133
+ The absolute form looks like it has four slashes, but it's the same
134
+ three-slash prefix as the others — the fourth slash is the leading
135
+ `/` of the absolute path. This is the single most common SQLite-URL
136
+ paper-cut.
137
+
138
+ Both file forms create the file on first connection, so a fresh
139
+ checkout running `uv run regstack init && uv run uvicorn …` works
140
+ with no `mkdir` or `touch` step.
141
+
105
142
  ## JWT
106
143
 
107
144
  ```{list-table}
@@ -234,14 +271,14 @@ The active backend exposes the same five repository protocols on
234
271
  ```toml
235
272
  [email]
236
273
  backend = "console" # console | smtp | ses
237
- from_address = "noreply@…"
238
- from_name = "MyApp"
274
+ from_address = "noreply@app.example.com"
275
+ from_name = "Example App"
239
276
 
240
277
  # smtp
241
- smtp_host = "smtp.example.com"
278
+ smtp_host = "smtp.app.example.com"
242
279
  smtp_port = 587
243
280
  smtp_starttls = true
244
- smtp_username = "myapp"
281
+ smtp_username = "<username>"
245
282
  # smtp_password is a SecretStr — set via REGSTACK_EMAIL__SMTP_PASSWORD
246
283
 
247
284
  # ses