regstack 0.3.0__tar.gz → 0.4.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 (220) hide show
  1. {regstack-0.3.0 → regstack-0.4.0}/CLAUDE.md +17 -2
  2. {regstack-0.3.0 → regstack-0.4.0}/PKG-INFO +12 -5
  3. {regstack-0.3.0 → regstack-0.4.0}/README.md +7 -4
  4. {regstack-0.3.0 → regstack-0.4.0}/docs/api.md +73 -0
  5. {regstack-0.3.0 → regstack-0.4.0}/docs/architecture.md +48 -2
  6. {regstack-0.3.0 → regstack-0.4.0}/docs/changelog.md +18 -0
  7. {regstack-0.3.0 → regstack-0.4.0}/docs/conf.py +8 -1
  8. {regstack-0.3.0 → regstack-0.4.0}/docs/configuration.md +34 -1
  9. {regstack-0.3.0 → regstack-0.4.0}/docs/embedding.md +58 -3
  10. {regstack-0.3.0 → regstack-0.4.0}/docs/index.md +5 -1
  11. {regstack-0.3.0 → regstack-0.4.0}/docs/oauth.md +19 -0
  12. regstack-0.4.0/docs/security-reports/README.md +15 -0
  13. {regstack-0.3.0 → regstack-0.4.0}/docs/security.md +71 -0
  14. {regstack-0.3.0 → regstack-0.4.0}/pyproject.toml +15 -1
  15. regstack-0.4.0/scripts/security-review-prompt.md +583 -0
  16. regstack-0.4.0/src/regstack/cli/__main__.py +49 -0
  17. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/base.py +25 -27
  18. regstack-0.4.0/src/regstack/version.py +1 -0
  19. regstack-0.4.0/src/regstack/wizard/__init__.py +5 -0
  20. regstack-0.4.0/src/regstack/wizard/oauth_google/__init__.py +6 -0
  21. regstack-0.4.0/src/regstack/wizard/oauth_google/cli.py +224 -0
  22. regstack-0.4.0/src/regstack/wizard/oauth_google/routes.py +269 -0
  23. regstack-0.4.0/src/regstack/wizard/oauth_google/server.py +121 -0
  24. regstack-0.4.0/src/regstack/wizard/oauth_google/static/wizard.css +316 -0
  25. regstack-0.4.0/src/regstack/wizard/oauth_google/static/wizard.js +353 -0
  26. regstack-0.4.0/src/regstack/wizard/oauth_google/templates/wizard.html +260 -0
  27. regstack-0.4.0/src/regstack/wizard/oauth_google/validators.py +259 -0
  28. regstack-0.4.0/src/regstack/wizard/oauth_google/window.py +72 -0
  29. regstack-0.4.0/src/regstack/wizard/oauth_google/writer.py +248 -0
  30. {regstack-0.3.0 → regstack-0.4.0}/tasks.py +12 -1
  31. regstack-0.4.0/tests/e2e/conftest.py +82 -0
  32. regstack-0.4.0/tests/e2e/test_wizard_oauth_flow.py +144 -0
  33. regstack-0.4.0/tests/unit/__init__.py +0 -0
  34. regstack-0.4.0/tests/unit/test_wizard_oauth_cli.py +93 -0
  35. regstack-0.4.0/tests/unit/test_wizard_oauth_routes.py +232 -0
  36. regstack-0.4.0/tests/unit/test_wizard_oauth_validators.py +257 -0
  37. regstack-0.4.0/tests/unit/test_wizard_oauth_writer.py +293 -0
  38. {regstack-0.3.0 → regstack-0.4.0}/uv.lock +269 -1
  39. regstack-0.3.0/src/regstack/cli/__main__.py +0 -29
  40. regstack-0.3.0/src/regstack/version.py +0 -1
  41. {regstack-0.3.0 → regstack-0.4.0}/.github/workflows/publish.yml +0 -0
  42. {regstack-0.3.0 → regstack-0.4.0}/.github/workflows/test.yml +0 -0
  43. {regstack-0.3.0 → regstack-0.4.0}/.gitignore +0 -0
  44. {regstack-0.3.0 → regstack-0.4.0}/.python-version +0 -0
  45. {regstack-0.3.0 → regstack-0.4.0}/.readthedocs.yaml +0 -0
  46. {regstack-0.3.0 → regstack-0.4.0}/CHANGELOG.md +0 -0
  47. {regstack-0.3.0 → regstack-0.4.0}/LICENSE +0 -0
  48. {regstack-0.3.0 → regstack-0.4.0}/NOTICE +0 -0
  49. {regstack-0.3.0 → regstack-0.4.0}/SECURITY.md +0 -0
  50. {regstack-0.3.0 → regstack-0.4.0}/docs/_static/.gitkeep +0 -0
  51. {regstack-0.3.0 → regstack-0.4.0}/docs/_templates/.gitkeep +0 -0
  52. {regstack-0.3.0 → regstack-0.4.0}/docs/cli.md +0 -0
  53. {regstack-0.3.0 → regstack-0.4.0}/docs/quickstart.md +0 -0
  54. {regstack-0.3.0 → regstack-0.4.0}/docs/theming.md +0 -0
  55. {regstack-0.3.0 → regstack-0.4.0}/examples/_common/__init__.py +0 -0
  56. {regstack-0.3.0 → regstack-0.4.0}/examples/_common/app.py +0 -0
  57. {regstack-0.3.0 → regstack-0.4.0}/examples/mongo/README.md +0 -0
  58. {regstack-0.3.0 → regstack-0.4.0}/examples/mongo/branding/theme.css +0 -0
  59. {regstack-0.3.0 → regstack-0.4.0}/examples/mongo/main.py +0 -0
  60. {regstack-0.3.0 → regstack-0.4.0}/examples/mongo/regstack.toml +0 -0
  61. {regstack-0.3.0 → regstack-0.4.0}/examples/postgres/README.md +0 -0
  62. {regstack-0.3.0 → regstack-0.4.0}/examples/postgres/main.py +0 -0
  63. {regstack-0.3.0 → regstack-0.4.0}/examples/postgres/regstack.toml +0 -0
  64. {regstack-0.3.0 → regstack-0.4.0}/examples/sqlite/README.md +0 -0
  65. {regstack-0.3.0 → regstack-0.4.0}/examples/sqlite/main.py +0 -0
  66. {regstack-0.3.0 → regstack-0.4.0}/examples/sqlite/regstack.toml +0 -0
  67. {regstack-0.3.0 → regstack-0.4.0}/regstack.toml.example +0 -0
  68. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/__init__.py +0 -0
  69. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/app.py +0 -0
  70. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/__init__.py +0 -0
  71. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/clock.py +0 -0
  72. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/dependencies.py +0 -0
  73. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/jwt.py +0 -0
  74. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/lockout.py +0 -0
  75. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/mfa.py +0 -0
  76. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/password.py +0 -0
  77. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/auth/tokens.py +0 -0
  78. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/__init__.py +0 -0
  79. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/base.py +0 -0
  80. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/factory.py +0 -0
  81. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/__init__.py +0 -0
  82. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/backend.py +0 -0
  83. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/client.py +0 -0
  84. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/indexes.py +0 -0
  85. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
  86. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
  87. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
  88. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
  89. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +0 -0
  90. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +0 -0
  91. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
  92. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
  93. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/protocols.py +0 -0
  94. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/__init__.py +0 -0
  95. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/backend.py +0 -0
  96. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/migrations/__init__.py +0 -0
  97. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/migrations/env.py +0 -0
  98. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  99. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  100. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
  101. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/__init__.py +0 -0
  102. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  103. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  104. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
  105. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +0 -0
  106. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/oauth_state_repo.py +0 -0
  107. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
  108. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
  109. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/schema.py +0 -0
  110. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/backends/sql/types.py +0 -0
  111. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/__init__.py +0 -0
  112. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/_runtime.py +0 -0
  113. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/admin.py +0 -0
  114. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/doctor.py +0 -0
  115. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/init.py +0 -0
  116. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/cli/migrate.py +0 -0
  117. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/config/__init__.py +0 -0
  118. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/config/loader.py +0 -0
  119. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/config/schema.py +0 -0
  120. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/config/secrets.py +0 -0
  121. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/__init__.py +0 -0
  122. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/base.py +0 -0
  123. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/composer.py +0 -0
  124. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/console.py +0 -0
  125. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/factory.py +0 -0
  126. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/ses.py +0 -0
  127. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/smtp.py +0 -0
  128. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/email_change.html +0 -0
  129. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/email_change.subject.txt +0 -0
  130. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/email_change.txt +0 -0
  131. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/password_reset.html +0 -0
  132. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  133. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/password_reset.txt +0 -0
  134. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  135. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  136. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/verification.html +0 -0
  137. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/verification.subject.txt +0 -0
  138. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/email/templates/verification.txt +0 -0
  139. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/hooks/__init__.py +0 -0
  140. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/hooks/events.py +0 -0
  141. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/__init__.py +0 -0
  142. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/_objectid.py +0 -0
  143. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/login_attempt.py +0 -0
  144. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/mfa_code.py +0 -0
  145. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/oauth_identity.py +0 -0
  146. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/oauth_state.py +0 -0
  147. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/pending_registration.py +0 -0
  148. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/models/user.py +0 -0
  149. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/__init__.py +0 -0
  150. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/errors.py +0 -0
  151. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/providers/__init__.py +0 -0
  152. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/providers/google.py +0 -0
  153. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/oauth/registry.py +0 -0
  154. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/__init__.py +0 -0
  155. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/_schemas.py +0 -0
  156. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/account.py +0 -0
  157. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/admin.py +0 -0
  158. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/login.py +0 -0
  159. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/logout.py +0 -0
  160. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/oauth.py +0 -0
  161. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/password.py +0 -0
  162. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/phone.py +0 -0
  163. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/register.py +0 -0
  164. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/routers/verify.py +0 -0
  165. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/__init__.py +0 -0
  166. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/base.py +0 -0
  167. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/factory.py +0 -0
  168. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/null.py +0 -0
  169. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/sns.py +0 -0
  170. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/sms/twilio.py +0 -0
  171. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/__init__.py +0 -0
  172. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/pages.py +0 -0
  173. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/static/css/core.css +0 -0
  174. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/static/css/theme.css +0 -0
  175. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/static/js/regstack.js +0 -0
  176. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  177. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/forgot.html +0 -0
  178. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/login.html +0 -0
  179. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/me.html +0 -0
  180. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  181. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
  182. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/register.html +0 -0
  183. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/reset.html +0 -0
  184. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/auth/verify.html +0 -0
  185. {regstack-0.3.0 → regstack-0.4.0}/src/regstack/ui/templates/base.html +0 -0
  186. {regstack-0.3.0 → regstack-0.4.0}/tasks/oauth-design.md +0 -0
  187. {regstack-0.3.0 → regstack-0.4.0}/tests/__init__.py +0 -0
  188. {regstack-0.3.0 → regstack-0.4.0}/tests/_fake_google/__init__.py +0 -0
  189. {regstack-0.3.0 → regstack-0.4.0}/tests/_fake_google/provider.py +0 -0
  190. {regstack-0.3.0 → regstack-0.4.0}/tests/conftest.py +0 -0
  191. {regstack-0.3.0/tests/integration → regstack-0.4.0/tests/e2e}/__init__.py +0 -0
  192. {regstack-0.3.0/tests/unit → regstack-0.4.0/tests/integration}/__init__.py +0 -0
  193. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_account_management.py +0 -0
  194. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_admin_router.py +0 -0
  195. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_happy_path.py +0 -0
  196. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_indexes.py +0 -0
  197. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_login_lockout.py +0 -0
  198. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_mfa.py +0 -0
  199. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_oauth_google_router.py +0 -0
  200. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_oauth_repos.py +0 -0
  201. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_oauth_ui.py +0 -0
  202. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_password_reset.py +0 -0
  203. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_sql_migrations.py +0 -0
  204. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_ui_router.py +0 -0
  205. {regstack-0.3.0 → regstack-0.4.0}/tests/integration/test_verification.py +0 -0
  206. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_base_install_imports.py +0 -0
  207. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_cli.py +0 -0
  208. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_cli_doctor.py +0 -0
  209. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_cli_init.py +0 -0
  210. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_config_loader.py +0 -0
  211. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_jwt.py +0 -0
  212. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_lockout.py +0 -0
  213. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_mail_composer.py +0 -0
  214. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_mfa_code_repo.py +0 -0
  215. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_oauth_google.py +0 -0
  216. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_password.py +0 -0
  217. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_ses_backend.py +0 -0
  218. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_sms.py +0 -0
  219. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_smtp_backend.py +0 -0
  220. {regstack-0.3.0 → regstack-0.4.0}/tests/unit/test_ui_env.py +0 -0
