regstack 0.5.9__tar.gz → 0.7.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 (259) hide show
  1. {regstack-0.5.9 → regstack-0.7.0}/.gitignore +12 -0
  2. {regstack-0.5.9 → regstack-0.7.0}/CHANGELOG.md +270 -0
  3. {regstack-0.5.9 → regstack-0.7.0}/PKG-INFO +10 -7
  4. {regstack-0.5.9 → regstack-0.7.0}/pyproject.toml +54 -11
  5. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/app.py +54 -0
  6. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/auth/dependencies.py +36 -1
  7. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/auth/rate_limit.py +2 -0
  8. regstack-0.7.0/src/regstack/backends/mongo/indexes.py +192 -0
  9. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/repositories/user_repo.py +18 -0
  10. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +23 -2
  11. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/cli/__main__.py +39 -7
  12. regstack-0.7.0/src/regstack/cli/_results.py +29 -0
  13. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/cli/doctor.py +2 -9
  14. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/cli/init.py +6 -21
  15. regstack-0.7.0/src/regstack/cli/validate/__init__.py +9 -0
  16. regstack-0.7.0/src/regstack/cli/validate/capture.py +56 -0
  17. regstack-0.7.0/src/regstack/cli/validate/cli.py +326 -0
  18. regstack-0.7.0/src/regstack/cli/validate/http.py +126 -0
  19. regstack-0.7.0/src/regstack/cli/validate/logtail.py +276 -0
  20. regstack-0.7.0/src/regstack/cli/validate/phases/__init__.py +5 -0
  21. regstack-0.7.0/src/regstack/cli/validate/phases/account.py +148 -0
  22. regstack-0.7.0/src/regstack/cli/validate/phases/cleanup.py +71 -0
  23. regstack-0.7.0/src/regstack/cli/validate/phases/core_auth.py +171 -0
  24. regstack-0.7.0/src/regstack/cli/validate/phases/feature_discover.py +68 -0
  25. regstack-0.7.0/src/regstack/cli/validate/phases/oauth.py +57 -0
  26. regstack-0.7.0/src/regstack/cli/validate/phases/password_reset.py +99 -0
  27. regstack-0.7.0/src/regstack/cli/validate/phases/reachability.py +50 -0
  28. regstack-0.7.0/src/regstack/cli/validate/phases/sms_mfa.py +123 -0
  29. regstack-0.7.0/src/regstack/cli/validate/report.py +33 -0
  30. regstack-0.7.0/src/regstack/cli/validate/runner.py +129 -0
  31. regstack-0.7.0/src/regstack/config/schema.py +373 -0
  32. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/composer.py +3 -1
  33. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/console.py +8 -2
  34. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/factory.py +1 -1
  35. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/ses.py +18 -1
  36. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/models/user.py +10 -4
  37. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/account.py +20 -8
  38. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/admin.py +46 -2
  39. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/login.py +17 -5
  40. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/oauth.py +47 -4
  41. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/password.py +1 -2
  42. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/register.py +1 -2
  43. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/verify.py +16 -3
  44. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/sms/factory.py +1 -1
  45. regstack-0.7.0/src/regstack/sms/null.py +38 -0
  46. regstack-0.7.0/src/regstack/version.py +1 -0
  47. regstack-0.5.9/.github/workflows/publish.yml +0 -69
  48. regstack-0.5.9/.github/workflows/test.yml +0 -147
  49. regstack-0.5.9/.python-version +0 -1
  50. regstack-0.5.9/.readthedocs.yaml +0 -23
  51. regstack-0.5.9/CLAUDE.md +0 -358
  52. regstack-0.5.9/docs/_static/.gitkeep +0 -0
  53. regstack-0.5.9/docs/_templates/.gitkeep +0 -0
  54. regstack-0.5.9/docs/api.md +0 -351
  55. regstack-0.5.9/docs/architecture.md +0 -250
  56. regstack-0.5.9/docs/changelog.md +0 -687
  57. regstack-0.5.9/docs/cli.md +0 -187
  58. regstack-0.5.9/docs/conf.py +0 -98
  59. regstack-0.5.9/docs/configuration.md +0 -425
  60. regstack-0.5.9/docs/embedding.md +0 -252
  61. regstack-0.5.9/docs/index.md +0 -197
  62. regstack-0.5.9/docs/oauth.md +0 -154
  63. regstack-0.5.9/docs/quickstart.md +0 -162
  64. regstack-0.5.9/docs/security-reports/README.md +0 -15
  65. regstack-0.5.9/docs/security.md +0 -328
  66. regstack-0.5.9/docs/theming.md +0 -213
  67. regstack-0.5.9/examples/_common/app.py +0 -101
  68. regstack-0.5.9/examples/mongo/README.md +0 -30
  69. regstack-0.5.9/examples/mongo/branding/theme.css +0 -25
  70. regstack-0.5.9/examples/mongo/main.py +0 -31
  71. regstack-0.5.9/examples/mongo/regstack.toml +0 -30
  72. regstack-0.5.9/examples/postgres/README.md +0 -29
  73. regstack-0.5.9/examples/postgres/main.py +0 -30
  74. regstack-0.5.9/examples/postgres/regstack.toml +0 -29
  75. regstack-0.5.9/examples/sqlite/README.md +0 -33
  76. regstack-0.5.9/examples/sqlite/main.py +0 -34
  77. regstack-0.5.9/examples/sqlite/regstack.toml +0 -30
  78. regstack-0.5.9/scripts/ccr_coverage_setup.py +0 -242
  79. regstack-0.5.9/scripts/security-review-prompt.md +0 -583
  80. regstack-0.5.9/src/regstack/backends/mongo/indexes.py +0 -98
  81. regstack-0.5.9/src/regstack/cli/__init__.py +0 -0
  82. regstack-0.5.9/src/regstack/config/schema.py +0 -208
  83. regstack-0.5.9/src/regstack/sms/null.py +0 -26
  84. regstack-0.5.9/src/regstack/version.py +0 -1
  85. regstack-0.5.9/tasks/oauth-design.md +0 -729
  86. regstack-0.5.9/tasks.py +0 -415
  87. regstack-0.5.9/tests/__init__.py +0 -0
  88. regstack-0.5.9/tests/_fake_google/__init__.py +0 -14
  89. regstack-0.5.9/tests/_fake_google/provider.py +0 -166
  90. regstack-0.5.9/tests/conftest.py +0 -266
  91. regstack-0.5.9/tests/e2e/__init__.py +0 -0
  92. regstack-0.5.9/tests/e2e/conftest.py +0 -121
  93. regstack-0.5.9/tests/e2e/test_theme_designer.py +0 -87
  94. regstack-0.5.9/tests/e2e/test_wizard_oauth_flow.py +0 -144
  95. regstack-0.5.9/tests/integration/__init__.py +0 -0
  96. regstack-0.5.9/tests/integration/test_account_management.py +0 -294
  97. regstack-0.5.9/tests/integration/test_admin_router.py +0 -270
  98. regstack-0.5.9/tests/integration/test_happy_path.py +0 -174
  99. regstack-0.5.9/tests/integration/test_indexes.py +0 -43
  100. regstack-0.5.9/tests/integration/test_login_lockout.py +0 -82
  101. regstack-0.5.9/tests/integration/test_mfa.py +0 -353
  102. regstack-0.5.9/tests/integration/test_oauth_google_router.py +0 -709
  103. regstack-0.5.9/tests/integration/test_oauth_repos.py +0 -267
  104. regstack-0.5.9/tests/integration/test_oauth_ui.py +0 -168
  105. regstack-0.5.9/tests/integration/test_password_reset.py +0 -121
  106. regstack-0.5.9/tests/integration/test_rate_limits.py +0 -215
  107. regstack-0.5.9/tests/integration/test_sql_migrations.py +0 -89
  108. regstack-0.5.9/tests/integration/test_ui_router.py +0 -117
  109. regstack-0.5.9/tests/integration/test_verification.py +0 -143
  110. regstack-0.5.9/tests/unit/__init__.py +0 -0
  111. regstack-0.5.9/tests/unit/test_base_install_imports.py +0 -79
  112. regstack-0.5.9/tests/unit/test_cli.py +0 -133
  113. regstack-0.5.9/tests/unit/test_cli_doctor.py +0 -164
  114. regstack-0.5.9/tests/unit/test_cli_init.py +0 -150
  115. regstack-0.5.9/tests/unit/test_cli_migrate.py +0 -151
  116. regstack-0.5.9/tests/unit/test_config_loader.py +0 -41
  117. regstack-0.5.9/tests/unit/test_jwt.py +0 -61
  118. regstack-0.5.9/tests/unit/test_lockout.py +0 -101
  119. regstack-0.5.9/tests/unit/test_mail_composer.py +0 -88
  120. regstack-0.5.9/tests/unit/test_mfa_code_repo.py +0 -126
  121. regstack-0.5.9/tests/unit/test_oauth_google.py +0 -445
  122. regstack-0.5.9/tests/unit/test_password.py +0 -16
  123. regstack-0.5.9/tests/unit/test_ses_backend.py +0 -26
  124. regstack-0.5.9/tests/unit/test_sms.py +0 -77
  125. regstack-0.5.9/tests/unit/test_smtp_backend.py +0 -62
  126. regstack-0.5.9/tests/unit/test_theme_designer_cli.py +0 -190
  127. regstack-0.5.9/tests/unit/test_theme_designer_routes.py +0 -179
  128. regstack-0.5.9/tests/unit/test_theme_designer_validators.py +0 -154
  129. regstack-0.5.9/tests/unit/test_theme_designer_writer.py +0 -156
  130. regstack-0.5.9/tests/unit/test_ui_env.py +0 -51
  131. regstack-0.5.9/tests/unit/test_wizard_oauth_cli.py +0 -230
  132. regstack-0.5.9/tests/unit/test_wizard_oauth_routes.py +0 -232
  133. regstack-0.5.9/tests/unit/test_wizard_oauth_validators.py +0 -257
  134. regstack-0.5.9/tests/unit/test_wizard_oauth_writer.py +0 -293
  135. regstack-0.5.9/uv.lock +0 -3308
  136. {regstack-0.5.9 → regstack-0.7.0}/LICENSE +0 -0
  137. {regstack-0.5.9 → regstack-0.7.0}/NOTICE +0 -0
  138. {regstack-0.5.9 → regstack-0.7.0}/README.md +0 -0
  139. {regstack-0.5.9 → regstack-0.7.0}/SECURITY.md +0 -0
  140. {regstack-0.5.9 → regstack-0.7.0}/regstack.toml.example +0 -0
  141. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/__init__.py +0 -0
  142. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/auth/__init__.py +0 -0
  143. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/auth/clock.py +0 -0
  144. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/auth/jwt.py +0 -0
  145. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/auth/lockout.py +0 -0
  146. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/auth/mfa.py +0 -0
  147. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/auth/password.py +0 -0
  148. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/auth/tokens.py +0 -0
  149. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/__init__.py +0 -0
  150. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/base.py +0 -0
  151. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/factory.py +0 -0
  152. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/__init__.py +0 -0
  153. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/backend.py +0 -0
  154. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/client.py +0 -0
  155. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
  156. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
  157. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
  158. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
  159. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +0 -0
  160. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +0 -0
  161. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
  162. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/protocols.py +0 -0
  163. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/__init__.py +0 -0
  164. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/backend.py +0 -0
  165. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/migrations/__init__.py +0 -0
  166. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/migrations/env.py +0 -0
  167. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  168. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  169. {regstack-0.5.9/examples/_common → regstack-0.7.0/src/regstack/backends/sql/repositories}/__init__.py +0 -0
  170. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  171. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  172. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
  173. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +0 -0
  174. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/repositories/oauth_state_repo.py +0 -0
  175. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
  176. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
  177. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/schema.py +0 -0
  178. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/backends/sql/types.py +0 -0
  179. {regstack-0.5.9/src/regstack/backends/sql/repositories → regstack-0.7.0/src/regstack/cli}/__init__.py +0 -0
  180. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/cli/_runtime.py +0 -0
  181. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/cli/admin.py +0 -0
  182. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/cli/migrate.py +0 -0
  183. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/config/__init__.py +0 -0
  184. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/config/loader.py +0 -0
  185. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/config/secrets.py +0 -0
  186. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/__init__.py +0 -0
  187. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/base.py +0 -0
  188. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/smtp.py +0 -0
  189. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/email_change.html +0 -0
  190. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/email_change.subject.txt +0 -0
  191. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/email_change.txt +0 -0
  192. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/password_reset.html +0 -0
  193. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  194. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/password_reset.txt +0 -0
  195. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  196. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  197. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/verification.html +0 -0
  198. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/verification.subject.txt +0 -0
  199. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/email/templates/verification.txt +0 -0
  200. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/hooks/__init__.py +0 -0
  201. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/hooks/events.py +0 -0
  202. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/models/__init__.py +0 -0
  203. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/models/_objectid.py +0 -0
  204. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/models/login_attempt.py +0 -0
  205. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/models/mfa_code.py +0 -0
  206. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/models/oauth_identity.py +0 -0
  207. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/models/oauth_state.py +0 -0
  208. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/models/pending_registration.py +0 -0
  209. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/oauth/__init__.py +0 -0
  210. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/oauth/base.py +0 -0
  211. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/oauth/errors.py +0 -0
  212. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/oauth/providers/__init__.py +0 -0
  213. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/oauth/providers/google.py +0 -0
  214. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/oauth/registry.py +0 -0
  215. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/__init__.py +0 -0
  216. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/_helpers.py +0 -0
  217. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/_schemas.py +0 -0
  218. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/logout.py +0 -0
  219. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/routers/phone.py +0 -0
  220. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/sms/__init__.py +0 -0
  221. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/sms/base.py +0 -0
  222. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/sms/sns.py +0 -0
  223. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/sms/twilio.py +0 -0
  224. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/__init__.py +0 -0
  225. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/pages.py +0 -0
  226. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/static/css/core.css +0 -0
  227. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/static/css/theme.css +0 -0
  228. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/static/js/regstack.js +0 -0
  229. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  230. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/templates/auth/forgot.html +0 -0
  231. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/templates/auth/login.html +0 -0
  232. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/templates/auth/me.html +0 -0
  233. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  234. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
  235. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/templates/auth/register.html +0 -0
  236. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/templates/auth/reset.html +0 -0
  237. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/templates/auth/verify.html +0 -0
  238. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/ui/templates/base.html +0 -0
  239. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/__init__.py +0 -0
  240. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/oauth_google/__init__.py +0 -0
  241. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/oauth_google/cli.py +0 -0
  242. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/oauth_google/routes.py +0 -0
  243. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/oauth_google/server.py +0 -0
  244. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
  245. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
  246. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
  247. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/oauth_google/validators.py +0 -0
  248. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/oauth_google/window.py +0 -0
  249. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/oauth_google/writer.py +0 -0
  250. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/theme_designer/__init__.py +0 -0
  251. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/theme_designer/cli.py +0 -0
  252. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/theme_designer/routes.py +0 -0
  253. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/theme_designer/server.py +0 -0
  254. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/theme_designer/static/designer.css +0 -0
  255. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/theme_designer/static/designer.js +0 -0
  256. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/theme_designer/templates/designer.html +0 -0
  257. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/theme_designer/validators.py +0 -0
  258. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/theme_designer/window.py +0 -0
  259. {regstack-0.5.9 → regstack-0.7.0}/src/regstack/wizard/theme_designer/writer.py +0 -0
