regstack 0.5.6__tar.gz → 0.5.9__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.
- {regstack-0.5.6 → regstack-0.5.9}/CHANGELOG.md +148 -0
- {regstack-0.5.6 → regstack-0.5.9}/PKG-INFO +1 -1
- {regstack-0.5.6 → regstack-0.5.9}/docs/changelog.md +112 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/configuration.md +64 -2
- {regstack-0.5.6 → regstack-0.5.9}/docs/security.md +7 -4
- {regstack-0.5.6 → regstack-0.5.9}/pyproject.toml +1 -1
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/repositories/blacklist_repo.py +4 -1
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +11 -2
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -15
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/protocols.py +14 -1
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/repositories/oauth_state_repo.py +11 -2
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/models/user.py +13 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/account.py +1 -1
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/oauth.py +171 -15
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/verify.py +6 -2
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/static/js/regstack.js +9 -0
- regstack-0.5.9/src/regstack/version.py +1 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_oauth_google_router.py +93 -0
- {regstack-0.5.6 → regstack-0.5.9}/uv.lock +1 -1
- regstack-0.5.6/src/regstack/version.py +0 -1
- {regstack-0.5.6 → regstack-0.5.9}/.github/workflows/publish.yml +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/.github/workflows/test.yml +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/.gitignore +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/.python-version +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/.readthedocs.yaml +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/CLAUDE.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/LICENSE +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/NOTICE +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/README.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/SECURITY.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/_static/.gitkeep +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/_templates/.gitkeep +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/api.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/architecture.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/cli.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/conf.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/embedding.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/index.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/oauth.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/quickstart.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/security-reports/README.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/docs/theming.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/_common/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/_common/app.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/mongo/README.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/mongo/branding/theme.css +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/mongo/main.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/mongo/regstack.toml +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/postgres/README.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/postgres/main.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/postgres/regstack.toml +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/sqlite/README.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/sqlite/main.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/examples/sqlite/regstack.toml +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/regstack.toml.example +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/scripts/ccr_coverage_setup.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/scripts/security-review-prompt.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/app.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/auth/clock.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/auth/dependencies.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/auth/jwt.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/auth/lockout.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/auth/password.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/auth/rate_limit.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/base.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/factory.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/backend.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/client.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/indexes.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/backend.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/migrations/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/migrations/env.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/repositories/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/schema.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/types.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/cli/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/cli/__main__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/cli/admin.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/cli/doctor.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/cli/init.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/config/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/config/loader.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/config/schema.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/config/secrets.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/base.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/composer.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/console.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/factory.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/ses.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/smtp.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/hooks/events.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/models/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/models/_objectid.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/models/oauth_identity.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/models/oauth_state.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/oauth/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/oauth/base.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/oauth/errors.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/oauth/providers/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/oauth/providers/google.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/oauth/registry.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/_helpers.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/admin.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/login.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/logout.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/password.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/phone.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/routers/register.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/sms/base.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/sms/factory.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/sms/null.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/sms/sns.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/sms/twilio.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/pages.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/templates/auth/login.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/templates/auth/me.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/oauth_google/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/oauth_google/cli.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/oauth_google/routes.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/oauth_google/server.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/oauth_google/validators.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/oauth_google/window.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/oauth_google/writer.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/theme_designer/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/theme_designer/cli.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/theme_designer/routes.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/theme_designer/server.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/theme_designer/static/designer.css +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/theme_designer/static/designer.js +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/theme_designer/templates/designer.html +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/theme_designer/validators.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/theme_designer/window.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/src/regstack/wizard/theme_designer/writer.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tasks/oauth-design.md +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tasks.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/_fake_google/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/_fake_google/provider.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/conftest.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/e2e/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/e2e/conftest.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/e2e/test_theme_designer.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/e2e/test_wizard_oauth_flow.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_account_management.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_admin_router.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_happy_path.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_indexes.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_login_lockout.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_mfa.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_oauth_repos.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_oauth_ui.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_password_reset.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_rate_limits.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_sql_migrations.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_ui_router.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/integration/test_verification.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/__init__.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_base_install_imports.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_cli.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_cli_doctor.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_cli_init.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_cli_migrate.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_config_loader.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_jwt.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_lockout.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_mail_composer.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_mfa_code_repo.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_oauth_google.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_password.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_ses_backend.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_sms.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_smtp_backend.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_theme_designer_cli.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_theme_designer_routes.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_theme_designer_validators.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_theme_designer_writer.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_ui_env.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_wizard_oauth_cli.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_wizard_oauth_routes.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_wizard_oauth_validators.py +0 -0
- {regstack-0.5.6 → regstack-0.5.9}/tests/unit/test_wizard_oauth_writer.py +0 -0
|
@@ -5,6 +5,118 @@ authoritative copy lives at
|
|
|
5
5
|
[`docs/changelog.md`](docs/changelog.md) and is rendered into the
|
|
6
6
|
Sphinx docs.
|
|
7
7
|
|
|
8
|
+
## 0.5.9 — 2026-05-13
|
|
9
|
+
|
|
10
|
+
**`OAuthConfig.enforce_mfa_on_oauth_signin` is now wired.** The flag
|
|
11
|
+
has been on the config since the OAuth router shipped (0.3.0) and the
|
|
12
|
+
wizard surfaced it, but the callback never read it — operators who
|
|
13
|
+
flipped it on still got OAuth sign-ins that bypassed the SMS second
|
|
14
|
+
factor. The High #1 finding from the post-0.5.6 consistency audit.
|
|
15
|
+
|
|
16
|
+
Now, when the flag is `true` and the resolved user has SMS MFA set
|
|
17
|
+
up (`is_mfa_enabled=True` plus a `phone_number`):
|
|
18
|
+
|
|
19
|
+
- The OAuth callback sends the SMS code and stashes a short-lived
|
|
20
|
+
`login_mfa` pending JWT in the state row (instead of a session
|
|
21
|
+
token).
|
|
22
|
+
- `POST /oauth/exchange` returns `mfa_required=True` and
|
|
23
|
+
`mfa_pending_token=...` (with no `access_token`) so the SPA knows
|
|
24
|
+
to redirect to `/account/mfa-confirm`.
|
|
25
|
+
- The SPA's bundled `regstack.js` `oauth-complete` handler stashes
|
|
26
|
+
the pending token under `regstack.mfa_pending` (same key the
|
|
27
|
+
password-login MFA flow uses) and redirects.
|
|
28
|
+
- The user enters the SMS code and hits the existing
|
|
29
|
+
`POST /login/mfa-confirm` endpoint — same downstream path as the
|
|
30
|
+
password-login second factor.
|
|
31
|
+
|
|
32
|
+
Link flows (`mode="link"`) are exempt: the user was already
|
|
33
|
+
authenticated when they kicked off the link, so adding SMS friction
|
|
34
|
+
on top of an already-authenticated link operation has no
|
|
35
|
+
threat-model win.
|
|
36
|
+
|
|
37
|
+
The `ExchangeResponse` model grew two optional fields
|
|
38
|
+
(`mfa_required: bool = False`, `mfa_pending_token: str | None = None`)
|
|
39
|
+
and `access_token` is now defaulted to `""` so the MFA branch can
|
|
40
|
+
return cleanly. Existing handlers reading `access_token` keep
|
|
41
|
+
working — they just need to check `mfa_required` first.
|
|
42
|
+
|
|
43
|
+
## 0.5.8 — 2026-05-13
|
|
44
|
+
|
|
45
|
+
Audit-driven consistency cleanup — small fixes across the API surface
|
|
46
|
+
flagged by the post-0.5.6 consistency review.
|
|
47
|
+
|
|
48
|
+
**Security**
|
|
49
|
+
|
|
50
|
+
- **`oauth.completion_ttl_seconds` is finally enforced.** The flag has
|
|
51
|
+
been on `OAuthConfig` since the OAuth router shipped, but the
|
|
52
|
+
callback never used it: a state row stayed valid for the full
|
|
53
|
+
`state_ttl_seconds` (300s default) between callback completion and
|
|
54
|
+
`/oauth/exchange`. Now `set_result_token(...)` bumps the row's
|
|
55
|
+
expiry down to `now + completion_ttl_seconds` (30s default), so the
|
|
56
|
+
blast radius of a stolen state_id post-callback is the documented
|
|
57
|
+
30-second window. `OAuthStateRepoProtocol.set_result_token` grew an
|
|
58
|
+
optional `new_expires_at=` kwarg to make this atomic with the
|
|
59
|
+
token write.
|
|
60
|
+
|
|
61
|
+
**Changed (UserPublic surface)**
|
|
62
|
+
|
|
63
|
+
- `UserPublic` now serialises `updated_at` and
|
|
64
|
+
`tokens_invalidated_after`. SPAs comparing the latter against their
|
|
65
|
+
cached session JWT's `iat` can detect a forced sign-out after a
|
|
66
|
+
password / email change without an extra round-trip.
|
|
67
|
+
|
|
68
|
+
**Changed (hook payloads)**
|
|
69
|
+
|
|
70
|
+
- `oauth_signin_started` in `mode="link"` now carries the
|
|
71
|
+
authenticated `user=` kwarg, matching `oauth_signin_completed` and
|
|
72
|
+
`oauth_account_linked`. The `mode="signin"` call site stays without
|
|
73
|
+
`user=` (there isn't one yet — sign-in is what produces it).
|
|
74
|
+
|
|
75
|
+
**Internal**
|
|
76
|
+
|
|
77
|
+
- `OAuthConfig.completion_ttl_seconds` config field is now load-bearing
|
|
78
|
+
(was previously declared-but-unread).
|
|
79
|
+
- `MessageResponse` in `routers/oauth.py` deleted; the router now uses
|
|
80
|
+
the shared one from `routers/_schemas.py`. OpenAPI no longer carries
|
|
81
|
+
two identically-named schemas.
|
|
82
|
+
- `MongoOAuthStateRepo` / `SqlOAuthStateRepo` `set_result_token` grew
|
|
83
|
+
a `new_expires_at` parameter (default `None`, so existing callers
|
|
84
|
+
see no change).
|
|
85
|
+
- `MongoBlacklistRepo.purge_expired` switched from `$lte` to `$lt` to
|
|
86
|
+
match the rest of the `purge_expired` family across both backends.
|
|
87
|
+
Edge-instant tokens get one more microsecond of life — the
|
|
88
|
+
bulk-revoke check (which DOES use `<=`) is unchanged.
|
|
89
|
+
- Dead `create()` and `delete_by_id()` methods removed from
|
|
90
|
+
`MongoPendingRepo` — neither was in the protocol or the SQL impl,
|
|
91
|
+
and nothing in src or tests called them.
|
|
92
|
+
- OAuth `start` and `callback` endpoints now declare
|
|
93
|
+
`response_class=RedirectResponse` and `status_code=302`. OpenAPI
|
|
94
|
+
surfaces the redirect intent properly.
|
|
95
|
+
- Custom-claim JWT encoder in `routers/account.py` (email-change
|
|
96
|
+
token) now emits `iat` as a float instead of `int`, matching the
|
|
97
|
+
three other custom-claim encoders and the bulk-revoke contract.
|
|
98
|
+
- `routers/verify.py` `created_at` for resent pending registrations
|
|
99
|
+
now goes through `rs.clock.now()` instead of wall-clock
|
|
100
|
+
`datetime.now(UTC)` — keeps `FrozenClock`-driven tests
|
|
101
|
+
deterministic.
|
|
102
|
+
- `BaseUser.model_config = ConfigDict(extra="allow")` got a
|
|
103
|
+
comment explaining why it's the only model in the package that
|
|
104
|
+
doesn't `extra="forbid"`.
|
|
105
|
+
|
|
106
|
+
## 0.5.7 — 2026-05-13
|
|
107
|
+
|
|
108
|
+
Documentation-only follow-up to 0.5.6.
|
|
109
|
+
|
|
110
|
+
- `docs/configuration.md` now documents the per-route `*_rate_limit`
|
|
111
|
+
family (added in 0.5.4) instead of pointing at
|
|
112
|
+
`login_max_per_minute` / `login_max_per_hour` as reserved future
|
|
113
|
+
fields.
|
|
114
|
+
- `docs/security.md` no longer references
|
|
115
|
+
`PasswordHasher.needs_rehash` (removed in 0.5.6). Replacement
|
|
116
|
+
guidance points hosts at `pwdlib.PasswordHash.verify_and_update`.
|
|
117
|
+
- Root `CHANGELOG.md` backfilled with 0.4.0 and 0.5.0 entries so it
|
|
118
|
+
matches `docs/changelog.md`.
|
|
119
|
+
|
|
8
120
|
## 0.5.6 — 2026-05-13
|
|
9
121
|
|
|
10
122
|
Eleven days of security-review remediation, supply-chain hardening,
|
|
@@ -86,6 +198,42 @@ to use it, call `pwdlib.PasswordHash.verify_and_update` directly.
|
|
|
86
198
|
`routers/logout.py` (was listed in `KNOWN_EVENTS` but no router
|
|
87
199
|
emitted it).
|
|
88
200
|
|
|
201
|
+
## 0.5.0 — 2026-05-02
|
|
202
|
+
|
|
203
|
+
**Theme designer.** `regstack theme design` opens a native pywebview
|
|
204
|
+
window with controls for every `--rs-*` CSS custom property and a
|
|
205
|
+
real-time preview of the bundled SSR widgets (sign-in form, success /
|
|
206
|
+
error banners, danger-zone button). Saving writes `regstack-theme.css`;
|
|
207
|
+
the designer round-trips values back into the form on next launch so
|
|
208
|
+
iteration is non-destructive. `--print-only` mode takes repeatable
|
|
209
|
+
`--var NAME=VALUE` pairs (with a `dark:` prefix for dark-scheme
|
|
210
|
+
overrides) and writes the file headlessly. Lives in
|
|
211
|
+
`regstack.wizard.theme_designer`; registered as a lazy Click subgroup
|
|
212
|
+
so `regstack init` / `doctor` don't pay the pywebview/uvicorn import
|
|
213
|
+
cost.
|
|
214
|
+
|
|
215
|
+
**Docs.** New "About the examples" convention block at the top of
|
|
216
|
+
`docs/index.md`. Every URL, email, smtp host, and admin command across
|
|
217
|
+
the docs now extrapolates from the same fictional app at
|
|
218
|
+
`app.example.com` with `<username>` / `<password>` placeholders.
|
|
219
|
+
|
|
220
|
+
## 0.4.0 — 2026-05-02
|
|
221
|
+
|
|
222
|
+
**OAuth setup wizard.** `regstack oauth setup` opens a native webview
|
|
223
|
+
window that walks an operator through registering a Google OAuth 2.0
|
|
224
|
+
client and merges the credentials into `regstack.toml` +
|
|
225
|
+
`regstack.secrets.env` non-destructively (preserves comments, other
|
|
226
|
+
tables, unrelated keys). 12-step SPA inside a local-only 127.0.0.1
|
|
227
|
+
FastAPI server, gated by a per-launch random token. Each "Next" click
|
|
228
|
+
hits a server-side validator so the Write step can never be reached
|
|
229
|
+
with bad data. `--print-only` mode skips the GUI for headless / CI
|
|
230
|
+
use.
|
|
231
|
+
|
|
232
|
+
Three new base dependencies — `pywebview>=5.0`, `tomlkit>=0.13`,
|
|
233
|
+
`uvicorn[standard]>=0.29` — for the wizard's local server.
|
|
234
|
+
`pytest-playwright` added to the `dev` extra; new `inv test-e2e` task
|
|
235
|
+
chained into `inv test-all`.
|
|
236
|
+
|
|
89
237
|
## 0.3.0 — 2026-04-30
|
|
90
238
|
|
|
91
239
|
**OAuth — Sign in with Google.** Opt-in via the new `oauth` extra
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regstack
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.9
|
|
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
|
|
@@ -3,6 +3,118 @@
|
|
|
3
3
|
All notable changes to this project are documented here. Versions follow
|
|
4
4
|
[Semantic Versioning](https://semver.org/) once `1.0.0` ships.
|
|
5
5
|
|
|
6
|
+
## 0.5.9 — 2026-05-13
|
|
7
|
+
|
|
8
|
+
### Security
|
|
9
|
+
|
|
10
|
+
- **`OAuthConfig.enforce_mfa_on_oauth_signin` is now wired.** The flag
|
|
11
|
+
has been on the config since 0.3.0 and surfaced through the OAuth
|
|
12
|
+
setup wizard, but the callback never read it — operators who
|
|
13
|
+
enabled it still got OAuth sign-ins that bypassed the SMS second
|
|
14
|
+
factor. The High #1 finding from the post-0.5.6 consistency audit.
|
|
15
|
+
|
|
16
|
+
When the flag is `true` and the resolved user has SMS MFA set up
|
|
17
|
+
(`is_mfa_enabled=True` plus a `phone_number`), the OAuth callback
|
|
18
|
+
now sends the SMS code, stashes a short-lived `login_mfa` pending
|
|
19
|
+
JWT in the state row instead of a session token, and the SPA's
|
|
20
|
+
`regstack.js` `oauth-complete` handler reroutes through the
|
|
21
|
+
existing `/account/mfa-confirm` page → `POST /login/mfa-confirm`
|
|
22
|
+
flow.
|
|
23
|
+
|
|
24
|
+
Link flows (`mode="link"`) are intentionally exempt: the user was
|
|
25
|
+
already authenticated when they kicked off the link, so re-MFAing
|
|
26
|
+
is friction without a threat-model win.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `ExchangeResponse` gained two optional fields:
|
|
31
|
+
- `mfa_required: bool = False`
|
|
32
|
+
- `mfa_pending_token: str | None = None`
|
|
33
|
+
|
|
34
|
+
`access_token` defaults to `""` (instead of being required) so the
|
|
35
|
+
MFA branch can return without one. Existing SPAs that read
|
|
36
|
+
`access_token` keep working — they just need to branch on
|
|
37
|
+
`mfa_required` first.
|
|
38
|
+
|
|
39
|
+
- `oauth_signin_completed` hook now carries `mfa_required=<bool>`
|
|
40
|
+
alongside the existing `user`, `provider`, `mode`, `was_new` kwargs
|
|
41
|
+
so observability handlers can distinguish "session minted" from
|
|
42
|
+
"MFA second-step in progress" outcomes.
|
|
43
|
+
|
|
44
|
+
## 0.5.8 — 2026-05-13
|
|
45
|
+
|
|
46
|
+
Audit-driven consistency cleanup — small fixes across the API surface
|
|
47
|
+
flagged by the post-0.5.6 consistency review.
|
|
48
|
+
|
|
49
|
+
### Security
|
|
50
|
+
|
|
51
|
+
- **`oauth.completion_ttl_seconds` is now enforced.** This flag has
|
|
52
|
+
been on `OAuthConfig` since the OAuth router shipped, but the
|
|
53
|
+
callback never used it. A state row stayed valid for the full
|
|
54
|
+
`state_ttl_seconds` (300s default) between callback completion and
|
|
55
|
+
the SPA's `/oauth/exchange` call. The callback now shortens the
|
|
56
|
+
row's `expires_at` to `now + completion_ttl_seconds` (30s default)
|
|
57
|
+
when it stashes the result token, so the blast radius of a stolen
|
|
58
|
+
state_id post-callback is the documented 30-second window.
|
|
59
|
+
|
|
60
|
+
### Changed (UserPublic surface)
|
|
61
|
+
|
|
62
|
+
- `UserPublic` now serialises `updated_at` and
|
|
63
|
+
`tokens_invalidated_after`. SPAs comparing the latter against their
|
|
64
|
+
cached session JWT's `iat` can detect a forced sign-out after a
|
|
65
|
+
password / email change without an extra round-trip.
|
|
66
|
+
|
|
67
|
+
### Changed (hook payloads)
|
|
68
|
+
|
|
69
|
+
- `oauth_signin_started` in `mode="link"` now carries the
|
|
70
|
+
authenticated `user=` kwarg, matching `oauth_signin_completed` and
|
|
71
|
+
`oauth_account_linked`. The `mode="signin"` call site stays
|
|
72
|
+
user-less (there isn't one yet — sign-in is what produces it).
|
|
73
|
+
|
|
74
|
+
### Internal
|
|
75
|
+
|
|
76
|
+
- `OAuthStateRepoProtocol.set_result_token` grew an optional
|
|
77
|
+
`new_expires_at=` kwarg so the callback can re-set the row's
|
|
78
|
+
expiry atomically with the token write. Both Mongo and SQL impls
|
|
79
|
+
updated.
|
|
80
|
+
- `MessageResponse` in `routers/oauth.py` deleted; the router now
|
|
81
|
+
uses the shared one from `routers/_schemas.py`. OpenAPI no longer
|
|
82
|
+
carries two identically-named schemas.
|
|
83
|
+
- `MongoBlacklistRepo.purge_expired` switched from `$lte` to `$lt`
|
|
84
|
+
to match the rest of the `purge_expired` family. Bulk-revoke
|
|
85
|
+
(which DOES use `<=` for the conservative same-instant
|
|
86
|
+
interpretation) is unchanged.
|
|
87
|
+
- Dead `create()` / `delete_by_id()` methods removed from
|
|
88
|
+
`MongoPendingRepo` — neither was in the protocol or the SQL impl.
|
|
89
|
+
- OAuth `start` and `callback` endpoints now declare
|
|
90
|
+
`response_class=RedirectResponse` and `status_code=302`.
|
|
91
|
+
- Custom-claim JWT encoder in `routers/account.py` (email-change
|
|
92
|
+
token) now emits `iat` as a float instead of `int`, matching the
|
|
93
|
+
three other custom-claim encoders.
|
|
94
|
+
- `routers/verify.py` `created_at` for resent pending registrations
|
|
95
|
+
now goes through `rs.clock.now()` instead of wall-clock
|
|
96
|
+
`datetime.now(UTC)`.
|
|
97
|
+
- `BaseUser.model_config = ConfigDict(extra="allow")` is now
|
|
98
|
+
documented inline (it's the only model in the package that
|
|
99
|
+
doesn't `extra="forbid"`, on purpose).
|
|
100
|
+
|
|
101
|
+
## 0.5.7 — 2026-05-13
|
|
102
|
+
|
|
103
|
+
Documentation-only follow-up to 0.5.6 — no runtime code changes.
|
|
104
|
+
|
|
105
|
+
### Docs
|
|
106
|
+
|
|
107
|
+
- `docs/configuration.md` now documents the per-route `*_rate_limit`
|
|
108
|
+
family (added in 0.5.4) instead of pointing at
|
|
109
|
+
`login_max_per_minute` / `login_max_per_hour` as reserved future
|
|
110
|
+
fields.
|
|
111
|
+
- `docs/security.md` no longer references
|
|
112
|
+
`PasswordHasher.needs_rehash` (removed in 0.5.6). Replacement
|
|
113
|
+
guidance points hosts at `pwdlib.PasswordHash.verify_and_update`
|
|
114
|
+
inside a `user_logged_in` hook.
|
|
115
|
+
- Root `CHANGELOG.md` backfilled with 0.4.0 and 0.5.0 entries so it
|
|
116
|
+
matches this file. The two changelogs are now in sync.
|
|
117
|
+
|
|
6
118
|
## 0.5.6 — 2026-05-13
|
|
7
119
|
|
|
8
120
|
A rollup release that consolidates 11 days of security-review
|
|
@@ -234,12 +234,74 @@ with no `mkdir` or `touch` step.
|
|
|
234
234
|
- Sliding window for failures, also TTL on the `login_attempts` collection.
|
|
235
235
|
* - `login_max_per_minute`
|
|
236
236
|
- `5`
|
|
237
|
-
-
|
|
237
|
+
- Deprecated since 0.5.4; kept for back-compat but no longer
|
|
238
|
+
wired. Use `login_rate_limit` (see "Per-route rate limits" below).
|
|
238
239
|
* - `login_max_per_hour`
|
|
239
240
|
- `20`
|
|
240
|
-
- Same
|
|
241
|
+
- Same as above. Use a slowapi-syntax limit string on
|
|
242
|
+
`login_rate_limit` like `"5/minute;20/hour"`.
|
|
241
243
|
```
|
|
242
244
|
|
|
245
|
+
## Per-route rate limits
|
|
246
|
+
|
|
247
|
+
New in 0.5.4. Opt-in: install the `rate_limit` extra (`pip install
|
|
248
|
+
'regstack[rate_limit]'` — pulls in `slowapi`) or pass your own
|
|
249
|
+
`slowapi.Limiter` instance as `RegStack(rate_limiter=...)`.
|
|
250
|
+
|
|
251
|
+
Each field is a slowapi-syntax string. Empty / unset = no limit on
|
|
252
|
+
that route. The per-account `LockoutService` (see "Lockout (login)"
|
|
253
|
+
above) is unchanged and stacks on top of `login_rate_limit` — they
|
|
254
|
+
defend different axes: lockout defends one account against
|
|
255
|
+
credential-stuffing; the IP rate limits defend each endpoint
|
|
256
|
+
against one IP spamming requests across many accounts.
|
|
257
|
+
|
|
258
|
+
```{list-table}
|
|
259
|
+
:header-rows: 1
|
|
260
|
+
:widths: 35 15 50
|
|
261
|
+
|
|
262
|
+
* - Field
|
|
263
|
+
- Default
|
|
264
|
+
- Notes
|
|
265
|
+
|
|
266
|
+
* - `login_rate_limit`
|
|
267
|
+
- `""`
|
|
268
|
+
- Per-IP on `POST /login` (and the MFA confirm step).
|
|
269
|
+
* - `register_rate_limit`
|
|
270
|
+
- `""`
|
|
271
|
+
- Per-IP on `POST /register`.
|
|
272
|
+
* - `forgot_password_rate_limit`
|
|
273
|
+
- `""`
|
|
274
|
+
- Per-IP on `POST /forgot-password`.
|
|
275
|
+
* - `reset_password_rate_limit`
|
|
276
|
+
- `""`
|
|
277
|
+
- Per-IP on `POST /reset-password`.
|
|
278
|
+
* - `verify_rate_limit`
|
|
279
|
+
- `""`
|
|
280
|
+
- Per-IP on `POST /verify`.
|
|
281
|
+
* - `resend_verification_rate_limit`
|
|
282
|
+
- `""`
|
|
283
|
+
- Per-IP on `POST /resend-verification`.
|
|
284
|
+
* - `change_password_rate_limit`
|
|
285
|
+
- `""`
|
|
286
|
+
- Per-IP on `POST /change-password`.
|
|
287
|
+
* - `change_email_rate_limit`
|
|
288
|
+
- `""`
|
|
289
|
+
- Per-IP on `POST /change-email`.
|
|
290
|
+
* - `confirm_email_change_rate_limit`
|
|
291
|
+
- `""`
|
|
292
|
+
- Per-IP on `POST /confirm-email-change`.
|
|
293
|
+
* - `delete_account_rate_limit`
|
|
294
|
+
- `""`
|
|
295
|
+
- Per-IP on `DELETE /account`.
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
If any `*_rate_limit` is set but neither a `rate_limiter=` argument
|
|
299
|
+
nor the `rate_limit` extra is available, `RegStack.router` raises
|
|
300
|
+
`RuntimeError` on first access — failing closed beats silently
|
|
301
|
+
disabling the protection. The host owns `app.state.limiter` and
|
|
302
|
+
the `RateLimitExceeded` exception handler; slowapi itself defines
|
|
303
|
+
the 429 response shape.
|
|
304
|
+
|
|
243
305
|
## SMS / 2FA
|
|
244
306
|
|
|
245
307
|
```{list-table}
|
|
@@ -10,10 +10,13 @@ are tradeoffs, they're documented here rather than buried in code.
|
|
|
10
10
|
|
|
11
11
|
## Passwords
|
|
12
12
|
|
|
13
|
-
- **Hashing.** Argon2id with library defaults
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
- **Hashing.** Argon2id with library defaults via `pwdlib`.
|
|
14
|
+
regstack does not expose a standalone "needs rehash?" method —
|
|
15
|
+
if the Argon2 parameters change later and you want existing
|
|
16
|
+
hashes upgraded on next login, call
|
|
17
|
+
`pwdlib.PasswordHash.verify_and_update(password, hashed)`
|
|
18
|
+
directly inside a host-side `user_logged_in` hook (it returns
|
|
19
|
+
both `(verified, new_hash_or_None)` in one pass).
|
|
17
20
|
- **Length.** Minimum 8, maximum 128 (UTF-8). Validated by the
|
|
18
21
|
pydantic input model on every create / change endpoint.
|
|
19
22
|
- **Storage.** Plaintext is never logged or returned. The
|
{regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/repositories/blacklist_repo.py
RENAMED
|
@@ -36,5 +36,8 @@ class BlacklistRepo:
|
|
|
36
36
|
# sweep — useful in tests and on Mongos where the TTL monitor's 60-second
|
|
37
37
|
# cycle hasn't fired yet.
|
|
38
38
|
reference = now or datetime.now(UTC)
|
|
39
|
-
|
|
39
|
+
# Strict `<` matches the rest of the purge_expired family across both
|
|
40
|
+
# backends — a token whose exp is the reference instant is still
|
|
41
|
+
# nominally valid for one more microsecond.
|
|
42
|
+
result = await self._collection.delete_many({"exp": {"$lt": reference}})
|
|
40
43
|
return int(result.deleted_count)
|
{regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/mongo/repositories/oauth_state_repo.py
RENAMED
|
@@ -25,10 +25,19 @@ class MongoOAuthStateRepo:
|
|
|
25
25
|
doc = await self._collection.find_one({"_id": state_id})
|
|
26
26
|
return self._hydrate(doc)
|
|
27
27
|
|
|
28
|
-
async def set_result_token(
|
|
28
|
+
async def set_result_token(
|
|
29
|
+
self,
|
|
30
|
+
state_id: str,
|
|
31
|
+
token: str,
|
|
32
|
+
*,
|
|
33
|
+
new_expires_at: datetime | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
updates: dict[str, Any] = {"result_token": token}
|
|
36
|
+
if new_expires_at is not None:
|
|
37
|
+
updates["expires_at"] = new_expires_at
|
|
29
38
|
await self._collection.update_one(
|
|
30
39
|
{"_id": state_id},
|
|
31
|
-
{"$set":
|
|
40
|
+
{"$set": updates},
|
|
32
41
|
)
|
|
33
42
|
|
|
34
43
|
async def consume(self, state_id: str) -> OAuthState | None:
|
|
@@ -4,9 +4,7 @@ from datetime import UTC, datetime
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from bson import ObjectId
|
|
7
|
-
from pymongo.errors import DuplicateKeyError
|
|
8
7
|
|
|
9
|
-
from regstack.backends.protocols import PendingAlreadyExistsError
|
|
10
8
|
from regstack.models.pending_registration import PendingRegistration
|
|
11
9
|
|
|
12
10
|
if TYPE_CHECKING:
|
|
@@ -36,14 +34,6 @@ class PendingRepo:
|
|
|
36
34
|
pending.id = str(result["_id"])
|
|
37
35
|
return pending
|
|
38
36
|
|
|
39
|
-
async def create(self, pending: PendingRegistration) -> PendingRegistration:
|
|
40
|
-
try:
|
|
41
|
-
result = await self._collection.insert_one(pending.to_mongo())
|
|
42
|
-
except DuplicateKeyError as exc:
|
|
43
|
-
raise PendingAlreadyExistsError(pending.email) from exc
|
|
44
|
-
pending.id = str(result.inserted_id)
|
|
45
|
-
return pending
|
|
46
|
-
|
|
47
37
|
async def find_by_token_hash(self, token_hash: str) -> PendingRegistration | None:
|
|
48
38
|
doc = await self._collection.find_one({"token_hash": token_hash})
|
|
49
39
|
return self._hydrate(doc)
|
|
@@ -52,11 +42,6 @@ class PendingRepo:
|
|
|
52
42
|
doc = await self._collection.find_one({"email": email})
|
|
53
43
|
return self._hydrate(doc)
|
|
54
44
|
|
|
55
|
-
async def delete_by_id(self, pending_id: str) -> None:
|
|
56
|
-
if not ObjectId.is_valid(pending_id):
|
|
57
|
-
return
|
|
58
|
-
await self._collection.delete_one({"_id": ObjectId(pending_id)})
|
|
59
|
-
|
|
60
45
|
async def delete_by_email(self, email: str) -> None:
|
|
61
46
|
await self._collection.delete_one({"email": email})
|
|
62
47
|
|
|
@@ -289,9 +289,22 @@ class OAuthStateRepoProtocol(Protocol):
|
|
|
289
289
|
|
|
290
290
|
async def find(self, state_id: str) -> OAuthState | None: ...
|
|
291
291
|
|
|
292
|
-
async def set_result_token(
|
|
292
|
+
async def set_result_token(
|
|
293
|
+
self,
|
|
294
|
+
state_id: str,
|
|
295
|
+
token: str,
|
|
296
|
+
*,
|
|
297
|
+
new_expires_at: datetime | None = None,
|
|
298
|
+
) -> None:
|
|
293
299
|
"""Stash the session JWT after a successful callback so the
|
|
294
300
|
SPA can pick it up via :meth:`consume`.
|
|
301
|
+
|
|
302
|
+
If ``new_expires_at`` is given, the row's ``expires_at`` is
|
|
303
|
+
bumped to that timestamp at the same time — this is how
|
|
304
|
+
callers shorten the redemption window from
|
|
305
|
+
``oauth.state_ttl_seconds`` (covering the round-trip with
|
|
306
|
+
the provider) to ``oauth.completion_ttl_seconds`` (covering
|
|
307
|
+
only the SPA's exchange call after the callback lands).
|
|
295
308
|
"""
|
|
296
309
|
...
|
|
297
310
|
|
{regstack-0.5.6 → regstack-0.5.9}/src/regstack/backends/sql/repositories/oauth_state_repo.py
RENAMED
|
@@ -44,8 +44,17 @@ class SqlOAuthStateRepo:
|
|
|
44
44
|
row = (await conn.execute(stmt)).first()
|
|
45
45
|
return _row_to_state(row) if row else None
|
|
46
46
|
|
|
47
|
-
async def set_result_token(
|
|
48
|
-
|
|
47
|
+
async def set_result_token(
|
|
48
|
+
self,
|
|
49
|
+
state_id: str,
|
|
50
|
+
token: str,
|
|
51
|
+
*,
|
|
52
|
+
new_expires_at: datetime | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
values: dict[str, Any] = {"result_token": token}
|
|
55
|
+
if new_expires_at is not None:
|
|
56
|
+
values["expires_at"] = new_expires_at
|
|
57
|
+
stmt = update(self._t).where(self._t.c.id == state_id).values(**values)
|
|
49
58
|
async with self._engine.begin() as conn:
|
|
50
59
|
await conn.execute(stmt)
|
|
51
60
|
|
|
@@ -23,6 +23,12 @@ class BaseUser(BaseModel):
|
|
|
23
23
|
mixin via ``RegStack.extend_user_model``.
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
+
# `extra="allow"` here (and only here) is deliberate: hosts that
|
|
27
|
+
# add their own user fields via subclassing or
|
|
28
|
+
# `RegStack.extend_user_model` need those fields to round-trip
|
|
29
|
+
# through the DB cleanly. Every request/response model
|
|
30
|
+
# (UserCreate, UserUpdate, UserPublic) uses `extra="forbid"`
|
|
31
|
+
# because those are the external API contract.
|
|
26
32
|
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
27
33
|
|
|
28
34
|
id: IdStr | None = Field(default=None, alias="_id")
|
|
@@ -88,7 +94,12 @@ class UserPublic(BaseModel):
|
|
|
88
94
|
phone_number: str | None = None
|
|
89
95
|
is_mfa_enabled: bool = False
|
|
90
96
|
created_at: datetime
|
|
97
|
+
updated_at: datetime
|
|
91
98
|
last_login: datetime | None = None
|
|
99
|
+
tokens_invalidated_after: datetime | None = None
|
|
100
|
+
"""Bulk-revoke cutoff. SPAs comparing this to their session JWT's
|
|
101
|
+
`iat` can tell when the token they hold has been invalidated by a
|
|
102
|
+
password change / email change without making another request."""
|
|
92
103
|
|
|
93
104
|
@classmethod
|
|
94
105
|
def from_user(cls, user: BaseUser) -> UserPublic:
|
|
@@ -104,5 +115,7 @@ class UserPublic(BaseModel):
|
|
|
104
115
|
phone_number=user.phone_number,
|
|
105
116
|
is_mfa_enabled=user.is_mfa_enabled,
|
|
106
117
|
created_at=user.created_at,
|
|
118
|
+
updated_at=user.updated_at,
|
|
107
119
|
last_login=user.last_login,
|
|
120
|
+
tokens_invalidated_after=user.tokens_invalidated_after,
|
|
108
121
|
)
|
|
@@ -237,7 +237,7 @@ def _encode_email_change_token(rs: RegStack, user_id: str, new_email: str, ttl:
|
|
|
237
237
|
payload: dict[str, Any] = {
|
|
238
238
|
"sub": user_id,
|
|
239
239
|
"jti": _secrets.token_urlsafe(16),
|
|
240
|
-
"iat":
|
|
240
|
+
"iat": now.timestamp(),
|
|
241
241
|
"exp": int((now + timedelta(seconds=ttl)).timestamp()),
|
|
242
242
|
"purpose": _EMAIL_CHANGE_PURPOSE,
|
|
243
243
|
_NEW_EMAIL_CLAIM: new_email,
|