@@ -60,8 +60,23 @@ The full plan, including milestone scope and deferred items, lives at
60
60
  Phone setup uses a separate signed `phone_setup` JWT carrying the
61
61
  proposed phone as a custom claim — same per-purpose key derivation as
62
62
  password-reset and email-change.
63
- - **OAuth**explicitly deferred. The `oauth/` package will hold a provider
64
- ABC only; concrete providers (Google first) come post-v1.
63
+ - **OAuth — done (0.3.0).** `OAuthProvider` ABC + `OAuthRegistry`,
64
+ Google provider (Authorization Code with PKCE, ID-token verification
65
+ via `pyjwt[crypto]` + `PyJWKClient`), 5 JSON endpoints,
66
+ `/account/oauth-complete` SSR token-handoff page, "Sign in with
67
+ Google" button on `/account/login`, Connected-accounts panel on
68
+ `/account/me`. Optional extra: `oauth = ["pyjwt[crypto]>=2.8"]`.
69
+ - **OAuth setup wizard — done.** `regstack oauth setup` opens a native
70
+ pywebview window over a 127.0.0.1 FastAPI server (random port +
71
+ one-shot launch token). 12-step SPA, per-step server validation,
72
+ non-clobbering tomlkit merge into `regstack.toml` +
73
+ `regstack.secrets.env`. Lives in `src/regstack/wizard/oauth_google/`;
74
+ Click subcommand registered through a `_LazyOauthGroup` so
75
+ `regstack init` / `doctor` don't pay the pywebview/uvicorn import
76
+ cost. `--print-only` mode runs the same merge headlessly. Tested at
77
+ four layers: validators (unit), writer (golden-file), routes
78
+ (TestClient), full SPA flow (Playwright e2e). Run e2e with
79
+ `inv test-e2e`; `inv test-all` chains it after the backend matrix.
65
80
 
