regstack 0.5.9__tar.gz → 0.6.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.
- {regstack-0.5.9 → regstack-0.6.0}/.github/workflows/publish.yml +18 -5
- {regstack-0.5.9 → regstack-0.6.0}/.github/workflows/test.yml +21 -7
- {regstack-0.5.9 → regstack-0.6.0}/.gitignore +12 -0
- {regstack-0.5.9 → regstack-0.6.0}/CHANGELOG.md +89 -0
- {regstack-0.5.9 → regstack-0.6.0}/PKG-INFO +9 -6
- {regstack-0.5.9 → regstack-0.6.0}/docs/changelog.md +96 -0
- {regstack-0.5.9 → regstack-0.6.0}/pyproject.toml +22 -8
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/__main__.py +37 -7
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/account.py +19 -6
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/admin.py +15 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/login.py +17 -5
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/oauth.py +28 -3
- regstack-0.6.0/src/regstack/version.py +1 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_account_management.py +32 -12
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_admin_router.py +31 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_oauth_google_router.py +26 -0
- regstack-0.6.0/tests/unit/test_cli_wizard_missing_extra.py +59 -0
- {regstack-0.5.9 → regstack-0.6.0}/uv.lock +16 -10
- regstack-0.5.9/src/regstack/version.py +0 -1
- {regstack-0.5.9 → regstack-0.6.0}/.python-version +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/.readthedocs.yaml +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/CLAUDE.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/LICENSE +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/NOTICE +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/README.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/SECURITY.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/_static/.gitkeep +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/_templates/.gitkeep +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/api.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/architecture.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/cli.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/conf.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/configuration.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/embedding.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/index.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/oauth.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/quickstart.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/security-reports/README.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/security.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/docs/theming.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/_common/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/_common/app.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/mongo/README.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/mongo/branding/theme.css +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/mongo/main.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/mongo/regstack.toml +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/postgres/README.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/postgres/main.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/postgres/regstack.toml +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/sqlite/README.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/sqlite/main.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/examples/sqlite/regstack.toml +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/regstack.toml.example +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/scripts/ccr_coverage_setup.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/scripts/security-review-prompt.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/app.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/clock.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/dependencies.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/jwt.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/lockout.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/password.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/rate_limit.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/base.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/factory.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/backend.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/client.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/indexes.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/protocols.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/backend.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/migrations/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/migrations/env.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/oauth_state_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/schema.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/backends/sql/types.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/admin.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/doctor.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/init.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/config/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/config/loader.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/config/schema.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/config/secrets.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/base.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/composer.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/console.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/factory.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/ses.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/smtp.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/hooks/events.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/_objectid.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/oauth_identity.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/oauth_state.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/models/user.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/base.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/errors.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/providers/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/providers/google.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/oauth/registry.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/_helpers.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/logout.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/password.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/phone.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/register.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/routers/verify.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/base.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/factory.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/null.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/sns.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/sms/twilio.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/pages.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/static/js/regstack.js +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/login.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/me.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/cli.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/routes.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/server.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/validators.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/window.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/oauth_google/writer.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/cli.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/routes.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/server.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/static/designer.css +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/static/designer.js +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/templates/designer.html +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/validators.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/window.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/src/regstack/wizard/theme_designer/writer.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tasks/oauth-design.md +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tasks.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/_fake_google/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/_fake_google/provider.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/conftest.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/e2e/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/e2e/conftest.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/e2e/test_theme_designer.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/e2e/test_wizard_oauth_flow.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_happy_path.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_indexes.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_login_lockout.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_mfa.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_oauth_repos.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_oauth_ui.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_password_reset.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_rate_limits.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_sql_migrations.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_ui_router.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/integration/test_verification.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/__init__.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_base_install_imports.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_cli.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_cli_doctor.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_cli_init.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_cli_migrate.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_config_loader.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_jwt.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_lockout.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_mail_composer.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_mfa_code_repo.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_oauth_google.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_password.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_ses_backend.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_sms.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_smtp_backend.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_theme_designer_cli.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_theme_designer_routes.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_theme_designer_validators.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_theme_designer_writer.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_ui_env.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_wizard_oauth_cli.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_wizard_oauth_routes.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_wizard_oauth_validators.py +0 -0
- {regstack-0.5.9 → regstack-0.6.0}/tests/unit/test_wizard_oauth_writer.py +0 -0
|
@@ -4,6 +4,11 @@ name: publish
|
|
|
4
4
|
# Configure the PyPI project (https://pypi.org/manage/account/publishing/)
|
|
5
5
|
# with the publisher: this repository, environment name `pypi`, workflow
|
|
6
6
|
# `publish.yml`. No PyPI tokens live in GitHub Actions secrets.
|
|
7
|
+
#
|
|
8
|
+
# Third-party actions are pinned to commit SHAs (not mutable tags) so a
|
|
9
|
+
# tag swap upstream can't substitute a malicious version. Update by
|
|
10
|
+
# resolving the latest SHA for the version you want and bumping the
|
|
11
|
+
# trailing `# vN` comment to match.
|
|
7
12
|
|
|
8
13
|
on:
|
|
9
14
|
push:
|
|
@@ -11,15 +16,22 @@ on:
|
|
|
11
16
|
- "v*"
|
|
12
17
|
workflow_dispatch:
|
|
13
18
|
|
|
19
|
+
# Workflow-level default. Individual jobs override where they need
|
|
20
|
+
# extra scopes (the `publish` job adds `id-token: write` for OIDC).
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
|
|
14
24
|
jobs:
|
|
15
25
|
build:
|
|
16
26
|
runs-on: ubuntu-latest
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
17
29
|
steps:
|
|
18
|
-
- uses: actions/checkout@v4
|
|
30
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
19
31
|
with:
|
|
20
32
|
fetch-depth: 0
|
|
21
33
|
|
|
22
|
-
- uses: astral-sh/setup-uv@v3
|
|
34
|
+
- uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
|
|
23
35
|
with:
|
|
24
36
|
enable-cache: true
|
|
25
37
|
|
|
@@ -42,7 +54,7 @@ jobs:
|
|
|
42
54
|
- name: Build sdist + wheel
|
|
43
55
|
run: uv build
|
|
44
56
|
|
|
45
|
-
- uses: actions/upload-artifact@v4
|
|
57
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
46
58
|
with:
|
|
47
59
|
name: dist
|
|
48
60
|
path: dist/
|
|
@@ -55,9 +67,10 @@ jobs:
|
|
|
55
67
|
name: pypi
|
|
56
68
|
url: https://pypi.org/p/regstack
|
|
57
69
|
permissions:
|
|
58
|
-
id-token: write # OIDC trusted-publisher
|
|
70
|
+
id-token: write # OIDC trusted-publisher exchange
|
|
71
|
+
contents: read
|
|
59
72
|
steps:
|
|
60
|
-
- uses: actions/download-artifact@v4
|
|
73
|
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
61
74
|
with:
|
|
62
75
|
name: dist
|
|
63
76
|
path: dist/
|
|
@@ -10,9 +10,19 @@ concurrency:
|
|
|
10
10
|
group: test-${{ github.ref }}
|
|
11
11
|
cancel-in-progress: true
|
|
12
12
|
|
|
13
|
+
# Workflow-level default — every job in this workflow gets read-only
|
|
14
|
+
# permissions unless it overrides explicitly. Third-party actions are
|
|
15
|
+
# pinned to commit SHAs so a tag swap upstream can't substitute a
|
|
16
|
+
# malicious version; update by resolving the latest SHA for the
|
|
17
|
+
# version you want and bumping the trailing `# vN` comment.
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
13
21
|
jobs:
|
|
14
22
|
pytest:
|
|
15
23
|
runs-on: ubuntu-latest
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
16
26
|
strategy:
|
|
17
27
|
fail-fast: false
|
|
18
28
|
matrix:
|
|
@@ -47,9 +57,9 @@ jobs:
|
|
|
47
57
|
# Connect as superuser so the per-test fixture can CREATE/DROP DATABASE.
|
|
48
58
|
REGSTACK_TEST_POSTGRES_URL: postgresql+asyncpg://regstack:regstack@localhost:5432
|
|
49
59
|
steps:
|
|
50
|
-
- uses: actions/checkout@v4
|
|
60
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
51
61
|
|
|
52
|
-
- uses: astral-sh/setup-uv@v3
|
|
62
|
+
- uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
|
|
53
63
|
with:
|
|
54
64
|
enable-cache: true
|
|
55
65
|
cache-dependency-glob: "uv.lock"
|
|
@@ -71,16 +81,18 @@ jobs:
|
|
|
71
81
|
|
|
72
82
|
docs:
|
|
73
83
|
runs-on: ubuntu-latest
|
|
84
|
+
permissions:
|
|
85
|
+
contents: read
|
|
74
86
|
steps:
|
|
75
|
-
- uses: actions/checkout@v4
|
|
76
|
-
- uses: astral-sh/setup-uv@v3
|
|
87
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
88
|
+
- uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
|
|
77
89
|
with:
|
|
78
90
|
enable-cache: true
|
|
79
91
|
cache-dependency-glob: "uv.lock"
|
|
80
92
|
- run: uv python pin 3.11
|
|
81
93
|
- run: uv sync --extra docs --extra dev
|
|
82
94
|
- run: uv run sphinx-build -b html -W --keep-going docs docs/_build/html
|
|
83
|
-
- uses: actions/upload-artifact@v4
|
|
95
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
84
96
|
with:
|
|
85
97
|
name: docs-html
|
|
86
98
|
path: docs/_build/html
|
|
@@ -93,9 +105,11 @@ jobs:
|
|
|
93
105
|
# equivalent in-process via meta_path blocking; this job verifies the
|
|
94
106
|
# actual built wheel installs and imports against a clean Python.
|
|
95
107
|
runs-on: ubuntu-latest
|
|
108
|
+
permissions:
|
|
109
|
+
contents: read
|
|
96
110
|
steps:
|
|
97
|
-
- uses: actions/checkout@v4
|
|
98
|
-
- uses: astral-sh/setup-uv@v3
|
|
111
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
112
|
+
- uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
|
|
99
113
|
with:
|
|
100
114
|
enable-cache: true
|
|
101
115
|
cache-dependency-glob: "uv.lock"
|
|
@@ -32,3 +32,15 @@ docs/_autosummary/
|
|
|
32
32
|
/regstack-bootstrap.json
|
|
33
33
|
**/regstack.secrets.env
|
|
34
34
|
**/regstack-bootstrap.json
|
|
35
|
+
|
|
36
|
+
# Defensive: keep credential material out of any commit, even from a
|
|
37
|
+
# misconfigured local dev env. None of these are checked in today, but
|
|
38
|
+
# the .env / *.pem / etc. patterns are recurring audit recommendations.
|
|
39
|
+
.env
|
|
40
|
+
.env.*
|
|
41
|
+
*.pem
|
|
42
|
+
*.key
|
|
43
|
+
*.p12
|
|
44
|
+
*.pfx
|
|
45
|
+
*.jks
|
|
46
|
+
*.crt
|
|
@@ -5,6 +5,95 @@ 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.6.0 — 2026-05-14
|
|
9
|
+
|
|
10
|
+
**Breaking change for wizard users.** The GUI setup wizards
|
|
11
|
+
(`regstack oauth setup`, `regstack theme design`) are now behind a
|
|
12
|
+
new optional `wizard` extra. `pip install regstack` no longer pulls
|
|
13
|
+
in `pywebview`, `tomlkit`, or `uvicorn[standard]` — three heavy
|
|
14
|
+
wizard-only dependencies that every library consumer was paying for,
|
|
15
|
+
including a platform browser engine on every fresh install. A
|
|
16
|
+
recurring audit recommendation since 0.5.0.
|
|
17
|
+
|
|
18
|
+
**Migration.**
|
|
19
|
+
|
|
20
|
+
- If you only embed regstack in a FastAPI app (no `regstack oauth
|
|
21
|
+
setup` or `regstack theme design`): no action needed. The base
|
|
22
|
+
install is now significantly slimmer.
|
|
23
|
+
- If you use either setup wizard: install the new extra —
|
|
24
|
+
`pip install 'regstack[wizard]'` or `uv sync --extra wizard`.
|
|
25
|
+
Running a wizard subcommand without the extra now exits with a
|
|
26
|
+
one-line install hint (no ImportError traceback).
|
|
27
|
+
- The `dev` extra continues to pull in the wizard deps directly so
|
|
28
|
+
`inv test-all` keeps working without an explicit `--extra wizard`.
|
|
29
|
+
|
|
30
|
+
Bumped to **0.6.0** (not 0.5.12) because removing top-level deps is
|
|
31
|
+
the kind of change that can surprise downstream `pip install
|
|
32
|
+
regstack` callers — even though "the GUI wizard CLIs need an extra
|
|
33
|
+
now" is the only observable effect.
|
|
34
|
+
|
|
35
|
+
## 0.5.11 — 2026-05-14
|
|
36
|
+
|
|
37
|
+
CI / workflow hygiene. No runtime code changes.
|
|
38
|
+
|
|
39
|
+
- **All third-party GitHub Actions pinned to commit SHAs.**
|
|
40
|
+
`actions/checkout@v4`, `astral-sh/setup-uv@v3`,
|
|
41
|
+
`actions/upload-artifact@v4`, and `actions/download-artifact@v4` now
|
|
42
|
+
use commit SHAs in `.github/workflows/publish.yml` and
|
|
43
|
+
`.github/workflows/test.yml`, with `# v4` / `# v3` trailing comments
|
|
44
|
+
so future operators can resolve and bump. `pypa/gh-action-pypi-publish`
|
|
45
|
+
was already SHA-pinned (#37). A tag swap upstream can no longer
|
|
46
|
+
substitute a malicious version.
|
|
47
|
+
- **`permissions:` blocks added to every workflow + job.** Both
|
|
48
|
+
workflows now declare a `permissions: contents: read` default at
|
|
49
|
+
the workflow level and re-state it per job (so a future addition of
|
|
50
|
+
a write-needing action doesn't silently inherit elevated scopes).
|
|
51
|
+
The `publish` job continues to declare `id-token: write` (OIDC
|
|
52
|
+
trusted-publisher exchange) — that's the only scope above
|
|
53
|
+
read-only anywhere in the workflows.
|
|
54
|
+
- **`.gitignore` defensive additions.** `.env`, `.env.*`, and the
|
|
55
|
+
common credential-file patterns (`*.pem`, `*.key`, `*.p12`,
|
|
56
|
+
`*.pfx`, `*.jks`, `*.crt`) are now ignored at the repo root. None
|
|
57
|
+
are present today; this is belt-and-braces for misconfigured local
|
|
58
|
+
dev environments. A recurring audit recommendation.
|
|
59
|
+
|
|
60
|
+
## 0.5.10 — 2026-05-14
|
|
61
|
+
|
|
62
|
+
Security fixes from the 2026-05-13 / 2026-05-14 daily reviews. All
|
|
63
|
+
warnings, no criticals — but several are real exploitable issues.
|
|
64
|
+
|
|
65
|
+
**Security.**
|
|
66
|
+
|
|
67
|
+
- **Open-redirect bypass in OAuth `redirect_to`.** `_validate_redirect`
|
|
68
|
+
was forwarding `urlsplit`'s judgment, but browsers normalize values
|
|
69
|
+
like `/\evil.com` and `////evil.com` into the protocol-relative
|
|
70
|
+
`//evil.com` — both of which `urlsplit` reports as same-origin
|
|
71
|
+
paths. The validator now rejects any backslash plus any value that
|
|
72
|
+
doesn't start with a single `/` followed by a non-slash character.
|
|
73
|
+
- **CVE-2025-62727 — `fastapi` floor raised to `>=0.120.0`.** Starlette
|
|
74
|
+
DoS via large request bodies after multipart processing.
|
|
75
|
+
- **CVE-2025-27516 — `jinja2` floor raised to `>=3.1.6`.** Sandbox
|
|
76
|
+
breakout via the `|attr` filter (only relevant if hosts allow
|
|
77
|
+
user-controlled templates; tightening the floor regardless).
|
|
78
|
+
- **Login lockout no longer skips disabled / unverified accounts.**
|
|
79
|
+
`POST /login` now records a failure before raising HTTP 403 for
|
|
80
|
+
`is_active=False` and (when `require_verification=True`)
|
|
81
|
+
`is_verified=False` users. Password verification was also re-ordered
|
|
82
|
+
to run **before** those checks, so an attacker without the password
|
|
83
|
+
can't distinguish disabled vs active accounts by HTTP code alone.
|
|
84
|
+
- **`POST /change-email` no longer enumerates registered addresses.**
|
|
85
|
+
An authenticated attacker could previously iterate the email
|
|
86
|
+
namespace via the 409 vs 202 response distinction. The endpoint now
|
|
87
|
+
always returns 202; if the candidate is already registered, no
|
|
88
|
+
confirmation email is sent (the legitimate user finds out by not
|
|
89
|
+
receiving it). Matches the existing anti-enumeration stance on
|
|
90
|
+
`/forgot-password` and `/resend-verification`.
|
|
91
|
+
- **Admin resend-verification rejects OAuth-only users.** Previously
|
|
92
|
+
attempted to construct a `PendingRegistration` from a user with
|
|
93
|
+
`hashed_password=None`, which either failed validation or stored
|
|
94
|
+
the literal string `"None"` in the pending row. Now returns 400
|
|
95
|
+
with a clear message.
|
|
96
|
+
|
|
8
97
|
## 0.5.9 — 2026-05-13
|
|
9
98
|
|
|
10
99
|
**`OAuthConfig.enforce_mfa_on_oauth_signin` is now wired.** The flag
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jdrumgoole/regstack
|
|
6
6
|
Project-URL: Repository, https://github.com/jdrumgoole/regstack
|
|
@@ -22,17 +22,14 @@ Requires-Dist: alembic>=1.13
|
|
|
22
22
|
Requires-Dist: click>=8.1
|
|
23
23
|
Requires-Dist: dnspython>=2.6
|
|
24
24
|
Requires-Dist: email-validator>=2.1
|
|
25
|
-
Requires-Dist: fastapi>=0.
|
|
26
|
-
Requires-Dist: jinja2>=3.1
|
|
25
|
+
Requires-Dist: fastapi>=0.120.0
|
|
26
|
+
Requires-Dist: jinja2>=3.1.6
|
|
27
27
|
Requires-Dist: pwdlib[argon2]>=0.2.1
|
|
28
28
|
Requires-Dist: pydantic-settings>=2.2
|
|
29
29
|
Requires-Dist: pydantic>=2.6
|
|
30
30
|
Requires-Dist: pyjwt>=2.12.1
|
|
31
31
|
Requires-Dist: python-multipart>=0.0.26
|
|
32
|
-
Requires-Dist: pywebview>=5.0
|
|
33
32
|
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
34
|
-
Requires-Dist: tomlkit>=0.13
|
|
35
|
-
Requires-Dist: uvicorn[standard]>=0.29
|
|
36
33
|
Provides-Extra: dev
|
|
37
34
|
Requires-Dist: anyio>=4.3; extra == 'dev'
|
|
38
35
|
Requires-Dist: asyncpg>=0.29; extra == 'dev'
|
|
@@ -47,8 +44,10 @@ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
|
47
44
|
Requires-Dist: pytest-playwright>=0.5; extra == 'dev'
|
|
48
45
|
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
|
|
49
46
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
47
|
+
Requires-Dist: pywebview>=5.0; extra == 'dev'
|
|
50
48
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
51
49
|
Requires-Dist: slowapi>=0.1.9; extra == 'dev'
|
|
50
|
+
Requires-Dist: tomlkit>=0.13; extra == 'dev'
|
|
52
51
|
Requires-Dist: uvicorn[standard]>=0.29; extra == 'dev'
|
|
53
52
|
Provides-Extra: docs
|
|
54
53
|
Requires-Dist: furo>=2024.1; extra == 'docs'
|
|
@@ -72,6 +71,10 @@ Provides-Extra: sns
|
|
|
72
71
|
Requires-Dist: aioboto3>=12.3; extra == 'sns'
|
|
73
72
|
Provides-Extra: twilio
|
|
74
73
|
Requires-Dist: twilio>=9.0; extra == 'twilio'
|
|
74
|
+
Provides-Extra: wizard
|
|
75
|
+
Requires-Dist: pywebview>=5.0; extra == 'wizard'
|
|
76
|
+
Requires-Dist: tomlkit>=0.13; extra == 'wizard'
|
|
77
|
+
Requires-Dist: uvicorn[standard]>=0.29; extra == 'wizard'
|
|
75
78
|
Description-Content-Type: text/markdown
|
|
76
79
|
|
|
77
80
|
# regstack
|
|
@@ -3,6 +3,102 @@
|
|
|
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.6.0 — 2026-05-14
|
|
7
|
+
|
|
8
|
+
### Changed (BREAKING — wizard install)
|
|
9
|
+
|
|
10
|
+
- **GUI setup wizards now require the optional `wizard` extra.**
|
|
11
|
+
`regstack oauth setup` and `regstack theme design` previously
|
|
12
|
+
worked from a bare `pip install regstack` because their deps —
|
|
13
|
+
`pywebview`, `tomlkit`, and `uvicorn[standard]` — were in the base
|
|
14
|
+
dependency list. That meant every library consumer (including pure
|
|
15
|
+
FastAPI host apps that never run a setup wizard) was paying for a
|
|
16
|
+
platform browser engine and an ASGI server at install time.
|
|
17
|
+
Those three packages now live in a new `wizard` extra; the base
|
|
18
|
+
install is significantly slimmer.
|
|
19
|
+
|
|
20
|
+
**Migration.**
|
|
21
|
+
|
|
22
|
+
- If you embed regstack but don't use the setup wizards: no
|
|
23
|
+
action needed. `import regstack`, `RegStack(...)`, and the
|
|
24
|
+
`regstack init` / `regstack doctor` / `regstack migrate` /
|
|
25
|
+
`regstack create-admin` CLIs all work on the bare install.
|
|
26
|
+
- If you use either setup wizard:
|
|
27
|
+
`pip install 'regstack[wizard]'` or
|
|
28
|
+
`uv sync --extra wizard`.
|
|
29
|
+
- The `dev` extra continues to pull in the wizard deps directly,
|
|
30
|
+
so `inv test-all` keeps working without an explicit `--extra
|
|
31
|
+
wizard`.
|
|
32
|
+
|
|
33
|
+
Running a wizard subcommand without the extra installed now
|
|
34
|
+
exits with a one-line install hint and a non-zero exit code,
|
|
35
|
+
rather than an `ImportError` traceback from deep inside the
|
|
36
|
+
wizard subtree.
|
|
37
|
+
|
|
38
|
+
Bumped to **0.6.0** rather than 0.5.12 because removing top-level
|
|
39
|
+
dependencies can surprise downstream callers; the version line
|
|
40
|
+
signals it's worth a glance at the migration note.
|
|
41
|
+
|
|
42
|
+
## 0.5.11 — 2026-05-14
|
|
43
|
+
|
|
44
|
+
CI / workflow hygiene. No runtime code changes.
|
|
45
|
+
|
|
46
|
+
### Security
|
|
47
|
+
|
|
48
|
+
- **All third-party GitHub Actions pinned to commit SHAs.**
|
|
49
|
+
`actions/checkout@v4`, `astral-sh/setup-uv@v3`,
|
|
50
|
+
`actions/upload-artifact@v4`, and `actions/download-artifact@v4`
|
|
51
|
+
now use commit SHAs across both workflows (`pypa/gh-action-pypi-publish`
|
|
52
|
+
was already SHA-pinned in 0.5.6). Tag swaps upstream can no longer
|
|
53
|
+
substitute a malicious version.
|
|
54
|
+
- **`permissions:` blocks declared on every workflow + job.** Both
|
|
55
|
+
workflows declare a `permissions: contents: read` default at the
|
|
56
|
+
workflow level and re-state it per job. The `publish` job
|
|
57
|
+
continues to add `id-token: write` for the OIDC trusted-publisher
|
|
58
|
+
exchange — that's the only scope above read-only anywhere in
|
|
59
|
+
the workflows.
|
|
60
|
+
|
|
61
|
+
### Internal
|
|
62
|
+
|
|
63
|
+
- `.gitignore` gained `.env`, `.env.*`, and the common credential-file
|
|
64
|
+
patterns (`*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.crt`).
|
|
65
|
+
Belt-and-braces for misconfigured local dev envs; nothing tracked
|
|
66
|
+
today depends on these.
|
|
67
|
+
|
|
68
|
+
## 0.5.10 — 2026-05-14
|
|
69
|
+
|
|
70
|
+
Security fixes from the 2026-05-13 / 2026-05-14 daily review reports.
|
|
71
|
+
|
|
72
|
+
### Security
|
|
73
|
+
|
|
74
|
+
- **Open-redirect bypass in OAuth `redirect_to`.** `_validate_redirect`
|
|
75
|
+
was forwarding `urlsplit`'s judgment, but browsers normalize values
|
|
76
|
+
like `/\evil.com` and `////evil.com` into the protocol-relative
|
|
77
|
+
`//evil.com`. Both forms now rejected.
|
|
78
|
+
- **CVE-2025-62727 — `fastapi>=0.120.0`** (was `>=0.110`). Starlette
|
|
79
|
+
DoS via large request bodies after multipart processing.
|
|
80
|
+
- **CVE-2025-27516 — `jinja2>=3.1.6`** (was `>=3.1`). Sandbox breakout
|
|
81
|
+
via the `|attr` filter.
|
|
82
|
+
- **Login lockout coverage extended.** `POST /login` was returning
|
|
83
|
+
HTTP 403 for `is_active=False` and (when `require_verification=True`)
|
|
84
|
+
unverified accounts **without** recording a failure — an attacker
|
|
85
|
+
guessing passwords against either category had unbounded probing.
|
|
86
|
+
The endpoint now:
|
|
87
|
+
- Verifies the password **before** the active/verified checks (so
|
|
88
|
+
an unauthenticated attacker can't distinguish disabled vs active
|
|
89
|
+
accounts by HTTP code).
|
|
90
|
+
- Records a failure before raising 403 in either branch.
|
|
91
|
+
- **`POST /change-email` anti-enumeration.** An authenticated
|
|
92
|
+
attacker could walk the registered-email namespace via the 409 vs
|
|
93
|
+
202 distinction. The endpoint now always returns 202; clashes are
|
|
94
|
+
logged server-side and the confirmation email is silently skipped.
|
|
95
|
+
Matches the existing stance on `/forgot-password` and
|
|
96
|
+
`/resend-verification`.
|
|
97
|
+
- **Admin resend-verification rejects OAuth-only users** with a clear
|
|
98
|
+
400 instead of attempting to construct a `PendingRegistration` with
|
|
99
|
+
`hashed_password=None` (which corrupted the pending-registrations
|
|
100
|
+
row).
|
|
101
|
+
|
|
6
102
|
## 0.5.9 — 2026-05-13
|
|
7
103
|
|
|
8
104
|
### Security
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "regstack"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -17,13 +17,18 @@ classifiers = [
|
|
|
17
17
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
|
-
|
|
20
|
+
# fastapi>=0.120.0 picks up CVE-2025-62727 (Starlette DoS via large
|
|
21
|
+
# request bodies after multipart processing).
|
|
22
|
+
"fastapi>=0.120.0",
|
|
21
23
|
"pydantic>=2.6",
|
|
22
24
|
"pydantic-settings>=2.2",
|
|
23
25
|
"pwdlib[argon2]>=0.2.1",
|
|
24
26
|
# pyjwt>=2.12.1 includes the fix for CVE-2026-32597 (`crit` header bypass).
|
|
25
27
|
"pyjwt>=2.12.1",
|
|
26
|
-
|
|
28
|
+
# jinja2>=3.1.6 picks up CVE-2025-27516 (sandbox breakout via the
|
|
29
|
+
# `|attr` filter; only relevant if hosts allow user-controlled
|
|
30
|
+
# templates, but we tighten the floor anyway).
|
|
31
|
+
"jinja2>=3.1.6",
|
|
27
32
|
"click>=8.1",
|
|
28
33
|
"dnspython>=2.6",
|
|
29
34
|
# python-multipart>=0.0.26 picks up CVE-2026-40347 (DoS via oversized
|
|
@@ -35,11 +40,6 @@ dependencies = [
|
|
|
35
40
|
"sqlalchemy[asyncio]>=2.0",
|
|
36
41
|
"aiosqlite>=0.20",
|
|
37
42
|
"alembic>=1.13",
|
|
38
|
-
# OAuth setup wizard (regstack oauth setup) — needs a webview shell,
|
|
39
|
-
# a small in-process FastAPI server, and round-trip-safe TOML editing.
|
|
40
|
-
"pywebview>=5.0",
|
|
41
|
-
"tomlkit>=0.13",
|
|
42
|
-
"uvicorn[standard]>=0.29",
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
[project.optional-dependencies]
|
|
@@ -51,6 +51,16 @@ twilio = ["twilio>=9.0"]
|
|
|
51
51
|
# cryptography>=46.0.7 picks up CVE-2026-26007 (ECC subgroup attack on the
|
|
52
52
|
# JWKS code path) plus CVE-2026-34073 and CVE-2026-39892.
|
|
53
53
|
oauth = ["pyjwt[crypto]>=2.12.1", "cryptography>=46.0.7"]
|
|
54
|
+
# Interactive setup wizards (`regstack oauth setup`, `regstack theme
|
|
55
|
+
# design`). These open a native pywebview window over a local 127.0.0.1
|
|
56
|
+
# FastAPI server and use tomlkit for non-destructive TOML merging.
|
|
57
|
+
# Optional because library consumers who never run the GUI tools don't
|
|
58
|
+
# need a platform browser engine pulled in by pywebview.
|
|
59
|
+
wizard = [
|
|
60
|
+
"pywebview>=5.0",
|
|
61
|
+
"tomlkit>=0.13",
|
|
62
|
+
"uvicorn[standard]>=0.29",
|
|
63
|
+
]
|
|
54
64
|
# Per-route rate limiting on the auth router. The limiter itself can be
|
|
55
65
|
# host-supplied (so hosts that already use slowapi share the same Limiter
|
|
56
66
|
# state), otherwise regstack constructs an in-memory one.
|
|
@@ -86,6 +96,10 @@ dev = [
|
|
|
86
96
|
"pytest-playwright>=0.5",
|
|
87
97
|
# Rate-limit tests exercise the slowapi integration.
|
|
88
98
|
"slowapi>=0.1.9",
|
|
99
|
+
# Wizard surface (now an optional `wizard` extra) — tests exercise
|
|
100
|
+
# the setup/theme-designer routes, writers, and Playwright e2e.
|
|
101
|
+
"pywebview>=5.0",
|
|
102
|
+
"tomlkit>=0.13",
|
|
89
103
|
]
|
|
90
104
|
|
|
91
105
|
[project.scripts]
|
|
@@ -8,13 +8,39 @@ from regstack.cli.init import init as init_cmd
|
|
|
8
8
|
from regstack.cli.migrate import migrate as migrate_cmd
|
|
9
9
|
from regstack.version import __version__
|
|
10
10
|
|
|
11
|
+
_WIZARD_EXTRA_HINT = (
|
|
12
|
+
"The {subcommand} command requires the optional 'wizard' extra "
|
|
13
|
+
"(pywebview + tomlkit + uvicorn). Install with "
|
|
14
|
+
"`pip install regstack[wizard]` or `uv sync --extra wizard`."
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _missing_wizard_extra(subcommand: str) -> click.Command:
|
|
19
|
+
"""Click command that prints a clear install hint and exits non-zero.
|
|
20
|
+
|
|
21
|
+
Used as the fallback when a wizard subcommand is invoked but the
|
|
22
|
+
``wizard`` extra isn't installed — gives a one-line actionable
|
|
23
|
+
message instead of an ImportError traceback from deep inside the
|
|
24
|
+
wizard package.
|
|
25
|
+
"""
|
|
26
|
+
name = subcommand.split()[-1]
|
|
27
|
+
|
|
28
|
+
@click.command(name=name, help="(needs `regstack[wizard]`)")
|
|
29
|
+
def _stub() -> None:
|
|
30
|
+
click.echo(_WIZARD_EXTRA_HINT.format(subcommand=f"`{subcommand}`"), err=True)
|
|
31
|
+
raise SystemExit(2)
|
|
32
|
+
|
|
33
|
+
return _stub
|
|
34
|
+
|
|
11
35
|
|
|
12
36
|
class _LazyOauthGroup(click.Group):
|
|
13
37
|
"""Defer wizard imports until ``regstack oauth …`` is actually run.
|
|
14
38
|
|
|
15
|
-
Importing the wizard pulls in pywebview, uvicorn, and tomlkit
|
|
16
|
-
|
|
17
|
-
|
|
39
|
+
Importing the wizard pulls in pywebview, uvicorn, and tomlkit (the
|
|
40
|
+
``wizard`` optional extra). Hosts who don't use the GUI tools
|
|
41
|
+
don't pay for those deps at install time; the cost is that
|
|
42
|
+
running a wizard subcommand without the extra exits with a clear
|
|
43
|
+
install hint instead of an ImportError.
|
|
18
44
|
"""
|
|
19
45
|
|
|
20
46
|
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
@@ -23,8 +49,10 @@ class _LazyOauthGroup(click.Group):
|
|
|
23
49
|
def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
|
|
24
50
|
if name != "setup":
|
|
25
51
|
return None
|
|
26
|
-
|
|
27
|
-
|
|
52
|
+
try:
|
|
53
|
+
from regstack.wizard.oauth_google.cli import setup as setup_cmd
|
|
54
|
+
except ImportError:
|
|
55
|
+
return _missing_wizard_extra("regstack oauth setup")
|
|
28
56
|
return setup_cmd
|
|
29
57
|
|
|
30
58
|
|
|
@@ -37,8 +65,10 @@ class _LazyThemeGroup(click.Group):
|
|
|
37
65
|
def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
|
|
38
66
|
if name != "design":
|
|
39
67
|
return None
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
try:
|
|
69
|
+
from regstack.wizard.theme_designer.cli import design as design_cmd
|
|
70
|
+
except ImportError:
|
|
71
|
+
return _missing_wizard_extra("regstack theme design")
|
|
42
72
|
return design_cmd
|
|
43
73
|
|
|
44
74
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
4
5
|
|
|
5
6
|
import jwt as pyjwt
|
|
@@ -20,6 +21,8 @@ if TYPE_CHECKING:
|
|
|
20
21
|
_EMAIL_CHANGE_PURPOSE = "email_change"
|
|
21
22
|
_NEW_EMAIL_CLAIM = "new_email"
|
|
22
23
|
|
|
24
|
+
log = logging.getLogger("regstack.account")
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
class ChangePasswordRequest(BaseModel):
|
|
25
28
|
model_config = ConfigDict(extra="forbid")
|
|
@@ -124,12 +127,24 @@ def build_account_router(rs: RegStack) -> APIRouter:
|
|
|
124
127
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
125
128
|
detail="Current password is incorrect.",
|
|
126
129
|
)
|
|
130
|
+
|
|
131
|
+
# Anti-enumeration: an authenticated attacker could otherwise
|
|
132
|
+
# walk the registered-email namespace by alternating 409 vs 202
|
|
133
|
+
# responses on this endpoint. Always return 202; if the address
|
|
134
|
+
# is already taken, log and skip the email send. The legitimate
|
|
135
|
+
# user finds out by not receiving the confirmation mail; the
|
|
136
|
+
# final uniqueness check still happens at /confirm-email-change
|
|
137
|
+
# via the users.update_email unique constraint.
|
|
127
138
|
clash = await rs.users.get_by_email(payload.new_email)
|
|
139
|
+
accepted = MessageResponse(
|
|
140
|
+
message="If that email address is not already in use, a confirmation link has been sent.",
|
|
141
|
+
)
|
|
128
142
|
if clash is not None:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
log.info(
|
|
144
|
+
"change-email blocked by clash (anti-enumeration): user_id=%s",
|
|
145
|
+
user.id,
|
|
132
146
|
)
|
|
147
|
+
return accepted
|
|
133
148
|
|
|
134
149
|
assert user.id is not None
|
|
135
150
|
ttl = rs.config.email_change_token_ttl_seconds
|
|
@@ -148,9 +163,7 @@ def build_account_router(rs: RegStack) -> APIRouter:
|
|
|
148
163
|
new_email=payload.new_email,
|
|
149
164
|
url=url,
|
|
150
165
|
)
|
|
151
|
-
return
|
|
152
|
-
message="A confirmation link has been sent to the new email address."
|
|
153
|
-
)
|
|
166
|
+
return accepted
|
|
154
167
|
|
|
155
168
|
@router.post(
|
|
156
169
|
"/confirm-email-change",
|
|
@@ -154,6 +154,21 @@ def build_admin_router(rs: RegStack) -> APIRouter:
|
|
|
154
154
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
155
155
|
detail="User is already verified.",
|
|
156
156
|
)
|
|
157
|
+
# OAuth-only users (no password) shouldn't be unverified — their
|
|
158
|
+
# OAuth identity was the verification. If somehow one ended up
|
|
159
|
+
# unverified, the password-bearing verification flow can't help:
|
|
160
|
+
# PendingRegistration requires a `hashed_password: str` and
|
|
161
|
+
# would store the literal string "None" (or fail validation,
|
|
162
|
+
# depending on the pydantic config). Surface this clearly
|
|
163
|
+
# instead of silently corrupting the pending-registrations row.
|
|
164
|
+
if user.hashed_password is None:
|
|
165
|
+
raise HTTPException(
|
|
166
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
167
|
+
detail=(
|
|
168
|
+
"User has no password (OAuth-only). Verify them directly "
|
|
169
|
+
"via the user record instead of resending verification email."
|
|
170
|
+
),
|
|
171
|
+
)
|
|
157
172
|
|
|
158
173
|
# Move the user to a pending registration row so the standard verify
|
|
159
174
|
# endpoint completes the flow. Less special-case code, one path.
|
|
@@ -70,11 +70,11 @@ def build_login_router(rs: RegStack) -> APIRouter:
|
|
|
70
70
|
if user is None or user.id is None:
|
|
71
71
|
await rs.lockout.record_failure(payload.email)
|
|
72
72
|
raise _INVALID
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
# Password verification runs first so the is_active and
|
|
74
|
+
# is_verified branches below are only reachable by an attacker
|
|
75
|
+
# who already knows the password. Without this ordering, an
|
|
76
|
+
# unauthenticated probe could distinguish disabled / unverified
|
|
77
|
+
# / non-existent accounts by HTTP code alone.
|
|
78
78
|
# An OAuth-only user (hashed_password=None) returns the same
|
|
79
79
|
# generic 401 as a wrong-password attempt — never reveal that
|
|
80
80
|
# the account exists but has no password set, so an attacker
|
|
@@ -84,7 +84,19 @@ def build_login_router(rs: RegStack) -> APIRouter:
|
|
|
84
84
|
):
|
|
85
85
|
await rs.lockout.record_failure(payload.email)
|
|
86
86
|
raise _INVALID
|
|
87
|
+
# Even with the right password, disabled / unverified accounts
|
|
88
|
+
# must still increment the lockout counter — otherwise a
|
|
89
|
+
# password-stuffing attacker who happens to be holding the
|
|
90
|
+
# correct credentials for a disabled account gets unbounded
|
|
91
|
+
# probing.
|
|
92
|
+
if not user.is_active:
|
|
93
|
+
await rs.lockout.record_failure(payload.email)
|
|
94
|
+
raise HTTPException(
|
|
95
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
96
|
+
detail="Account is disabled.",
|
|
97
|
+
)
|
|
87
98
|
if rs.config.require_verification and not user.is_verified:
|
|
99
|
+
await rs.lockout.record_failure(payload.email)
|
|
88
100
|
raise HTTPException(
|
|
89
101
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
90
102
|
detail="Email address has not been verified.",
|