@@ -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,276 @@ authoritative copy lives at
5
5
  [`docs/changelog.md`](docs/changelog.md) and is rendered into the
6
6
  Sphinx docs.
7
7
 
8
+ ## Unreleased
9
+
10
+ ## 0.7.0 — 2026-05-17
11
+
12
+ Two-week sprint that lands the `regstack validate` end-to-end probe,
13
+ seven security-review findings, a clutch of host-integration
14
+ ergonomic wins (per-link email URL templates, optional auth
15
+ dependency, admin `promote_pending`, explicit SES credentials), and
16
+ two breaking API trims (`UserPublic._id` → `id`,
17
+ `TokenTransport = "bearer"` only).
18
+
19
+ The headline is `regstack validate` — a new CLI command that drives
20
+ a real deployed install through every auth flow (register, verify,
21
+ login, logout, password reset, change-email, OAuth start, SMS 2FA)
22
+ from a remote operator workstation, scraping one-time tokens out of
23
+ the deployment's stdout via a `--log-source` of your choice
24
+ (`file:`, `ssh:`, `docker:`, `cmd:`). The companion to `regstack
25
+ doctor`: doctor checks the loaded config, validate checks the
26
+ running service.
27
+
28
+ **Breaking.**
29
+
30
+ - **`UserPublic` JSON key is `id`, not `_id`.** The `alias="_id"`
31
+ on `UserPublic.id` (and the accompanying `populate_by_name=True`)
32
+ is removed. Every endpoint returning a `UserPublic` —
33
+ `POST /api/auth/register`, `GET /api/auth/me`, `PATCH /api/auth/me`,
34
+ and the admin user endpoints — now sends `id` on the wire.
35
+ `BaseUser` (the Mongo-document model) keeps the alias because it
36
+ round-trips to BSON via `to_mongo()`; only the API contract is
37
+ touched. Clients that read `body["_id"]` should switch to
38
+ `body["id"]`. Hosts hand-rolling a `/me` override solely to swap
39
+ the key shape can drop it.
40
+ - **`TokenTransport` literal narrowed to `Literal["bearer"]`.**
41
+ `"cookie"` was previously accepted by config validation but
42
+ silently no-op'd (no router ever set `Set-Cookie`). Hosts that
43
+ set `transport = "cookie"` now get a clear pydantic
44
+ `literal_error` at startup instead of a silent
45
+ security-misconfiguration. `RegStackConfig.cookie_domain` is
46
+ removed along with it. `regstack init` no longer offers the
47
+ cookie option either.
48
+
49
+ **Added.**
50
+
51
+ - **`regstack validate`.** End-to-end probe of a deployed install
52
+ — registers a throwaway user, walks every auth flow, then
53
+ deletes it. Reads one-time tokens out of the deployment's
54
+ stdout via `--log-source` (file / ssh / docker / arbitrary
55
+ command). Skip phases with `--skip`. Companion to `regstack
56
+ doctor` (which only validates loaded config). See
57
+ `regstack validate --help` for the full operator runbook.
58
+ - **`email.log_bodies` and `sms.log_bodies` config flags** to
59
+ promote the console / null backends' body log lines from
60
+ DEBUG → INFO without enabling DEBUG globally. `email.log_bodies`
61
+ defaults to `False`; `sms.log_bodies` defaults to `True`
62
+ (preserves prior null-SMS behaviour). Other backends ignore.
63
+ - **`RegStackConfig.email_link_prefix` + auto-resolve from
64
+ `ui_prefix`.** Verification / reset / email-change links now
65
+ default to `<base_url><ui_prefix>/verify?token=...` when the
66
+ bundled UI router is enabled, instead of bare `/verify`. Hosts
67
+ whose SPA owns the auth pages can pin a path explicitly via
68
+ `email_link_prefix`; the bundled UI hosts get the right links
69
+ automatically.
70
+ - **`EmailConfig.from_name` defaults to `app_name`** when unset.
71
+ Hosts that change `app_name` to brand outgoing email also get
72
+ the matching `From:` header automatically. Explicit `from_name`
73
+ values still win.
74
+ - **Per-link email URL templates.** Three new optional fields on
75
+ `RegStackConfig` — `verify_url_template`,
76
+ `password_reset_url_template`, `email_change_url_template` —
77
+ let SPAs whose router shape doesn't fit
78
+ `/verify?token=...` rewrite the email links. Templates
79
+ substitute `{base_url}` and `{token}` literally. Hash-routed
80
+ SPA: `"{base_url}/#/verify/{token}"`. Sibling subdomain:
81
+ `"https://auth.example.com/verify/{token}"`. Default unset
82
+ falls back to the prefix-based composition above. New helpers
83
+ `RegStackConfig.resolve_{verify,password_reset,email_change}_url(token)`.
84
+ - **`current_user_optional` dependency.** Companion to
85
+ `current_user` / `current_admin` on `regstack.deps`. Returns
86
+ `BaseUser | None` instead of raising 401, for endpoints that
87
+ render differently for signed-in vs anonymous callers (cart
88
+ icon, comment-author prefill). Every form of auth failure —
89
+ missing header, wrong scheme, malformed / expired / revoked
90
+ token, deleted or bulk-revoked user — collapses to `None`.
91
+ - **`RegStack.promote_pending(email)` + admin route.** Converts
92
+ a `PendingRegistration` row directly into a verified active
93
+ user, bypassing the email-link round-trip. Hashed password and
94
+ full name carry over verbatim. Fires the same `user_verified`
95
+ hook as `POST /verify`. Useful for admin rescue of stuck
96
+ signups, batch seeding from a known-good list, and dev
97
+ fixtures. Exposed as `POST /admin/pending/{email}/promote`
98
+ when the admin router is enabled.
99
+ - **Explicit SES credential fields on `EmailConfig`.** New
100
+ `ses_access_key_id` / `ses_secret_access_key` (both `SecretStr |
101
+ None`) let hosts pass AWS creds directly instead of relying on
102
+ boto3's env-var fallthrough. Validated as a pair, mutually
103
+ exclusive with `ses_profile`.
104
+
105
+ **Security.**
106
+
107
+ - **CVE-2026-42561 — `python-multipart>=0.0.27`.** Closes a
108
+ network-exploitable DoS via unbounded multipart part-header
109
+ parsing (CVSS 7.5). Previous floor `>=0.0.26` had the earlier
110
+ CVE-2026-40347 fix only.
111
+ - **sdist no longer ships internal docs to PyPI.** Added a
112
+ `[tool.hatch.build.targets.sdist]` exclude block. The published
113
+ source tarball used to contain `CLAUDE.md` (with a developer
114
+ home-directory path), the security-review prompt, the full test
115
+ suite, build tooling, and (when built from a worktree) a `.git`
116
+ text file pointing at the operator's worktrees directory.
117
+ - **Defensive `ObjectId.is_valid()` on nine Mongo UserRepo
118
+ mutations.** `set_last_login`, `set_tokens_invalidated_after`,
119
+ `update_password`, `set_active`, `set_superuser`, `set_full_name`,
120
+ `set_phone`, `set_mfa_enabled`, and `update_email` now match
121
+ `get_by_id` / `delete`: invalid input no-ops instead of raising
122
+ `bson.errors.InvalidId` (which would have surfaced as a 500 on
123
+ any future caller passing raw external input).
124
+ - **Per-IP rate-limit map covers `/login/mfa-confirm` and
125
+ `/oauth/exchange`.** Two new config fields:
126
+ `login_mfa_confirm_rate_limit`, `oauth_exchange_rate_limit`.
127
+ The per-code attempt counter on `mfa_codes` defends each
128
+ individual code; this adds the per-IP layer against distributed
129
+ guessing across many source IPs.
130
+ - **OAuth callback `error` query parameter sanitized before
131
+ logging.** A compromised or malicious OAuth provider could
132
+ previously inject newlines / ANSI escapes into the log stream
133
+ via the `error=...` redirect. The callback now strips control
134
+ characters and caps length at 200 before logging.
135
+ - **`oauth_states.mode` validated at the MongoDB storage layer.**
136
+ A `$jsonSchema` validator on the collection enforces
137
+ `mode IN ('signin', 'link')`, matching the SQL backend's
138
+ existing `CheckConstraint`. `OAuthState.model_validate()`
139
+ already enforced this at the app layer; this is defence-in-depth.
140
+ - **Migration `0002` downgrade refuses to roll back when OAuth-only
141
+ users exist.** The downgrade re-applies `NOT NULL` to
142
+ `users.hashed_password`; if any row has `NULL` (OAuth-only
143
+ signup), it now raises `RuntimeError` with a clear remediation
144
+ message instead of silently succeeding on SQLite (where
145
+ `batch_alter_table`'s CREATE-COPY-DROP-RENAME path skipped
146
+ NOT NULL enforcement).
147
+ - **PEP 740 sigstore attestations on the PyPI publish workflow.**
148
+ Each published wheel / sdist is now cryptographically bound to
149
+ the specific GitHub Actions run that produced it, so consumers
150
+ can verify the artefact came from this repo's CI.
151
+ - **`workflow_dispatch` removed from `publish.yml`.** Manual runs
152
+ previously uploaded artefacts to Actions storage with no
153
+ version validation, where they could be confused with a real
154
+ release build. Tag-push is the only supported trigger.
155
+
156
+ **Fixed.**
157
+
158
+ - **`regstack doctor --send-test-email` honours the new `from_name`
159
+ fall-back.** Before, the probe path passed `config.email.from_name`
160
+ (now `Optional[str]`) straight into `EmailMessage.from_name`
161
+ (typed `str`), producing a `None <addr>` From: header when
162
+ unset.
163
+ - **`install_schema()` survives a legacy unnamed unique-on-email
164
+ index.** A host that previously ran
165
+ `db.users.create_index([("email", 1)], unique=True)` from its
166
+ own pre-regstack auth code has a Mongo-auto-named `email_1`
167
+ index. `install_indexes` previously crashed on first boot with
168
+ `IndexOptionsConflict`. It now detects any unnamed/legacy
169
+ unique index over `{"email": 1}`, drops it, and proceeds.
170
+ Idempotent on a healthy DB.
171
+ - **`POST /verify` no longer 500s on the admin-promote-meets-user-
172
+ clicks-verify race.** The endpoint now catches
173
+ `UserAlreadyExistsError` from `users.create` and returns a
174
+ graceful 400 ("This email is already registered. Please sign
175
+ in.") instead of letting the unique-constraint violation bubble
176
+ up as a 500.
177
+
178
+ **Internal.**
179
+
180
+ - **GitHub Actions pinned ahead of Node 20 deprecation.**
181
+ `actions/checkout` v4→v6.0.2, `astral-sh/setup-uv` v3→v8.1.0,
182
+ `actions/upload-artifact` v4→v7.0.1, `actions/download-artifact`
183
+ v4→v8.0.1. All pins remain commit SHAs.
184
+ - **Daily scheduled security-review reports** land under
185
+ `docs/security-reports/` for 2026-05-15 through 2026-05-17.
186
+ The 2026-05-17 report is `[security-clean]`: all warnings from
187
+ the prior two days resolved in this release.
188
+
189
+ ## 0.6.0 — 2026-05-14
190
+
191
+ **Breaking change for wizard users.** The GUI setup wizards
192
+ (`regstack oauth setup`, `regstack theme design`) are now behind a
193
+ new optional `wizard` extra. `pip install regstack` no longer pulls
194
+ in `pywebview`, `tomlkit`, or `uvicorn[standard]` — three heavy
195
+ wizard-only dependencies that every library consumer was paying for,
196
+ including a platform browser engine on every fresh install. A
197
+ recurring audit recommendation since 0.5.0.
198
+
199
+ **Migration.**
200
+
201
+ - If you only embed regstack in a FastAPI app (no `regstack oauth
202
+ setup` or `regstack theme design`): no action needed. The base
203
+ install is now significantly slimmer.
204
+ - If you use either setup wizard: install the new extra —
205
+ `pip install 'regstack[wizard]'` or `uv sync --extra wizard`.
206
+ Running a wizard subcommand without the extra now exits with a
207
+ one-line install hint (no ImportError traceback).
208
+ - The `dev` extra continues to pull in the wizard deps directly so
209
+ `inv test-all` keeps working without an explicit `--extra wizard`.
210
+
211
+ Bumped to **0.6.0** (not 0.5.12) because removing top-level deps is
212
+ the kind of change that can surprise downstream `pip install
213
+ regstack` callers — even though "the GUI wizard CLIs need an extra
214
+ now" is the only observable effect.
215
+
216
+ ## 0.5.11 — 2026-05-14
217
+
218
+ CI / workflow hygiene. No runtime code changes.
219
+
220
+ - **All third-party GitHub Actions pinned to commit SHAs.**
221
+ `actions/checkout@v4`, `astral-sh/setup-uv@v3`,
222
+ `actions/upload-artifact@v4`, and `actions/download-artifact@v4` now
223
+ use commit SHAs in `.github/workflows/publish.yml` and
224
+ `.github/workflows/test.yml`, with `# v4` / `# v3` trailing comments
225
+ so future operators can resolve and bump. `pypa/gh-action-pypi-publish`
226
+ was already SHA-pinned (#37). A tag swap upstream can no longer
227
+ substitute a malicious version.
228
+ - **`permissions:` blocks added to every workflow + job.** Both
229
+ workflows now declare a `permissions: contents: read` default at
230
+ the workflow level and re-state it per job (so a future addition of
231
+ a write-needing action doesn't silently inherit elevated scopes).
232
+ The `publish` job continues to declare `id-token: write` (OIDC
233
+ trusted-publisher exchange) — that's the only scope above
234
+ read-only anywhere in the workflows.
235
+ - **`.gitignore` defensive additions.** `.env`, `.env.*`, and the
236
+ common credential-file patterns (`*.pem`, `*.key`, `*.p12`,
237
+ `*.pfx`, `*.jks`, `*.crt`) are now ignored at the repo root. None
238
+ are present today; this is belt-and-braces for misconfigured local
239
+ dev environments. A recurring audit recommendation.
240
+
241
+ ## 0.5.10 — 2026-05-14
242
+
243
+ Security fixes from the 2026-05-13 / 2026-05-14 daily reviews. All
244
+ warnings, no criticals — but several are real exploitable issues.
245
+
246
+ **Security.**
247
+
248
+ - **Open-redirect bypass in OAuth `redirect_to`.** `_validate_redirect`
249
+ was forwarding `urlsplit`'s judgment, but browsers normalize values
250
+ like `/\evil.com` and `////evil.com` into the protocol-relative
251
+ `//evil.com` — both of which `urlsplit` reports as same-origin
252
+ paths. The validator now rejects any backslash plus any value that
253
+ doesn't start with a single `/` followed by a non-slash character.
254
+ - **CVE-2025-62727 — `fastapi` floor raised to `>=0.120.0`.** Starlette
255
+ DoS via large request bodies after multipart processing.
256
+ - **CVE-2025-27516 — `jinja2` floor raised to `>=3.1.6`.** Sandbox
257
+ breakout via the `|attr` filter (only relevant if hosts allow
258
+ user-controlled templates; tightening the floor regardless).
259
+ - **Login lockout no longer skips disabled / unverified accounts.**
260
+ `POST /login` now records a failure before raising HTTP 403 for
261
+ `is_active=False` and (when `require_verification=True`)
262
+ `is_verified=False` users. Password verification was also re-ordered
263
+ to run **before** those checks, so an attacker without the password
264
+ can't distinguish disabled vs active accounts by HTTP code alone.
265
+ - **`POST /change-email` no longer enumerates registered addresses.**
266
+ An authenticated attacker could previously iterate the email
267
+ namespace via the 409 vs 202 response distinction. The endpoint now
268
+ always returns 202; if the candidate is already registered, no
269
+ confirmation email is sent (the legitimate user finds out by not
270
+ receiving it). Matches the existing anti-enumeration stance on
271
+ `/forgot-password` and `/resend-verification`.
272
+ - **Admin resend-verification rejects OAuth-only users.** Previously
273
+ attempted to construct a `PendingRegistration` from a user with
274
+ `hashed_password=None`, which either failed validation or stored
275
+ the literal string `"None"` in the pending row. Now returns 400
276
+ with a clear message.
277
+
8
278
  ## 0.5.9 — 2026-05-13
9
279
 
10
280
  **`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.7.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
- Requires-Dist: python-multipart>=0.0.26
32
- Requires-Dist: pywebview>=5.0
31
+ Requires-Dist: python-multipart>=0.0.27
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regstack"
3
- version = "0.5.9"
3
+ version = "0.7.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,29 +17,30 @@ 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
- # python-multipart>=0.0.26 picks up CVE-2026-40347 (DoS via oversized
30
- # multipart preamble).
31
- "python-multipart>=0.0.26",
34
+ # python-multipart>=0.0.27 picks up CVE-2026-40347 (DoS via oversized
35
+ # multipart preamble) and CVE-2026-42561 (DoS via unbounded part-header
36
+ # count/size — CVSS 7.5, network-exploitable).
37
+ "python-multipart>=0.0.27",
32
38
  "email-validator>=2.1",
33
39
  "aiosmtplib>=3.0",
34
40
  # SQL stack — bundled by default because SQLite is the default backend.
35
41
  "sqlalchemy[asyncio]>=2.0",
36
42
  "aiosqlite>=0.20",
37
43
  "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
44
  ]
44
45
 
45
46
  [project.optional-dependencies]
@@ -51,6 +52,16 @@ twilio = ["twilio>=9.0"]
51
52
  # cryptography>=46.0.7 picks up CVE-2026-26007 (ECC subgroup attack on the
52
53
  # JWKS code path) plus CVE-2026-34073 and CVE-2026-39892.
53
54
  oauth = ["pyjwt[crypto]>=2.12.1", "cryptography>=46.0.7"]
55
+ # Interactive setup wizards (`regstack oauth setup`, `regstack theme
56
+ # design`). These open a native pywebview window over a local 127.0.0.1
57
+ # FastAPI server and use tomlkit for non-destructive TOML merging.
58
+ # Optional because library consumers who never run the GUI tools don't
59
+ # need a platform browser engine pulled in by pywebview.
60
+ wizard = [
61
+ "pywebview>=5.0",
62
+ "tomlkit>=0.13",
63
+ "uvicorn[standard]>=0.29",
64
+ ]
54
65
  # Per-route rate limiting on the auth router. The limiter itself can be
55
66
  # host-supplied (so hosts that already use slowapi share the same Limiter
56
67
  # state), otherwise regstack constructs an in-memory one.
@@ -86,6 +97,10 @@ dev = [
86
97
  "pytest-playwright>=0.5",
87
98
  # Rate-limit tests exercise the slowapi integration.
88
99
  "slowapi>=0.1.9",
100
+ # Wizard surface (now an optional `wizard` extra) — tests exercise
101
+ # the setup/theme-designer routes, writers, and Playwright e2e.
102
+ "pywebview>=5.0",
103
+ "tomlkit>=0.13",
89
104
  ]
90
105
 
91
106
  [project.scripts]
@@ -104,6 +119,34 @@ packages = ["src/regstack"]
104
119
  # Hatch's package autodiscovery already picks up template / static assets
105
120
  # under src/regstack — no force-include needed (it would double-pack).
106
121
 
122
+ [tool.hatch.build.targets.sdist]
123
+ # Hatchling's sdist default includes every git-tracked file. Without this
124
+ # block the published source tarball ships internal planning docs,
125
+ # security-review prompts, the full test suite, and CI configuration to
126
+ # anyone who runs `pip download --no-binary`. The wheel target above is
127
+ # already tight (src/regstack only); this list is the matching sdist
128
+ # discipline. Flagged as W-2 in the 2026-05-15 / 2026-05-16 security
129
+ # reviews — CLAUDE.md in particular contains a developer home path.
130
+ exclude = [
131
+ ".git",
132
+ ".github/",
133
+ ".python-version",
134
+ ".readthedocs.yaml",
135
+ "CLAUDE.md",
136
+ "docs/",
137
+ "examples/",
138
+ "scripts/",
139
+ "tasks.py",
140
+ "tasks/",
141
+ "tests/",
142
+ "uv.lock",
143
+ ]
144
+ # Note on `.git`: hatchling normally skips it because it's a directory, but
145
+ # when this repo is built from a git *worktree* (e.g. release prep on a
146
+ # branch), `.git` is a 1-line text file containing an absolute path to the
147
+ # primary repo's worktrees dir — which leaks the developer's home directory
148
+ # into PyPI. Excluding it explicitly is harmless for non-worktree builds.
149
+
107
150
  [tool.pytest.ini_options]
108
151
  minversion = "8.0"
109
152
  asyncio_mode = "auto"
@@ -341,6 +341,60 @@ class RegStack:
341
341
  )
342
342
  return await self.users.create(user)
343
343
 
344
+ async def promote_pending(self, email: str) -> BaseUser:
345
+ """Convert a pending registration directly into a verified user.
346
+
347
+ Bypasses the email-link round-trip. Useful when:
348
+
349
+ - a user lost their verification link and ``resend-verification``
350
+ isn't an option (admin-triggered onboarding, dev fixtures);
351
+ - a CLI batch operation seeds users from a known-good list;
352
+ - an admin is rescuing a stuck signup.
353
+
354
+ The pending row's ``hashed_password`` and ``full_name`` carry
355
+ over verbatim — the user logs in with the password they
356
+ originally registered with. The pending row is deleted on
357
+ success. Fires the ``user_verified`` hook so analytics /
358
+ downstream listeners see the same event the email-driven
359
+ ``POST /verify`` produces.
360
+
361
+ Args:
362
+ email: The email address whose pending registration should
363
+ be promoted.
364
+
365
+ Returns:
366
+ The newly persisted, active, verified
367
+ :class:`~regstack.models.user.BaseUser`.
368
+
369
+ Raises:
370
+ LookupError: If no pending registration exists for that
371
+ email (caller's job to surface as 404 / CLI error).
372
+ UserAlreadyExistsError: If a non-pending user with that
373
+ email already exists (caller's job to surface as 409).
374
+ """
375
+ pending = await self.pending.find_by_email(email)
376
+ if pending is None:
377
+ raise LookupError(f"No pending registration for {email!r}.")
378
+ # Match ``POST /verify``'s contract: an expired pending row is
379
+ # not a valid promotion target. Mongo's TTL reap is eventual
380
+ # and the SQL backends only purge on ``purge_expired()``, so a
381
+ # row past its window can still appear here. Treat it as
382
+ # missing rather than silently promoting a stale invitation.
383
+ if pending.expires_at <= self.clock.now():
384
+ await self.pending.delete_by_email(pending.email)
385
+ raise LookupError(f"Pending registration for {email!r} has expired.")
386
+ user = BaseUser(
387
+ email=pending.email,
388
+ hashed_password=pending.hashed_password,
389
+ full_name=pending.full_name,
390
+ is_active=True,
391
+ is_verified=True,
392
+ )
393
+ user = await self.users.create(user)
394
+ await self.pending.delete_by_email(pending.email)
395
+ await self.hooks.fire("user_verified", user=user)
396
+ return user
397
+
344
398
  # --- Extension surface ------------------------------------------------
345
399
 
346
400
  def set_email_backend(self, service: EmailService) -> None:
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Awaitable, Callable
4
- from typing import TYPE_CHECKING
4
+ from typing import TYPE_CHECKING, Optional
5
5
 
6
6
  from fastapi import Depends, HTTPException, Request, status
7
7
  from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -14,6 +14,10 @@ if TYPE_CHECKING:
14
14
  from regstack.models.user import BaseUser
15
15
 
16
16
  UserDependency = Callable[..., Awaitable["BaseUser"]]
17
+ # typing.Optional avoids a PEP-604 union-inside-string-forward-ref,
18
+ # which some mypy / pyright configurations refuse to resolve when
19
+ # BaseUser is only imported under TYPE_CHECKING.
20
+ OptionalUserDependency = Callable[..., Awaitable[Optional["BaseUser"]]]
17
21
 
18
22
  _bearer = HTTPBearer(auto_error=False)
19
23
 
@@ -82,6 +86,37 @@ class AuthDependencies:
82
86
 
83
87
  return _dep
84
88
 
89
+ def current_user_optional(self) -> OptionalUserDependency:
90
+ """Return a FastAPI dependency that yields the user or ``None``.
91
+
92
+ For endpoints that render differently for authenticated vs
93
+ anonymous callers (think "show your cart icon if logged in").
94
+ Treats every form of auth failure — missing header, bad scheme,
95
+ expired token, revoked jti, deleted user, bulk-revoked session
96
+ — as anonymous and returns ``None`` rather than raising 401.
97
+
98
+ On success the user is stashed on
99
+ ``request.state.regstack_user`` (same as :meth:`current_user`),
100
+ so downstream middleware sees the same shape for either path.
101
+
102
+ Returns:
103
+ A callable suitable for ``Depends(...)``. Never raises an
104
+ ``HTTPException`` — auth problems collapse to ``None``.
105
+ """
106
+
107
+ async def _dep(
108
+ request: Request,
109
+ creds: HTTPAuthorizationCredentials | None = Depends(_bearer),
110
+ ) -> BaseUser | None:
111
+ try:
112
+ user = await self._authenticate(creds)
113
+ except HTTPException:
114
+ return None
115
+ request.state.regstack_user = user
116
+ return user
117
+
118
+ return _dep
119
+
85
120
  def current_admin(self) -> UserDependency:
86
121
  """Return a FastAPI dependency that yields a *superuser*.
87
122
 
@@ -43,6 +43,7 @@ if TYPE_CHECKING:
43
43
  # decoration walks the assembled APIRouter and matches by `route.path`.
44
44
  ROUTE_LIMIT_MAP: dict[str, str] = {
45
45
  "login_rate_limit": "/login",
46
+ "login_mfa_confirm_rate_limit": "/login/mfa-confirm",
46
47
  "register_rate_limit": "/register",
47
48
  "forgot_password_rate_limit": "/forgot-password",
48
49
  "reset_password_rate_limit": "/reset-password",
@@ -52,6 +53,7 @@ ROUTE_LIMIT_MAP: dict[str, str] = {
52
53
  "change_email_rate_limit": "/change-email",
53
54
  "confirm_email_change_rate_limit": "/confirm-email-change",
54
55
  "delete_account_rate_limit": "/account",
56
+ "oauth_exchange_rate_limit": "/oauth/exchange",
55
57
  }
56
58
 
57
59