66
81
  ## Three kinds of single-use proof
67
82
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regstack
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -29,7 +29,10 @@ Requires-Dist: pydantic-settings>=2.2
29
29
  Requires-Dist: pydantic>=2.6
30
30
  Requires-Dist: pyjwt>=2.8
31
31
  Requires-Dist: python-multipart>=0.0.9
32
+ Requires-Dist: pywebview>=5.0
32
33
  Requires-Dist: sqlalchemy[asyncio]>=2.0
34
+ Requires-Dist: tomlkit>=0.13
35
+ Requires-Dist: uvicorn[standard]>=0.29
33
36
  Provides-Extra: dev
34
37
  Requires-Dist: anyio>=4.3; extra == 'dev'
35
38
  Requires-Dist: asyncpg>=0.29; extra == 'dev'
@@ -40,6 +43,7 @@ Requires-Dist: pyjwt[crypto]>=2.8; extra == 'dev'
40
43
  Requires-Dist: pymongo>=4.9; extra == 'dev'
41
44
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
42
45
  Requires-Dist: pytest-cov>=5.0; extra == 'dev'
46
+ Requires-Dist: pytest-playwright>=0.5; extra == 'dev'
43
47
  Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
44
48
  Requires-Dist: pytest>=8.0; extra == 'dev'
45
49
  Requires-Dist: ruff>=0.4; extra == 'dev'
@@ -78,9 +82,9 @@ auth bugs.**
78
82
 
