regstack 0.2.6__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 (222) hide show
  1. {regstack-0.2.6 → regstack-0.4.0}/CHANGELOG.md +34 -0
  2. {regstack-0.2.6 → regstack-0.4.0}/CLAUDE.md +17 -2
  3. {regstack-0.2.6 → regstack-0.4.0}/PKG-INFO +15 -5
  4. {regstack-0.2.6 → regstack-0.4.0}/README.md +7 -4
  5. {regstack-0.2.6 → regstack-0.4.0}/docs/api.md +73 -0
  6. {regstack-0.2.6 → regstack-0.4.0}/docs/architecture.md +48 -2
  7. {regstack-0.2.6 → regstack-0.4.0}/docs/changelog.md +101 -0
  8. {regstack-0.2.6 → regstack-0.4.0}/docs/conf.py +8 -1
  9. {regstack-0.2.6 → regstack-0.4.0}/docs/configuration.md +34 -1
  10. {regstack-0.2.6 → regstack-0.4.0}/docs/embedding.md +58 -3
  11. {regstack-0.2.6 → regstack-0.4.0}/docs/index.md +6 -1
  12. regstack-0.4.0/docs/oauth.md +154 -0
  13. regstack-0.4.0/docs/security-reports/README.md +15 -0
  14. {regstack-0.2.6 → regstack-0.4.0}/docs/security.md +71 -0
  15. {regstack-0.2.6 → regstack-0.4.0}/pyproject.toml +18 -1
  16. regstack-0.4.0/scripts/security-review-prompt.md +583 -0
  17. regstack-0.4.0/src/regstack/__init__.py +12 -0
  18. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/app.py +29 -0
  19. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/base.py +4 -0
  20. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/backend.py +6 -0
  21. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/indexes.py +27 -0
  22. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/__init__.py +6 -0
  23. regstack-0.4.0/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +63 -0
  24. regstack-0.4.0/src/regstack/backends/mongo/repositories/oauth_state_repo.py +45 -0
  25. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/protocols.py +89 -0
  26. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/backend.py +6 -0
  27. regstack-0.4.0/src/regstack/backends/sql/migrations/versions/0002_oauth.py +91 -0
  28. regstack-0.4.0/src/regstack/backends/sql/repositories/oauth_identity_repo.py +87 -0
  29. regstack-0.4.0/src/regstack/backends/sql/repositories/oauth_state_repo.py +66 -0
  30. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/schema.py +47 -1
  31. regstack-0.4.0/src/regstack/cli/__main__.py +49 -0
  32. regstack-0.4.0/src/regstack/config/__init__.py +4 -0
  33. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/config/schema.py +37 -0
  34. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/hooks/events.py +4 -0
  35. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/__init__.py +5 -0
  36. regstack-0.4.0/src/regstack/models/oauth_identity.py +55 -0
  37. regstack-0.4.0/src/regstack/models/oauth_state.py +82 -0
  38. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/user.py +5 -1
  39. regstack-0.4.0/src/regstack/oauth/__init__.py +42 -0
  40. regstack-0.4.0/src/regstack/oauth/base.py +186 -0
  41. regstack-0.4.0/src/regstack/oauth/errors.py +43 -0
  42. regstack-0.4.0/src/regstack/oauth/providers/__init__.py +6 -0
  43. regstack-0.4.0/src/regstack/oauth/providers/google.py +200 -0
  44. regstack-0.4.0/src/regstack/oauth/registry.py +74 -0
  45. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/__init__.py +7 -0
  46. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/account.py +28 -0
  47. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/login.py +7 -1
  48. regstack-0.4.0/src/regstack/routers/oauth.py +536 -0
  49. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/pages.py +12 -0
  50. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/static/js/regstack.js +133 -0
  51. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/login.html +14 -0
  52. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/me.html +8 -0
  53. regstack-0.4.0/src/regstack/ui/templates/auth/oauth_complete.html +14 -0
  54. regstack-0.4.0/src/regstack/version.py +1 -0
  55. regstack-0.4.0/src/regstack/wizard/__init__.py +5 -0
  56. regstack-0.4.0/src/regstack/wizard/oauth_google/__init__.py +6 -0
  57. regstack-0.4.0/src/regstack/wizard/oauth_google/cli.py +224 -0
  58. regstack-0.4.0/src/regstack/wizard/oauth_google/routes.py +269 -0
  59. regstack-0.4.0/src/regstack/wizard/oauth_google/server.py +121 -0
  60. regstack-0.4.0/src/regstack/wizard/oauth_google/static/wizard.css +316 -0
  61. regstack-0.4.0/src/regstack/wizard/oauth_google/static/wizard.js +353 -0
  62. regstack-0.4.0/src/regstack/wizard/oauth_google/templates/wizard.html +260 -0
  63. regstack-0.4.0/src/regstack/wizard/oauth_google/validators.py +259 -0
  64. regstack-0.4.0/src/regstack/wizard/oauth_google/window.py +72 -0
  65. regstack-0.4.0/src/regstack/wizard/oauth_google/writer.py +248 -0
  66. regstack-0.4.0/tasks/oauth-design.md +729 -0
  67. {regstack-0.2.6 → regstack-0.4.0}/tasks.py +12 -1
  68. regstack-0.4.0/tests/_fake_google/__init__.py +14 -0
  69. regstack-0.4.0/tests/_fake_google/provider.py +166 -0
  70. regstack-0.4.0/tests/e2e/conftest.py +82 -0
  71. regstack-0.4.0/tests/e2e/test_wizard_oauth_flow.py +144 -0
  72. regstack-0.4.0/tests/integration/test_oauth_google_router.py +583 -0
  73. regstack-0.4.0/tests/integration/test_oauth_repos.py +267 -0
  74. regstack-0.4.0/tests/integration/test_oauth_ui.py +168 -0
  75. regstack-0.4.0/tests/unit/__init__.py +0 -0
  76. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_base_install_imports.py +22 -12
  77. regstack-0.4.0/tests/unit/test_oauth_google.py +445 -0
  78. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_ui_env.py +1 -0
  79. regstack-0.4.0/tests/unit/test_wizard_oauth_cli.py +93 -0
  80. regstack-0.4.0/tests/unit/test_wizard_oauth_routes.py +232 -0
  81. regstack-0.4.0/tests/unit/test_wizard_oauth_validators.py +257 -0
  82. regstack-0.4.0/tests/unit/test_wizard_oauth_writer.py +293 -0
  83. {regstack-0.2.6 → regstack-0.4.0}/uv.lock +340 -2
  84. regstack-0.2.6/src/regstack/__init__.py +0 -5
  85. regstack-0.2.6/src/regstack/cli/__main__.py +0 -29
  86. regstack-0.2.6/src/regstack/config/__init__.py +0 -4
  87. regstack-0.2.6/src/regstack/version.py +0 -1
  88. {regstack-0.2.6 → regstack-0.4.0}/.github/workflows/publish.yml +0 -0
  89. {regstack-0.2.6 → regstack-0.4.0}/.github/workflows/test.yml +0 -0
  90. {regstack-0.2.6 → regstack-0.4.0}/.gitignore +0 -0
  91. {regstack-0.2.6 → regstack-0.4.0}/.python-version +0 -0
  92. {regstack-0.2.6 → regstack-0.4.0}/.readthedocs.yaml +0 -0
  93. {regstack-0.2.6 → regstack-0.4.0}/LICENSE +0 -0
  94. {regstack-0.2.6 → regstack-0.4.0}/NOTICE +0 -0
  95. {regstack-0.2.6 → regstack-0.4.0}/SECURITY.md +0 -0
  96. {regstack-0.2.6 → regstack-0.4.0}/docs/_static/.gitkeep +0 -0
  97. {regstack-0.2.6 → regstack-0.4.0}/docs/_templates/.gitkeep +0 -0
  98. {regstack-0.2.6 → regstack-0.4.0}/docs/cli.md +0 -0
  99. {regstack-0.2.6 → regstack-0.4.0}/docs/quickstart.md +0 -0
  100. {regstack-0.2.6 → regstack-0.4.0}/docs/theming.md +0 -0
  101. {regstack-0.2.6 → regstack-0.4.0}/examples/_common/__init__.py +0 -0
  102. {regstack-0.2.6 → regstack-0.4.0}/examples/_common/app.py +0 -0
  103. {regstack-0.2.6 → regstack-0.4.0}/examples/mongo/README.md +0 -0
  104. {regstack-0.2.6 → regstack-0.4.0}/examples/mongo/branding/theme.css +0 -0
  105. {regstack-0.2.6 → regstack-0.4.0}/examples/mongo/main.py +0 -0
  106. {regstack-0.2.6 → regstack-0.4.0}/examples/mongo/regstack.toml +0 -0
  107. {regstack-0.2.6 → regstack-0.4.0}/examples/postgres/README.md +0 -0
  108. {regstack-0.2.6 → regstack-0.4.0}/examples/postgres/main.py +0 -0
  109. {regstack-0.2.6 → regstack-0.4.0}/examples/postgres/regstack.toml +0 -0
  110. {regstack-0.2.6 → regstack-0.4.0}/examples/sqlite/README.md +0 -0
  111. {regstack-0.2.6 → regstack-0.4.0}/examples/sqlite/main.py +0 -0
  112. {regstack-0.2.6 → regstack-0.4.0}/examples/sqlite/regstack.toml +0 -0
  113. {regstack-0.2.6 → regstack-0.4.0}/regstack.toml.example +0 -0
  114. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/__init__.py +0 -0
  115. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/clock.py +0 -0
  116. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/dependencies.py +0 -0
  117. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/jwt.py +0 -0
  118. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/lockout.py +0 -0
  119. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/mfa.py +0 -0
  120. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/password.py +0 -0
  121. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/auth/tokens.py +0 -0
  122. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/__init__.py +0 -0
  123. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/factory.py +0 -0
  124. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/__init__.py +0 -0
  125. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/client.py +0 -0
  126. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
  127. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
  128. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
  129. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
  130. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
  131. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/__init__.py +0 -0
  132. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/migrations/__init__.py +0 -0
  133. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/migrations/env.py +0 -0
  134. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  135. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  136. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/__init__.py +0 -0
  137. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  138. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  139. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
  140. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
  141. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
  142. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/backends/sql/types.py +0 -0
  143. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/__init__.py +0 -0
  144. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/_runtime.py +0 -0
  145. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/admin.py +0 -0
  146. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/doctor.py +0 -0
  147. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/init.py +0 -0
  148. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/cli/migrate.py +0 -0
  149. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/config/loader.py +0 -0
  150. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/config/secrets.py +0 -0
  151. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/__init__.py +0 -0
  152. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/base.py +0 -0
  153. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/composer.py +0 -0
  154. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/console.py +0 -0
  155. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/factory.py +0 -0
  156. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/ses.py +0 -0
  157. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/smtp.py +0 -0
  158. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/email_change.html +0 -0
  159. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/email_change.subject.txt +0 -0
  160. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/email_change.txt +0 -0
  161. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/password_reset.html +0 -0
  162. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  163. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/password_reset.txt +0 -0
  164. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  165. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  166. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/verification.html +0 -0
  167. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/verification.subject.txt +0 -0
  168. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/email/templates/verification.txt +0 -0
  169. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/hooks/__init__.py +0 -0
  170. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/_objectid.py +0 -0
  171. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/login_attempt.py +0 -0
  172. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/mfa_code.py +0 -0
  173. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/models/pending_registration.py +0 -0
  174. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/_schemas.py +0 -0
  175. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/admin.py +0 -0
  176. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/logout.py +0 -0
  177. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/password.py +0 -0
  178. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/phone.py +0 -0
  179. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/register.py +0 -0
  180. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/routers/verify.py +0 -0
  181. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/__init__.py +0 -0
  182. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/base.py +0 -0
  183. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/factory.py +0 -0
  184. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/null.py +0 -0
  185. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/sns.py +0 -0
  186. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/sms/twilio.py +0 -0
  187. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/__init__.py +0 -0
  188. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/static/css/core.css +0 -0
  189. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/static/css/theme.css +0 -0
  190. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  191. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/forgot.html +0 -0
  192. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  193. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/register.html +0 -0
  194. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/reset.html +0 -0
  195. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/auth/verify.html +0 -0
  196. {regstack-0.2.6 → regstack-0.4.0}/src/regstack/ui/templates/base.html +0 -0
  197. {regstack-0.2.6 → regstack-0.4.0}/tests/__init__.py +0 -0
  198. {regstack-0.2.6 → regstack-0.4.0}/tests/conftest.py +0 -0
  199. {regstack-0.2.6/tests/integration → regstack-0.4.0/tests/e2e}/__init__.py +0 -0
  200. {regstack-0.2.6/tests/unit → regstack-0.4.0/tests/integration}/__init__.py +0 -0
  201. {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_account_management.py +0 -0
  202. {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_admin_router.py +0 -0
  203. {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_happy_path.py +0 -0
  204. {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_indexes.py +0 -0
  205. {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_login_lockout.py +0 -0
  206. {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_mfa.py +0 -0
  207. {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_password_reset.py +0 -0
  208. {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_sql_migrations.py +0 -0
  209. {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_ui_router.py +0 -0
  210. {regstack-0.2.6 → regstack-0.4.0}/tests/integration/test_verification.py +0 -0
  211. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_cli.py +0 -0
  212. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_cli_doctor.py +0 -0
  213. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_cli_init.py +0 -0
  214. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_config_loader.py +0 -0
  215. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_jwt.py +0 -0
  216. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_lockout.py +0 -0
  217. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_mail_composer.py +0 -0
  218. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_mfa_code_repo.py +0 -0
  219. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_password.py +0 -0
  220. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_ses_backend.py +0 -0
  221. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_sms.py +0 -0
  222. {regstack-0.2.6 → regstack-0.4.0}/tests/unit/test_smtp_backend.py +0 -0
@@ -5,6 +5,40 @@ authoritative copy lives at
5
5
  [`docs/changelog.md`](docs/changelog.md) and is rendered into the
6
6
  Sphinx docs.
7
7
 
8
+ ## 0.3.0 — 2026-04-30
9
+
10
+ **OAuth — Sign in with Google.** Opt-in via the new `oauth` extra
11
+ and `enable_oauth=True`. Five JSON endpoints, an SSR
12
+ `/account/oauth-complete` page, "Sign in with Google" button on the
13
+ login page, and a Connected-accounts panel on `/account/me`.
14
+
15
+ Schema migration `0002_oauth.py` creates `oauth_identities` +
16
+ `oauth_states` and makes `users.hashed_password` nullable
17
+ (OAuth-only users have no password). Roll forward via
18
+ `regstack migrate` or first-boot `install_schema()` — no manual
19
+ intervention.
20
+
21
+ Account-linking policy defaults to **refuse**: if a Google sign-in
22
+ arrives carrying an email that already belongs to a password-
23
+ registered user, the callback returns `?error=email_in_use` and the
24
+ user must sign in then explicitly link from `/account/me`. Hosts
25
+ that consciously accept the email-recycling threat for UX can flip
26
+ `oauth.auto_link_verified_emails = true`. See
27
+ [`docs/oauth.md`](https://regstack.readthedocs.io/en/latest/oauth.html)
28
+ and [`tasks/oauth-design.md`](https://github.com/jdrumgoole/regstack/blob/main/tasks/oauth-design.md)
29
+ for the full threat model.
30
+
31
+ **Migration**
32
+
33
+ - Install the new extra: `uv add 'regstack[oauth]'`.
34
+ - Set `enable_oauth = true` and provide `oauth.google_client_id` +
35
+ `oauth.google_client_secret`.
36
+ - Run `regstack migrate` (SQL backends only) or rely on
37
+ `install_schema()` at first boot.
38
+
39
+ `BaseUser.hashed_password` is now `str | None`. Code that imported
40
+ the field type explicitly will need to widen it.
41
+
8
42
  ## 0.2.6 — 2026-04-28
9
43
 
10
44
  Bug fix.
@@ -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.2.6
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,16 +29,21 @@ 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'
36
39
  Requires-Dist: httpx>=0.27; extra == 'dev'
37
40
  Requires-Dist: invoke>=2.2; extra == 'dev'
38
41
  Requires-Dist: mypy>=1.10; extra == 'dev'
42
+ Requires-Dist: pyjwt[crypto]>=2.8; extra == 'dev'
39
43
  Requires-Dist: pymongo>=4.9; extra == 'dev'
40
44
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
41
45
  Requires-Dist: pytest-cov>=5.0; extra == 'dev'
46
+ Requires-Dist: pytest-playwright>=0.5; extra == 'dev'
42
47
  Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
43
48
  Requires-Dist: pytest>=8.0; extra == 'dev'
44
49
  Requires-Dist: ruff>=0.4; extra == 'dev'
@@ -52,6 +57,8 @@ Requires-Dist: sphinx-copybutton>=0.5; extra == 'docs'
52
57
  Requires-Dist: sphinx>=7.3; extra == 'docs'
53
58
  Provides-Extra: mongo
54
59
  Requires-Dist: pymongo>=4.9; extra == 'mongo'
60
+ Provides-Extra: oauth
61
+ Requires-Dist: pyjwt[crypto]>=2.8; extra == 'oauth'
55
62
  Provides-Extra: postgres
56
63
  Requires-Dist: asyncpg>=0.29; extra == 'postgres'
57
64
  Provides-Extra: ses
@@ -75,9 +82,9 @@ auth bugs.**
75
82
 
76
83
  `pip install regstack`, point it at SQLite (default), [PostgreSQL](https://www.postgresql.org/),
77
84
  or [MongoDB](https://www.mongodb.com/), and you have register / login /
78
- verify-email / reset-password / change-email / delete-account / optional SMS
79
- two-factor / admin endpoints / themable HTML pages — all behind a small Python
80
- 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.
81
88
 
82
89
  📚 **Docs:** <https://regstack.readthedocs.io>
83
90
  &nbsp;·&nbsp;
@@ -129,6 +136,7 @@ result everywhere is what regstack is for.
129
136
  ✔ Forgot / reset password — anti-enumeration: identical responses
130
137
  ✔ Change password (revokes old tokens) / change email (re-verify)
131
138
  ✔ Delete account
139
+ ✔ Sign in with Google (PKCE + ID-token verification, opt-in)
132
140
  ✔ Optional SMS two-factor (TOTP-style 6-digit codes over SMS)
133
141
  ✔ Server-side login lockout (HTTP 429 + Retry-After)
134
142
  ✔ Admin endpoints (list / disable / delete users, stats)
@@ -242,7 +250,9 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
242
250
 
243
251
  Alpha. Single-file SQLite is the default and runs with no infrastructure;
244
252
  PostgreSQL and MongoDB backends pass the same parametrized integration
245
- 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
246
256
  [changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
247
257
  for the per-release breakdown.
248
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,107 @@
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
+
24
+ ## 0.3.0 — 2026-04-30
25
+
26
+ **OAuth — Sign in with Google.** Built across four PRs (M1–M4 of
27
+ [`tasks/oauth-design.md`](https://github.com/jdrumgoole/regstack/blob/main/tasks/oauth-design.md));
28
+ this is the release cut that wraps them up.
29
+
30
+ ### Added
31
+
32
+ - New optional extra `oauth = ["pyjwt[crypto]>=2.8"]`.
33
+ - New `enable_oauth` flag and `OAuthConfig` sub-model
34
+ (`google_client_id`, `google_client_secret`, `google_redirect_uri`,
35
+ `auto_link_verified_emails`, `enforce_mfa_on_oauth_signin`,
36
+ `state_ttl_seconds`, `completion_ttl_seconds`).
37
+ - `regstack.oauth` package — `OAuthProvider` ABC, `OAuthRegistry`,
38
+ `OAuthTokens`, `OAuthUserInfo`, error hierarchy, and the concrete
39
+ `GoogleProvider` (Authorization Code with PKCE, ID-token
40
+ verification via `pyjwt[crypto]` + `PyJWKClient` against Google's
41
+ JWKS).
42
+ - Five JSON endpoints (mounted lazily when `enable_oauth=True` and a
43
+ provider is registered):
44
+ - `GET /oauth/{provider}/start`
45
+ - `GET /oauth/{provider}/callback`
46
+ - `POST /oauth/exchange`
47
+ - `POST /oauth/{provider}/link/start` (auth)
48
+ - `DELETE /oauth/{provider}/link` (auth)
49
+ - `GET /oauth/providers` (auth)
50
+ - New SSR page `/account/oauth-complete` (token-handoff round-trip).
51
+ - "Sign in with Google" button on `/account/login` and a Connected-
52
+ accounts panel on `/account/me`. Login page surfaces callback
53
+ errors via `?error=<code>` with translated banners.
54
+ - Two new repo protocols: `OAuthIdentityRepoProtocol`,
55
+ `OAuthStateRepoProtocol`. Mongo + SQL implementations with
56
+ parametrized integration tests over all three backends.
57
+ - Four new hook events: `oauth_signin_started`,
58
+ `oauth_signin_completed`, `oauth_account_linked`,
59
+ `oauth_account_unlinked`.
60
+ - `tests/_fake_google/` — in-process provider stub so the OAuth
61
+ test suite stays offline and parallel-safe.
62
+ - New docs page [`docs/oauth.md`](oauth.md) — host guide.
63
+
64
+ ### Changed (potentially breaking)
65
+
66
+ - **`BaseUser.hashed_password: str` → `str | None`.** OAuth-only
67
+ users have no password. The login route rejects password attempts
68
+ on these accounts with the same generic 401 wrong-password gets
69
+ (no enumeration). `change-password`, `change-email`, and
70
+ `delete-account` all return 400 for OAuth-only users with a
71
+ pointer at the password-reset flow, which doubles as a "set
72
+ initial password" path.
73
+ - `users.hashed_password` is now nullable in the SQL schema —
74
+ migration `0002_oauth.py` flips the column via `batch_alter_table`
75
+ (SQLite-safe). Existing rows are unaffected.
76
+ - New SQL tables `oauth_identities` and `oauth_states`. Mongo
77
+ collections + indexes added by `install_schema()`.
78
+
79
+ ### Security defaults
80
+
81
+ - **Account-linking policy defaults to refuse.** When a Google
82
+ sign-in carries an email that already belongs to a regstack user,
83
+ the callback returns `?error=email_in_use`. Hosts can opt into
84
+ auto-linking via `oauth.auto_link_verified_emails = true`, which
85
+ also requires `email_verified=true` on the ID token. The threat
86
+ model is in `tasks/oauth-design.md` § 1.
87
+ - **Server-side PKCE.** `code_verifier` is stored on the
88
+ `oauth_states` row and never enters the URL.
89
+ - **One-time token-handoff.** `/oauth/exchange` consumes the state
90
+ row atomically; second exchange returns 404.
91
+ - **Refuse to unlink the only sign-in method.** Returns 400 for
92
+ OAuth-only users attempting to unlink their only provider.
93
+ - **OAuth sessions are normal session JWTs** — the existing
94
+ `tokens_invalidated_after` bulk-revoke applies, so a password
95
+ change kills any OAuth-issued session too.
96
+
97
+ ### Migration notes
98
+
99
+ - Install the extra: `uv add 'regstack[oauth]'`.
100
+ - Configure: set `enable_oauth = true` and provide
101
+ `oauth.google_client_id` + `oauth.google_client_secret` (the
102
+ secret in `regstack.secrets.env` as
103
+ `REGSTACK_OAUTH__GOOGLE_CLIENT_SECRET`).
104
+ - Schema: roll forward via `regstack migrate` or rely on
105
+ `install_schema()` at first boot.
106
+
6
107
  ## 0.2.6 — 2026-04-28
7
108
 
8
109
  **Bug fix.**
@@ -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/),
@@ -120,6 +124,7 @@ configuration
120
124
  architecture
121
125
  security
122
126
  embedding
127
+ oauth
123
128
  theming
124
129
  cli
125
130
  ```
@@ -134,7 +139,7 @@ changelog
134
139
 
135
140
  ## Project status
136
141
 
137
- 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
138
143
  runs with no infrastructure; PostgreSQL and MongoDB pass the same
139
144
  parametrized integration suite. The full backend matrix runs in
140
145
  parallel against every test, so a green CI on `main` is a strong