79
83
  `pip install regstack`, point it at SQLite (default), [PostgreSQL](https://www.postgresql.org/),
80
84
  or [MongoDB](https://www.mongodb.com/), and you have register / login /
81
- verify-email / reset-password / change-email / delete-account / optional SMS
82
- two-factor / admin endpoints / themable HTML pages — all behind a small Python
83
- API and one config file.
85
+ verify-email / reset-password / change-email / delete-account / Sign in
86
+ with Google / optional SMS two-factor / admin endpoints / themable
87
+ HTML pages — all behind a small Python API and one config file.
84
88
 
85
89
  📚 **Docs:** <https://regstack.readthedocs.io>
86
90
  &nbsp;·&nbsp;
@@ -132,6 +136,7 @@ result everywhere is what regstack is for.
132
136
  ✔ Forgot / reset password — anti-enumeration: identical responses
133
137
  ✔ Change password (revokes old tokens) / change email (re-verify)
134
138
  ✔ Delete account
139
+ ✔ Sign in with Google (PKCE + ID-token verification, opt-in)
135
140
  ✔ Optional SMS two-factor (TOTP-style 6-digit codes over SMS)
136
141
  ✔ Server-side login lockout (HTTP 429 + Retry-After)
137
142
  ✔ Admin endpoints (list / disable / delete users, stats)
@@ -245,7 +250,9 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
245
250
 
246
251
  Alpha. Single-file SQLite is the default and runs with no infrastructure;
247
252
  PostgreSQL and MongoDB backends pass the same parametrized integration
248
- suite. Latest tagged release: `v0.2.1`. See the
253
+ 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
249
256
  [changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
250
257
  for the per-release breakdown.
251
258
 
@@ -11,9 +11,9 @@ auth bugs.**
11
11
 
12
12
  `pip install regstack`, point it at SQLite (default), [PostgreSQL](https://www.postgresql.org/),
13
13
  or [MongoDB](https://www.mongodb.com/), and you have register / login /
14
- verify-email / reset-password / change-email / delete-account / optional SMS
15
- two-factor / admin endpoints / themable HTML pages — all behind a small Python
16
- API and one config file.
14
+ verify-email / reset-password / change-email / delete-account / Sign in
15
+ with Google / optional SMS two-factor / admin endpoints / themable
16
+ HTML pages — all behind a small Python API and one config file.
17
17
 
18
18
  📚 **Docs:** <https://regstack.readthedocs.io>
19
19
  &nbsp;·&nbsp;
@@ -65,6 +65,7 @@ result everywhere is what regstack is for.
65
65
  ✔ Forgot / reset password — anti-enumeration: identical responses
66
66
  ✔ Change password (revokes old tokens) / change email (re-verify)
67
67
  ✔ Delete account
68
+ ✔ Sign in with Google (PKCE + ID-token verification, opt-in)
68
69
  ✔ Optional SMS two-factor (TOTP-style 6-digit codes over SMS)
69
70
  ✔ Server-side login lockout (HTTP 429 + Retry-After)
70
71
  ✔ Admin endpoints (list / disable / delete users, stats)
@@ -178,7 +179,9 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
178
179
 
179
180
  Alpha. Single-file SQLite is the default and runs with no infrastructure;
180
181
  PostgreSQL and MongoDB backends pass the same parametrized integration
181
- suite. Latest tagged release: `v0.2.1`. See the
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
182
185
  [changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
183
186
  for the per-release breakdown.
184
187
 
@@ -18,6 +18,7 @@ The handful of things you import from `regstack` directly:
18
18
  - [`RegStackConfig`](#regstack.config.schema.RegStackConfig) — top-level config.
19
19
  - [`EmailConfig`](#regstack.config.schema.EmailConfig) — email-backend sub-config.
20
20
  - [`SmsConfig`](#regstack.config.schema.SmsConfig) — SMS-backend sub-config.
21
+ - [`OAuthConfig`](#regstack.config.schema.OAuthConfig) — OAuth provider sub-config.
21
22
 
22
23
  Most embeddings need only `RegStack` and `RegStackConfig`.
23
24
 
@@ -59,6 +60,11 @@ default.
59
60
  :show-inheritance:
60
61
  :exclude-members: model_config, model_fields, model_computed_fields
61
62
 
63
+ .. autoclass:: regstack.config.schema.OAuthConfig
64
+ :members:
65
+ :show-inheritance:
66
+ :exclude-members: model_config, model_fields, model_computed_fields
67
+
62
68
  .. autofunction:: regstack.config.loader.load_config
63
69
  ```
64
70
 
@@ -247,6 +253,73 @@ MessageBird, …) and pass the instance to `regstack.set_email_backend`
247
253
  .. autofunction:: regstack.sms.factory.build_sms_service
248
254
  ```
249
255
 
256
+ ## OAuth
257
+
258
+ Opt-in subsystem behind `enable_oauth` and the `oauth` extra. v1
259
+ ships Google; the abstraction is shaped so adding GitHub /
260
+ Microsoft / Apple later is a new module under
261
+ `regstack.oauth.providers` plus a registry entry. The full host
262
+ guide is in [OAuth](oauth.md); the threat model is in
263
+ [Security model](security.md#oauth-sign-in-with-google).
264
+
265
+ ### Provider abstraction
266
+
267
+ ```{eval-rst}
268
+ .. autoclass:: regstack.oauth.base.OAuthProvider
269
+ :members:
270
+
271
+ .. autoclass:: regstack.oauth.base.OAuthTokens
272
+ :members:
273
+
274
+ .. autoclass:: regstack.oauth.base.OAuthUserInfo
275
+ :members:
276
+
277
+ .. autoclass:: regstack.oauth.registry.OAuthRegistry
278
+ :members:
279
+
280
+ .. autoexception:: regstack.oauth.errors.OAuthError
281
+
282
+ .. autoexception:: regstack.oauth.errors.OAuthConfigError
283
+
284
+ .. autoexception:: regstack.oauth.errors.OAuthTokenExchangeError
285
+
286
+ .. autoexception:: regstack.oauth.errors.OAuthIdTokenError
287
+ ```
288
+
289
+ ### Google provider
290
+
291
+ ```{eval-rst}
292
+ .. autoclass:: regstack.oauth.providers.google.GoogleProvider
293
+ :members:
294
+ :show-inheritance:
295
+ ```
296
+
297
+ ### Identity + state storage
298
+
299
+ ```{eval-rst}
300
+ .. autoclass:: regstack.models.oauth_identity.OAuthIdentity
301
+ :members:
302
+ :exclude-members: model_config, model_fields, model_computed_fields
303
+
304
+ .. autoclass:: regstack.models.oauth_state.OAuthState
305
+ :members:
306
+ :exclude-members: model_config, model_fields, model_computed_fields
307
+
308
+ .. autoclass:: regstack.backends.protocols.OAuthIdentityRepoProtocol
309
+ :members:
310
+
311
+ .. autoclass:: regstack.backends.protocols.OAuthStateRepoProtocol
312
+ :members:
313
+
314
+ .. autoexception:: regstack.backends.protocols.OAuthIdentityAlreadyLinkedError
315
+ ```
316
+
317
+ ### Router
318
+
319
+ ```{eval-rst}
320
+ .. autofunction:: regstack.routers.oauth.build_oauth_router
321
+ ```
322
+
250
323
  ## Hooks
251
324
 
252
325
  The event bus regstack uses to fire side-effect notifications
@@ -139,8 +139,52 @@ The composite `router` conditionally includes:
139
139
  - `password` (forgot/reset) — when `enable_password_reset`.
140
140
  - `phone` and the `mfa-confirm` route — when `enable_sms_2fa`.
141
141
  - `admin` — when `enable_admin_router`.
142
-
143
- `ui_router` mounts the same conditional pages.
142
+ - `oauth` — when `enable_oauth` AND at least one provider is
143
+ registered on `rs.oauth`.
144
+
145
+ `ui_router` mounts the same conditional pages, plus
146
+ `/account/oauth-complete` when `enable_oauth` is on.
147
+
148
+ ## OAuth subsystem
149
+
150
+ Opt-in. Lives in `regstack.oauth/`; hosts pull it in via the
151
+ `oauth` extra (`pyjwt[crypto]>=2.8`). Imports are lazy — the
152
+ package keeps importing on a base install with no `cryptography`
153
+ installed, and the OAuth-specific modules only get loaded when
154
+ `enable_oauth` is on.
155
+
156
+ The shape is:
157
+
158
+ - `OAuthProvider` ABC — three methods: `authorization_url`,
159
+ `exchange_code`, `verify_id_token`.
160
+ - `OAuthRegistry` — name-keyed map of providers, scoped to one
161
+ `RegStack` instance. The `RegStack` constructor reads
162
+ `config.oauth` and registers `GoogleProvider` automatically when
163
+ `enable_oauth` and the credentials are set; hosts can also
164
+ register custom providers post-construction.
165
+ - `GoogleProvider` — Authorization Code with PKCE, ID-token
166
+ verification via `pyjwt[crypto]` + `PyJWKClient` against Google's
167
+ JWKS. ~150 lines hand-rolled rather than pulling `authlib`.
168
+ - Two new repos via the protocol pattern:
169
+ `OAuthIdentityRepoProtocol` (links between regstack users and
170
+ external accounts; double-unique on `(provider, subject_id)` and
171
+ `(user_id, provider)`) and `OAuthStateRepoProtocol` (in-flight
172
+ state rows carrying the PKCE `code_verifier`, redirect target,
173
+ mode, and the `result_token` slot the SPA exchanges).
174
+ - `build_oauth_router(rs)` — the router with the five endpoints
175
+ (`/start`, `/callback`, `/exchange`, `/link/start`, `/link`) plus
176
+ `/oauth/providers` for the SSR connected-accounts panel.
177
+
178
+ The token-handoff round-trip avoids putting access tokens in URLs:
179
+ the callback stashes the freshly-minted session JWT on the state
180
+ row's `result_token`, redirects to `/account/oauth-complete?id=…`,
181
+ and the SPA POSTs that id back to `/oauth/exchange` to retrieve the
182
+ token. The exchange consumes the row atomically — the same id can't
183
+ be exchanged twice.
184
+
185
+ The full design (including the four-milestone build sequence and
186
+ the threat model) is in
187
+ [`tasks/oauth-design.md`](https://github.com/jdrumgoole/regstack/blob/main/tasks/oauth-design.md).
144
188
 
145
189
  ## Hooks
146
190
 
@@ -157,6 +201,8 @@ primary auth flow. Known events:
157
201
  - `phone_setup_started` / `mfa_login_started`
158
202
  - `mfa_enabled` / `mfa_disabled`
159
203
  - `user_deleted`
204
+ - `oauth_signin_started` / `oauth_signin_completed`
205
+ - `oauth_account_linked` / `oauth_account_unlinked`
160
206
 
161
207
  Hosts are free to subscribe to custom event names too — the registry
162
208
  is just a `defaultdict(list)`. Use this surface to push events into
@@ -3,6 +3,24 @@
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.4.0 — 2026-05-02
7
+
8
+ ### Added
9
+
10
+ - **OAuth setup wizard.** `regstack oauth setup` opens a native
11
+ webview window that walks an operator through registering a Google
12
+ OAuth 2.0 client and merges the credentials into `regstack.toml` +
13
+ `regstack.secrets.env` non-destructively (preserves comments, other
14
+ tables, unrelated keys). 12-step SPA inside a local-only
15
+ 127.0.0.1 FastAPI server, gated by a per-launch random token. Each
16
+ Next click hits a server-side validator so the Write step can never
17
+ be reached with bad data. `--print-only` mode skips the GUI for
18
+ headless / CI use.
19
+ - Three new base dependencies: `pywebview>=5.0`, `tomlkit>=0.13`,
20
+ `uvicorn[standard]>=0.29` (the wizard's local server).
21
+ - `pytest-playwright` added to the `dev` extra; new `inv test-e2e`
22
+ task chained into `inv test-all`.
23
+
6
24
  ## 0.3.0 — 2026-04-30
7
25
 
8
26
  **OAuth — Sign in with Google.** Built across four PRs (M1–M4 of
@@ -38,7 +38,14 @@ extensions = [
38
38
  source_suffix = {".md": "markdown", ".rst": "restructuredtext"}
39
39
  master_doc = "index"
40
40
  templates_path = ["_templates"]
41
- exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
41
+ exclude_patterns = [
42
+ "_build",
43
+ "Thumbs.db",
44
+ ".DS_Store",
45
+ # Daily security review reports — these are GitHub-rendered
46
+ # markdown, not part of the published Sphinx site.
47
+ "security-reports/**",
48
+ ]
42
49
 
43
50
  # Suppress noisy autodoc warnings on dynamically-typed pydantic helpers.
44
51
  suppress_warnings = ["autodoc.import_object"]
@@ -63,6 +63,12 @@ addressed in env using a `__` separator: `REGSTACK_EMAIL__FROM_ADDRESS`.
63
63
  * - `mfa_code_collection`
64
64
  - `"mfa_codes"`
65
65
  -
66
+ * - `oauth_identity_collection`
67
+ - `"oauth_identities"`
68
+ -
69
+ * - `oauth_state_collection`
70
+ - `"oauth_states"`
71
+ -
66
72
  ```
67
73
 
68
74
  ## Backends
@@ -165,7 +171,9 @@ The active backend exposes the same five repository protocols on
165
171
  - Mounts `/phone/*` routes and gates the MFA second step in `/login`.
166
172
  * - `enable_oauth`
167
173
  - `false`
168
- - Reserved. No providers ship in v1.
174
+ - Mounts `/oauth/*` routes when at least one provider is registered
175
+ (currently Google). Requires the ``oauth`` extra
176
+ (``pip install 'regstack[oauth]'``).
169
177
  ```
170
178
 
171
179
  ## Lockout (login)
@@ -256,6 +264,31 @@ twilio_account_sid = "AC…"
256
264
  # twilio_auth_token via REGSTACK_SMS__TWILIO_AUTH_TOKEN
257
265
  ```
258
266
 
267
+ `[oauth]` (`OAuthConfig`):
268
+
269
+ ```toml
270
+ [oauth]
271
+ google_client_id = "12345.apps.googleusercontent.com"
272
+ # google_client_secret via REGSTACK_OAUTH__GOOGLE_CLIENT_SECRET
273
+ # google_redirect_uri = "https://your.app/api/auth/oauth/google/callback"
274
+ # (default: f"{base_url}{api_prefix}/oauth/google/callback")
275
+
276
+ # Account-linking policy. Off by default — see docs/oauth.md and
277
+ # docs/security.md for the threat model. On = a Google sign-in for an
278
+ # existing email-registered user is auto-linked when Google's
279
+ # email_verified=true. Hosts choosing on are accepting the email-
280
+ # recycling-at-the-provider risk in exchange for less friction.
281
+ auto_link_verified_emails = false
282
+
283
+ # When true, an OAuth sign-in for a user with SMS MFA enabled still
284
+ # goes through the second-factor step. Off by default — the OAuth
285
+ # provider already authenticated the human.
286
+ enforce_mfa_on_oauth_signin = false
287
+
288
+ state_ttl_seconds = 300 # in-flight state row lifetime
289
+ completion_ttl_seconds = 30 # /oauth/exchange window after callback
290
+ ```
291
+
259
292
  ## SSR / theming
260
293
 
261
294
  ```{list-table}
@@ -54,11 +54,22 @@ async def _send_to_crm(user) -> None:
54
54
  @regstack.on("user_deleted")
55
55
  async def _purge_host_data(user) -> None:
56
56
  await my_app.delete_all_data_for(user.id)
57
+
58
+
59
+ @regstack.on("oauth_signin_completed")
60
+ async def _track_signin(*, user, provider, mode, was_new) -> None:
61
+ if was_new:
62
+ await analytics.track("signup", {"user": user.id, "provider": provider})
63
+ else:
64
+ await analytics.track("login", {"user": user.id, "provider": provider})
57
65
  ```
58
66
 
59
67
  Handlers can be sync or async. Exceptions are logged but never break
60
68
  the primary auth flow — see [`HookRegistry`](architecture.md#hooks).
61
- The full event list is in the architecture guide.
69
+ The full event list (including the four OAuth events:
70
+ ``oauth_signin_started``, ``oauth_signin_completed``,
71
+ ``oauth_account_linked``, ``oauth_account_unlinked``) is in the
72
+ architecture guide.
62
73
 
63
74
  ## Custom email or SMS backends
64
75
 
@@ -101,6 +112,48 @@ regstack.set_email_backend(PostmarkEmailService(server_token=os.environ["POSTMAR
101
112
  The same pattern applies for SMS via `SmsService` and
102
113
  `set_sms_backend(...)`.
103
114
 
115
+ ## Enabling OAuth
116
+
117
+ Install the extra and configure a provider:
118
+
119
+ ```bash
120
+ uv add 'regstack[oauth]'
121
+ ```
122
+
123
+ ```toml
124
+ # regstack.toml
125
+ enable_oauth = true
126
+
127
+ [oauth]
128
+ google_client_id = "12345.apps.googleusercontent.com"
129
+ # google_client_secret in regstack.secrets.env
130
+ auto_link_verified_emails = false # security default — see oauth.md
131
+ ```
132
+
133
+ The router mounts five JSON endpoints under `/oauth/` (start /
134
+ callback / exchange / link-start / unlink) plus a `/oauth/providers`
135
+ list. The bundled SSR pages pick up the rest automatically: a
136
+ "Sign in with Google" button on `/account/login` and a Connected-
137
+ accounts panel on `/account/me`.
138
+
139
+ Hosts that need a custom provider (Apple, Microsoft, an internal
140
+ OIDC) can register one programmatically on the registry:
141
+
142
+ ```python
143
+ rs.oauth.register(MyCustomProvider(...))
144
+ ```
145
+
146
+ Anything implementing :class:`~regstack.oauth.base.OAuthProvider`
147
+ works — three abstract methods (`authorization_url`,
148
+ `exchange_code`, `verify_id_token`). The router parametrizes its
149
+ URL paths on the provider name, so a registered provider named
150
+ ``"github"`` is reachable at `/oauth/github/start` without any
151
+ router changes.
152
+
153
+ The full host guide — Google client setup, the linking-policy
154
+ decision, OAuth-only-user knock-on effects — is in
155
+ [OAuth](oauth.md).
156
+
104
157
  ## Overriding email and HTML templates
105
158
 
106
159
  Both surfaces share `RegStack.add_template_dir(path)`:
@@ -193,5 +246,7 @@ for production probes that need more than a TCP hit.
193
246
  - It does not enforce HTTPS. Run behind a TLS terminator.
194
247
  - It does not provision SES identities, Route 53 records, IAM users,
195
248
  or anything else outside the database.
196
- - It does not ship OAuth providers in v1 `oauth/` reserves the
197
- abstraction surface only, for a future milestone.
249
+ - It does not ship OAuth providers other than Google. The
250
+ abstraction is shaped to take GitHub / Microsoft / Apple later;
251
+ hosts that want a different provider today can implement
252
+ :class:`~regstack.oauth.base.OAuthProvider` and register it.
@@ -78,6 +78,10 @@ each time".
78
78
  Full template overrides are still possible per host.
79
79
  - **CLIs.** `regstack init` (interactive setup wizard),
80
80
  `regstack create-admin`, `regstack doctor`.
81
+ - **OAuth — Sign in with Google** (opt-in, since 0.3.0). Authorization
82
+ Code with PKCE, ID-token verification, identity-linking with a
83
+ default-refuse policy hosts can opt out of. Connected-accounts
84
+ panel on the SSR `/account/me` page. See [the OAuth guide](oauth.md).
81
85
  - **Pluggable email and SMS.** Email backends: `console` (dev), SMTP,
82
86
  [Amazon SES](https://aws.amazon.com/ses/). SMS backends:
83
87
  [Amazon SNS](https://aws.amazon.com/sns/),
@@ -135,7 +139,7 @@ changelog
135
139
 
136
140
  ## Project status
137
141
 
138
- Alpha. Latest tagged release: `v0.2.1`. SQLite is the default and
142
+ Alpha. Latest tagged release: `v0.4.0`. SQLite is the default and
139
143
  runs with no infrastructure; PostgreSQL and MongoDB pass the same
140
144
  parametrized integration suite. The full backend matrix runs in
141
145
  parallel against every test, so a green CI on `main` is a strong
@@ -53,6 +53,25 @@ In the [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
53
53
 
54
54
  ## Configure regstack
55
55
 
56
+ The fastest path is the **OAuth setup wizard**, which opens a native
57
+ window and walks you through every step (registering the GCP client,
58
+ pasting the redirect URI, picking your linking policy) and finally
59
+ merges the credentials into your existing `regstack.toml` and
60
+ `regstack.secrets.env` without disturbing other settings:
61
+
62
+ ```bash
63
+ uv run regstack oauth setup
64
+ ```
65
+
66
+ The wizard is **non-clobbering** — it preserves comments, unrelated
67
+ top-level keys, and unrelated tables (`[email]`, `[sms]`, etc.). Re-run
68
+ it any time you need to rotate credentials or change the linking
69
+ policy. On a headless host (CI, server) use
70
+ `regstack oauth setup --print-only --client-id=… --client-secret=…`
71
+ to get the same merge with no GUI.
72
+
73
+ If you'd rather edit by hand, the resulting files look like:
74
+
56
75
  ```toml
57
76
  # regstack.toml
58
77
  enable_oauth = true
@@ -0,0 +1,15 @@
1
+ # Security reports
2
+
3
+ Daily output from the scheduled security-review agent (see
4
+ [`scripts/security-review-prompt.md`](../../scripts/security-review-prompt.md)).
5
+
6
+ Each file is named `YYYY-MM-DD.md` and follows the structure declared
7
+ in the prompt: 🔴 CRITICAL / 🟠 WARNING / 🟡 INFO / 🟢 CLEAN findings,
8
+ plus a summary block.
9
+
10
+ The agent files a PR per report so each review is reviewable as a
11
+ diff against `main`. PR title prefix:
12
+
13
+ - `[security-critical]` — at least one CRITICAL finding.
14
+ - `[security-warning]` — at least one WARNING finding (no CRITICALs).
15
+ - `[security-clean]` — clean bill of health.
@@ -161,6 +161,77 @@ visible to logged-out users only.
161
161
  instead of a session token, sends an SMS, and requires
162
162
  `POST /login/mfa-confirm` to complete.
163
163
 
164
+ ## OAuth (Sign in with Google)
165
+
166
+ Opt-in subsystem behind `enable_oauth` and the `oauth` extra. Five
167
+ JSON endpoints plus an SSR token-handoff page. The full host-facing
168
+ guide is in [OAuth](oauth.md); this section is the threat model.
169
+
170
+ - **Server-side PKCE.** The `code_verifier` is generated server-side
171
+ and persisted on a `oauth_states` row; only its SHA-256
172
+ `code_challenge` ever travels through the browser. The token
173
+ exchange POSTs the verifier directly from the regstack server to
174
+ Google's token endpoint, so a leaked browser-side state value
175
+ alone can't drive a token exchange.
176
+ - **State row is the OAuth `state` parameter.** Random 32-byte
177
+ url-safe id; carries `code_verifier`, `nonce`, `redirect_to`,
178
+ `mode` (`signin` or `link`), optional `linking_user_id`. The
179
+ callback looks the row up by id, rejects missing / expired rows
180
+ with `?error=bad_state` or `?error=state_expired`. Mongo gets free
181
+ TTL via `expireAfterSeconds`; SQL backends rely on read-side
182
+ `expires_at > now()` plus `purge_expired()`.
183
+ - **ID token verification.** Signature against Google's JWKS
184
+ (`PyJWKClient` cached), `iss` matches Google, `aud` matches the
185
+ configured `client_id`, `exp > now`, `nonce` matches the value
186
+ stashed on the state row. Any failure raises
187
+ `OAuthIdTokenError` and the callback redirects to the login page
188
+ with `?error=id_token_failed` — the specific check that failed is
189
+ logged but not echoed.
190
+ - **Account-linking policy.** Defaults to **refuse**. If a Google
191
+ sign-in carries an email already owned by a regstack user, the
192
+ callback returns `?error=email_in_use` and the user has to sign
193
+ in with their existing password before linking from
194
+ `/account/me`. Auto-linking is available behind
195
+ `oauth.auto_link_verified_emails = true`; even then, regstack
196
+ requires `email_verified=true` on the ID token. The threat
197
+ auto-link accepts is *email recycling at the provider* — if
198
+ someone later acquires the original Gmail address, they could
199
+ sign in as the original regstack user. Hosts choosing auto-link
200
+ do so eyes-open. Full writeup in
201
+ `tasks/oauth-design.md` § 1.
202
+ - **One-time token-handoff.** After a successful callback, the
203
+ fresh session JWT is stashed on the `oauth_states.result_token`
204
+ field and the SPA exchanges its state-id for the token via
205
+ `POST /oauth/exchange`. The exchange consumes the row atomically
206
+ (read + delete in one transaction); a second exchange call with
207
+ the same id returns 404. Tokens never appear in URLs longer than
208
+ the callback redirect, no cookies are set.
209
+ - **OAuth-issued sessions are normal session JWTs** signed with the
210
+ same `session`-purpose key. The `tokens_invalidated_after` bulk-
211
+ revoke applies — a password change or admin-disable kills any
212
+ OAuth-issued session too.
213
+ - **Open-redirect protection.** `redirect_to` on `/start` is
214
+ validated same-origin against `config.base_url`; a request with
215
+ an off-site target returns 400.
216
+ - **Identity-row uniqueness.** `(provider, subject_id)` is unique
217
+ so two regstack users can't share one external account; a
218
+ second-user link attempt returns `?error=identity_in_use`.
219
+ `(user_id, provider)` is also unique so re-linking the same
220
+ provider to the same user returns `?error=already_linked`
221
+ rather than silently succeeding.
222
+ - **OAuth-only users.** A Google sign-up creates a user with
223
+ `hashed_password=None`. Login with a password against such an
224
+ account returns the same generic 401 a wrong-password attempt
225
+ gets — never reveal that an account exists but has no password
226
+ set, so an attacker can't enumerate which accounts to phish via
227
+ OAuth. `change-password` / `change-email` / `delete-account`
228
+ return 400 with a pointer at the password-reset flow, which
229
+ doubles as a "set initial password" path.
230
+ - **Refuse to unlink the only auth method.**
231
+ `DELETE /oauth/{provider}/link` returns 400 if the user has no
232
+ password and only the one identity. Forces them to either set a
233
+ password (via reset) or link another provider first.
234
+
164
235
  ## CSP and the SSR layer
165
236
 
166
237
  Content Security Policy (CSP) is a browser feature that restricts
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regstack"
3
- version = "0.3.0"
3
+ version = "0.4.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"
@@ -32,6 +32,11 @@ dependencies = [
32
32
  "sqlalchemy[asyncio]>=2.0",
33
33
  "aiosqlite>=0.20",
34
34
  "alembic>=1.13",
35
+ # OAuth setup wizard (regstack oauth setup) — needs a webview shell,
36
+ # a small in-process FastAPI server, and round-trip-safe TOML editing.
37
+ "pywebview>=5.0",
38
+ "tomlkit>=0.13",
39
+ "uvicorn[standard]>=0.29",
35
40
  ]
36
41
 
37
42
  [project.optional-dependencies]
@@ -65,6 +70,8 @@ dev = [
65
70
  "asyncpg>=0.29",
66
71
  # OAuth provider tests need the crypto bits to verify ID tokens.
67
72
  "pyjwt[crypto]>=2.8",
73
+ # E2E tests for the OAuth setup wizard (drives the SPA in headless Chromium).
74
+ "pytest-playwright>=0.5",
68
75
  ]
69
76
 
70
77
  [project.scripts]
@@ -98,11 +105,18 @@ markers = [
98
105
  filterwarnings = [
99
106
  "error",
100
107
  "ignore::DeprecationWarning:passlib.*",
108
+ # uvicorn[standard] still imports websockets.legacy on shutdown,
109
+ # which fires a DeprecationWarning. The wizard's e2e fixture runs
110
+ # uvicorn in a thread and pytest's threadexception plugin promotes
111
+ # thread warnings to errors — silence at the source.
112
+ "ignore::DeprecationWarning:websockets.*",
113
+ "ignore::DeprecationWarning:uvicorn.*",
101
114
  # CI surfaces PytestUnraisableExceptionWarning when async fixtures
102
115
  # from a previous test leak sockets/event loops past their cleanup
103
116
  # window — the sync UI tests shouldn't fail just because asyncpg or
104
117
  # aiosqlite teardown was sloppy in a sibling test.
105
118
  "ignore::pytest.PytestUnraisableExceptionWarning",
119
+ "ignore::pytest.PytestUnhandledThreadExceptionWarning",
106
120
  ]
107
121
 
108
122
  [tool.ruff]