regstack 0.4.0__tar.gz → 0.5.6__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.4.0 → regstack-0.5.6}/.github/workflows/publish.yml +6 -1
- {regstack-0.4.0 → regstack-0.5.6}/CHANGELOG.md +81 -0
- {regstack-0.4.0 → regstack-0.5.6}/CLAUDE.md +14 -0
- {regstack-0.4.0 → regstack-0.5.6}/PKG-INFO +14 -9
- {regstack-0.4.0 → regstack-0.5.6}/README.md +4 -4
- {regstack-0.4.0 → regstack-0.5.6}/docs/architecture.md +7 -3
- {regstack-0.4.0 → regstack-0.5.6}/docs/changelog.md +148 -0
- regstack-0.5.6/docs/cli.md +187 -0
- {regstack-0.4.0 → regstack-0.5.6}/docs/configuration.md +45 -8
- {regstack-0.4.0 → regstack-0.5.6}/docs/embedding.md +5 -5
- {regstack-0.4.0 → regstack-0.5.6}/docs/index.md +53 -2
- {regstack-0.4.0 → regstack-0.5.6}/docs/quickstart.md +9 -4
- {regstack-0.4.0 → regstack-0.5.6}/docs/security.md +50 -3
- {regstack-0.4.0 → regstack-0.5.6}/docs/theming.md +46 -1
- {regstack-0.4.0 → regstack-0.5.6}/pyproject.toml +19 -5
- regstack-0.5.6/scripts/ccr_coverage_setup.py +242 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/app.py +55 -2
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/dependencies.py +12 -4
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/password.py +2 -20
- regstack-0.5.6/src/regstack/auth/rate_limit.py +141 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/backend.py +5 -6
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/client.py +9 -3
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/indexes.py +2 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/blacklist_repo.py +14 -2
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +3 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +2 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +3 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +3 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/pending_repo.py +3 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/user_repo.py +3 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/__init__.py +3 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/env.py +5 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -2
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +4 -6
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/oauth_state_repo.py +3 -3
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/pending_repo.py +4 -6
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/user_repo.py +4 -4
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/types.py +4 -2
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/__main__.py +15 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/doctor.py +9 -5
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/config/schema.py +18 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/composer.py +9 -2
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/ses.py +1 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/hooks/events.py +7 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/_objectid.py +1 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/user.py +1 -1
- regstack-0.5.6/src/regstack/routers/_helpers.py +28 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/account.py +5 -23
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/admin.py +4 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/login.py +12 -5
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/logout.py +3 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/oauth.py +6 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/phone.py +12 -3
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/sns.py +1 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/twilio.py +1 -1
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/pages.py +9 -2
- regstack-0.5.6/src/regstack/version.py +1 -0
- regstack-0.5.6/src/regstack/wizard/theme_designer/__init__.py +8 -0
- regstack-0.5.6/src/regstack/wizard/theme_designer/cli.py +181 -0
- regstack-0.5.6/src/regstack/wizard/theme_designer/routes.py +278 -0
- regstack-0.5.6/src/regstack/wizard/theme_designer/server.py +87 -0
- regstack-0.5.6/src/regstack/wizard/theme_designer/static/designer.css +348 -0
- regstack-0.5.6/src/regstack/wizard/theme_designer/static/designer.js +272 -0
- regstack-0.5.6/src/regstack/wizard/theme_designer/templates/designer.html +89 -0
- regstack-0.5.6/src/regstack/wizard/theme_designer/validators.py +145 -0
- regstack-0.5.6/src/regstack/wizard/theme_designer/window.py +63 -0
- regstack-0.5.6/src/regstack/wizard/theme_designer/writer.py +194 -0
- {regstack-0.4.0 → regstack-0.5.6}/tasks.py +16 -1
- regstack-0.5.6/tests/e2e/conftest.py +121 -0
- regstack-0.5.6/tests/e2e/test_theme_designer.py +87 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_admin_router.py +39 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_happy_path.py +40 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_mfa.py +110 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_oauth_google_router.py +33 -0
- regstack-0.5.6/tests/integration/test_rate_limits.py +215 -0
- regstack-0.5.6/tests/unit/test_cli_migrate.py +151 -0
- regstack-0.5.6/tests/unit/test_theme_designer_cli.py +190 -0
- regstack-0.5.6/tests/unit/test_theme_designer_routes.py +179 -0
- regstack-0.5.6/tests/unit/test_theme_designer_validators.py +154 -0
- regstack-0.5.6/tests/unit/test_theme_designer_writer.py +156 -0
- regstack-0.5.6/tests/unit/test_wizard_oauth_cli.py +230 -0
- {regstack-0.4.0 → regstack-0.5.6}/uv.lock +54 -6
- regstack-0.4.0/docs/cli.md +0 -87
- regstack-0.4.0/src/regstack/version.py +0 -1
- regstack-0.4.0/tests/e2e/conftest.py +0 -82
- regstack-0.4.0/tests/unit/test_wizard_oauth_cli.py +0 -93
- {regstack-0.4.0 → regstack-0.5.6}/.github/workflows/test.yml +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/.gitignore +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/.python-version +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/.readthedocs.yaml +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/LICENSE +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/NOTICE +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/SECURITY.md +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/docs/_static/.gitkeep +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/docs/_templates/.gitkeep +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/docs/api.md +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/docs/conf.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/docs/oauth.md +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/docs/security-reports/README.md +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/_common/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/_common/app.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/mongo/README.md +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/mongo/branding/theme.css +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/mongo/main.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/mongo/regstack.toml +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/postgres/README.md +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/postgres/main.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/postgres/regstack.toml +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/sqlite/README.md +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/sqlite/main.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/examples/sqlite/regstack.toml +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/regstack.toml.example +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/scripts/security-review-prompt.md +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/clock.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/jwt.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/lockout.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/base.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/factory.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/protocols.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/backend.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/backends/sql/schema.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/admin.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/init.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/config/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/config/loader.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/config/secrets.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/base.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/console.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/factory.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/smtp.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/oauth_identity.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/oauth_state.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/base.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/errors.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/providers/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/providers/google.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/oauth/registry.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/password.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/register.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/routers/verify.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/base.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/factory.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/sms/null.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/static/js/regstack.js +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/login.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/me.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/cli.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/routes.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/server.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/validators.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/window.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/writer.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tasks/oauth-design.md +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/_fake_google/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/_fake_google/provider.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/conftest.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/e2e/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/e2e/test_wizard_oauth_flow.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_account_management.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_indexes.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_login_lockout.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_oauth_repos.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_oauth_ui.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_password_reset.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_sql_migrations.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_ui_router.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/integration/test_verification.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/__init__.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_base_install_imports.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_cli.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_cli_doctor.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_cli_init.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_config_loader.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_jwt.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_lockout.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_mail_composer.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_mfa_code_repo.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_oauth_google.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_password.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_ses_backend.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_sms.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_smtp_backend.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_ui_env.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_routes.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_validators.py +0 -0
- {regstack-0.4.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_writer.py +0 -0
|
@@ -61,4 +61,9 @@ jobs:
|
|
|
61
61
|
with:
|
|
62
62
|
name: dist
|
|
63
63
|
path: dist/
|
|
64
|
-
|
|
64
|
+
# Pinned to a commit SHA, not the mutable `release/v1` branch — the
|
|
65
|
+
# publish job has `id-token: write`, so a tag/branch swap in the
|
|
66
|
+
# action repo would let an attacker push a malicious wheel under our
|
|
67
|
+
# OIDC identity. Update by resolving the latest SHA on `release/v1`
|
|
68
|
+
# and bumping the trailing comment to match.
|
|
69
|
+
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
|
|
@@ -5,6 +5,87 @@ 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.6 — 2026-05-13
|
|
9
|
+
|
|
10
|
+
Eleven days of security-review remediation, supply-chain hardening,
|
|
11
|
+
a full `mypy --strict` cleanup pass, and the per-route rate-limits
|
|
12
|
+
feature rolled up into a single release.
|
|
13
|
+
|
|
14
|
+
**Per-route IP rate limits.** Opt-in via the new `rate_limit` extra
|
|
15
|
+
(or a host-supplied `slowapi.Limiter`) plus any of the new
|
|
16
|
+
`RegStackConfig.*_rate_limit` fields (`login_rate_limit`,
|
|
17
|
+
`register_rate_limit`, `forgot_password_rate_limit`,
|
|
18
|
+
`reset_password_rate_limit`, `verify_rate_limit`,
|
|
19
|
+
`resend_verification_rate_limit`, `change_password_rate_limit`,
|
|
20
|
+
`change_email_rate_limit`, `confirm_email_change_rate_limit`,
|
|
21
|
+
`delete_account_rate_limit`). Each accepts a slowapi-syntax string
|
|
22
|
+
(`"5/minute"`, `"5/minute;20/hour"`). Empty / unset means no limit
|
|
23
|
+
on that route — `LockoutService` still defends `/login` against
|
|
24
|
+
credential stuffing per-account. When `*_rate_limit` strings are
|
|
25
|
+
configured but neither a `rate_limiter=` argument is passed nor
|
|
26
|
+
the `rate_limit` extra is installed, `RegStack.router` raises
|
|
27
|
+
`RuntimeError` on first access — failing closed beats silently
|
|
28
|
+
disabling the protection. Hosts remain responsible for
|
|
29
|
+
`app.state.limiter` and `app.add_exception_handler(RateLimitExceeded, ...)`;
|
|
30
|
+
slowapi owns the 429 response shape. The previously-reserved
|
|
31
|
+
`login_max_per_minute` / `login_max_per_hour` fields are kept for
|
|
32
|
+
back-compat but unwired.
|
|
33
|
+
|
|
34
|
+
**Security fixes.**
|
|
35
|
+
|
|
36
|
+
- JWT 401 detail now returns a static `"Invalid or expired token."`;
|
|
37
|
+
no longer leaks the pyjwt failure reason (signature mismatch /
|
|
38
|
+
expired / malformed / audience mismatch).
|
|
39
|
+
- OAuth sign-in now honours `allow_registration=False`. Previously,
|
|
40
|
+
`/register` respected the flag but the OAuth `_resolve_user`
|
|
41
|
+
"brand-new account" branch did not, creating accounts even when
|
|
42
|
+
self-service signup was disabled.
|
|
43
|
+
- Admin `DELETE /admin/users/{id}` now cascades `oauth_identities`,
|
|
44
|
+
matching the user-initiated `DELETE /account` path. Previously
|
|
45
|
+
left orphan rows that blocked re-registration of the same Google
|
|
46
|
+
subject.
|
|
47
|
+
- `POST /phone/start` and `DELETE /phone` now return 400 (not crash
|
|
48
|
+
with HTTP 500) for OAuth-only users who have no `hashed_password`.
|
|
49
|
+
|
|
50
|
+
**Breaking change — hook contracts.** `mfa_login_started` and
|
|
51
|
+
`phone_setup_started` no longer include the raw OTP code in their
|
|
52
|
+
kwargs. Hooks are best-effort observability and are the documented
|
|
53
|
+
integration surface for analytics / logging / Slack notifications,
|
|
54
|
+
so a plaintext OTP in `**kwargs` is a leak waiting to happen.
|
|
55
|
+
Hosts that subscribed to either event to take over SMS delivery
|
|
56
|
+
should migrate to a custom `SmsService` subclass — the supported
|
|
57
|
+
delivery override.
|
|
58
|
+
|
|
59
|
+
**Dependency floors raised for CVEs.**
|
|
60
|
+
|
|
61
|
+
- `pyjwt>=2.12.1` for CVE-2026-32597 (`crit` header bypass, CVSS 7.5).
|
|
62
|
+
- `cryptography>=46.0.7` added explicitly to the `oauth` extra for
|
|
63
|
+
CVE-2026-26007 (ECC subgroup attack on the JWKS code path, CVSS
|
|
64
|
+
8.2) plus CVE-2026-34073 and CVE-2026-39892.
|
|
65
|
+
- `python-multipart>=0.0.26` for CVE-2026-40347 (DoS via oversized
|
|
66
|
+
multipart preamble).
|
|
67
|
+
|
|
68
|
+
**Supply chain.** `pypa/gh-action-pypi-publish` in `publish.yml`
|
|
69
|
+
pinned to a commit SHA instead of the mutable `release/v1` branch.
|
|
70
|
+
The publish job holds `id-token: write`, so a tag/branch swap
|
|
71
|
+
upstream would let an attacker push a malicious wheel under our
|
|
72
|
+
OIDC identity.
|
|
73
|
+
|
|
74
|
+
**Removed.** `PasswordHasher.needs_rehash` — called pwdlib's
|
|
75
|
+
non-existent `check_needs_rehash` and would `AttributeError` if
|
|
76
|
+
anyone invoked it. No callers in src or tests. If you were planning
|
|
77
|
+
to use it, call `pwdlib.PasswordHash.verify_and_update` directly.
|
|
78
|
+
|
|
79
|
+
**Internal.** 72 `mypy --strict` errors cleared across 35 files;
|
|
80
|
+
`inv lint` is now green end-to-end. Mongo
|
|
81
|
+
`BlacklistRepo.purge_expired` added (protocol parity with SQL).
|
|
82
|
+
`KNOWN_EVENTS` reconciled — 7 previously-undeclared events added
|
|
83
|
+
(`verification_requested`, `email_change_requested`, `email_changed`,
|
|
84
|
+
`phone_setup_started`, `mfa_login_started`, `mfa_enabled`,
|
|
85
|
+
`mfa_disabled`). `user_logged_out` now actually fires from
|
|
86
|
+
`routers/logout.py` (was listed in `KNOWN_EVENTS` but no router
|
|
87
|
+
emitted it).
|
|
88
|
+
|
|
8
89
|
## 0.3.0 — 2026-04-30
|
|
9
90
|
|
|
10
91
|
**OAuth — Sign in with Google.** Opt-in via the new `oauth` extra
|
|
@@ -77,6 +77,20 @@ The full plan, including milestone scope and deferred items, lives at
|
|
|
77
77
|
four layers: validators (unit), writer (golden-file), routes
|
|
78
78
|
(TestClient), full SPA flow (Playwright e2e). Run e2e with
|
|
79
79
|
`inv test-e2e`; `inv test-all` chains it after the backend matrix.
|
|
80
|
+
- **Theme designer — done.** `regstack theme design` opens a sibling
|
|
81
|
+
pywebview window with controls for every `--rs-*` variable and a
|
|
82
|
+
real-time preview of the bundled SSR widgets (renders the same
|
|
83
|
+
`.rs-*` classes the SSR pages render, so what you see is what
|
|
84
|
+
you'll ship). Save writes `regstack-theme.css`; the designer
|
|
85
|
+
round-trips values back into the form on next launch so iteration
|
|
86
|
+
is non-destructive. Lives in `src/regstack/wizard/theme_designer/`,
|
|
87
|
+
registered via a `_LazyThemeGroup` mirroring the OAuth pattern.
|
|
88
|
+
Same four-layer test stack (validators / writer / routes /
|
|
89
|
+
Playwright e2e). The shared scaffold (FastAPI app, uvicorn launcher,
|
|
90
|
+
pywebview shim, e2e fixture pattern) is duplicated rather than
|
|
91
|
+
abstracted into a base class — the two tools have meaningfully
|
|
92
|
+
different inputs and an early shared base would calcify the wrong
|
|
93
|
+
decisions. Refactor only when a third pywebview tool lands.
|
|
80
94
|
|
|
81
95
|
## Three kinds of single-use proof
|
|
82
96
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.6
|
|
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
|
|
@@ -27,8 +27,8 @@ Requires-Dist: jinja2>=3.1
|
|
|
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
|
-
Requires-Dist: pyjwt>=2.
|
|
31
|
-
Requires-Dist: python-multipart>=0.0.
|
|
30
|
+
Requires-Dist: pyjwt>=2.12.1
|
|
31
|
+
Requires-Dist: python-multipart>=0.0.26
|
|
32
32
|
Requires-Dist: pywebview>=5.0
|
|
33
33
|
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
34
34
|
Requires-Dist: tomlkit>=0.13
|
|
@@ -36,10 +36,11 @@ Requires-Dist: uvicorn[standard]>=0.29
|
|
|
36
36
|
Provides-Extra: dev
|
|
37
37
|
Requires-Dist: anyio>=4.3; extra == 'dev'
|
|
38
38
|
Requires-Dist: asyncpg>=0.29; extra == 'dev'
|
|
39
|
+
Requires-Dist: cryptography>=46.0.7; extra == 'dev'
|
|
39
40
|
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
40
41
|
Requires-Dist: invoke>=2.2; extra == 'dev'
|
|
41
42
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
42
|
-
Requires-Dist: pyjwt[crypto]>=2.
|
|
43
|
+
Requires-Dist: pyjwt[crypto]>=2.12.1; extra == 'dev'
|
|
43
44
|
Requires-Dist: pymongo>=4.9; extra == 'dev'
|
|
44
45
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
45
46
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
@@ -47,6 +48,7 @@ Requires-Dist: pytest-playwright>=0.5; extra == 'dev'
|
|
|
47
48
|
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
|
|
48
49
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
49
50
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
51
|
+
Requires-Dist: slowapi>=0.1.9; extra == 'dev'
|
|
50
52
|
Requires-Dist: uvicorn[standard]>=0.29; extra == 'dev'
|
|
51
53
|
Provides-Extra: docs
|
|
52
54
|
Requires-Dist: furo>=2024.1; extra == 'docs'
|
|
@@ -58,9 +60,12 @@ Requires-Dist: sphinx>=7.3; extra == 'docs'
|
|
|
58
60
|
Provides-Extra: mongo
|
|
59
61
|
Requires-Dist: pymongo>=4.9; extra == 'mongo'
|
|
60
62
|
Provides-Extra: oauth
|
|
61
|
-
Requires-Dist:
|
|
63
|
+
Requires-Dist: cryptography>=46.0.7; extra == 'oauth'
|
|
64
|
+
Requires-Dist: pyjwt[crypto]>=2.12.1; extra == 'oauth'
|
|
62
65
|
Provides-Extra: postgres
|
|
63
66
|
Requires-Dist: asyncpg>=0.29; extra == 'postgres'
|
|
67
|
+
Provides-Extra: rate-limit
|
|
68
|
+
Requires-Dist: slowapi>=0.1.9; extra == 'rate-limit'
|
|
64
69
|
Provides-Extra: ses
|
|
65
70
|
Requires-Dist: aioboto3>=12.3; extra == 'ses'
|
|
66
71
|
Provides-Extra: sns
|
|
@@ -143,7 +148,7 @@ result everywhere is what regstack is for.
|
|
|
143
148
|
✔ Server-rendered HTML pages, theme with one CSS file
|
|
144
149
|
✔ Pluggable email (console / SMTP / Amazon SES) and SMS (Amazon SNS / Twilio)
|
|
145
150
|
✔ Argon2 password hashing, CSP-friendly templates
|
|
146
|
-
✔ Setup
|
|
151
|
+
✔ Setup wizards (live in their own pywebview windows): `regstack init` (project bootstrap), `regstack oauth setup` (guided Google OAuth client config), `regstack theme design` (live theme designer with preview), and `regstack doctor` (config validator)
|
|
147
152
|
✔ Three storage backends: SQLite, PostgreSQL, MongoDB — chosen by URL
|
|
148
153
|
```
|
|
149
154
|
|
|
@@ -186,7 +191,7 @@ register from the command line:
|
|
|
186
191
|
```bash
|
|
187
192
|
curl -X POST http://localhost:8000/api/auth/register \
|
|
188
193
|
-H 'content-type: application/json' \
|
|
189
|
-
-d '{"email":"alice@example.com","password":"
|
|
194
|
+
-d '{"email":"alice@app.example.com","password":"<password>","full_name":"Alice"}'
|
|
190
195
|
```
|
|
191
196
|
|
|
192
197
|
The bundled example serves themed SSR pages at `/account/*`, prints
|
|
@@ -251,8 +256,8 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
|
|
|
251
256
|
Alpha. Single-file SQLite is the default and runs with no infrastructure;
|
|
252
257
|
PostgreSQL and MongoDB backends pass the same parametrized integration
|
|
253
258
|
suite. OAuth (Google) shipped in `v0.3.0`; the `regstack oauth setup`
|
|
254
|
-
guided wizard
|
|
255
|
-
See the
|
|
259
|
+
guided wizard in `v0.4.0`; the live `regstack theme design` tool in
|
|
260
|
+
`v0.5.0`. Latest tagged release: `v0.5.0`. See the
|
|
256
261
|
[changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
|
|
257
262
|
for the per-release breakdown.
|
|
258
263
|
|
|
@@ -72,7 +72,7 @@ result everywhere is what regstack is for.
|
|
|
72
72
|
✔ Server-rendered HTML pages, theme with one CSS file
|
|
73
73
|
✔ Pluggable email (console / SMTP / Amazon SES) and SMS (Amazon SNS / Twilio)
|
|
74
74
|
✔ Argon2 password hashing, CSP-friendly templates
|
|
75
|
-
✔ Setup
|
|
75
|
+
✔ Setup wizards (live in their own pywebview windows): `regstack init` (project bootstrap), `regstack oauth setup` (guided Google OAuth client config), `regstack theme design` (live theme designer with preview), and `regstack doctor` (config validator)
|
|
76
76
|
✔ Three storage backends: SQLite, PostgreSQL, MongoDB — chosen by URL
|
|
77
77
|
```
|
|
78
78
|
|
|
@@ -115,7 +115,7 @@ register from the command line:
|
|
|
115
115
|
```bash
|
|
116
116
|
curl -X POST http://localhost:8000/api/auth/register \
|
|
117
117
|
-H 'content-type: application/json' \
|
|
118
|
-
-d '{"email":"alice@example.com","password":"
|
|
118
|
+
-d '{"email":"alice@app.example.com","password":"<password>","full_name":"Alice"}'
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
The bundled example serves themed SSR pages at `/account/*`, prints
|
|
@@ -180,8 +180,8 @@ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdr
|
|
|
180
180
|
Alpha. Single-file SQLite is the default and runs with no infrastructure;
|
|
181
181
|
PostgreSQL and MongoDB backends pass the same parametrized integration
|
|
182
182
|
suite. OAuth (Google) shipped in `v0.3.0`; the `regstack oauth setup`
|
|
183
|
-
guided wizard
|
|
184
|
-
See the
|
|
183
|
+
guided wizard in `v0.4.0`; the live `regstack theme design` tool in
|
|
184
|
+
`v0.5.0`. Latest tagged release: `v0.5.0`. See the
|
|
185
185
|
[changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
|
|
186
186
|
for the per-release breakdown.
|
|
187
187
|
|
|
@@ -54,9 +54,13 @@ multi-tenant deployments where a single FastAPI app serves multiple
|
|
|
54
54
|
The backend is auto-built from `config.database_url` if not supplied
|
|
55
55
|
explicitly. URL scheme decides:
|
|
56
56
|
|
|
57
|
-
- `sqlite+aiosqlite
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
- `sqlite+aiosqlite:///PATH` → SQLAlchemy backend in SQLite mode.
|
|
58
|
+
`PATH` is `./dbname.db` for a relative file, `/var/lib/app/dbname.db`
|
|
59
|
+
for an absolute file, or `:memory:` for an ephemeral in-process DB.
|
|
60
|
+
- `postgresql+asyncpg://<username>:<password>@dbhost.example.com:5432/dbname`
|
|
61
|
+
→ SQLAlchemy backend in Postgres mode.
|
|
62
|
+
- `mongodb://<username>:<password>@dbhost.example.com:27017/dbname`
|
|
63
|
+
(or `mongodb+srv://…`) → Mongo backend.
|
|
60
64
|
|
|
61
65
|
The façade exposes:
|
|
62
66
|
|
|
@@ -3,6 +3,154 @@
|
|
|
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.6 — 2026-05-13
|
|
7
|
+
|
|
8
|
+
A rollup release that consolidates 11 days of security-review
|
|
9
|
+
remediation, supply-chain hardening, a full `mypy --strict` pass,
|
|
10
|
+
and the per-route rate-limits feature.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Per-route IP rate limits.** Opt-in via the new `rate_limit` extra
|
|
15
|
+
(or a host-supplied `slowapi.Limiter`) plus any of the new
|
|
16
|
+
`RegStackConfig.*_rate_limit` fields (`login_rate_limit`,
|
|
17
|
+
`register_rate_limit`, `forgot_password_rate_limit`,
|
|
18
|
+
`reset_password_rate_limit`, `verify_rate_limit`,
|
|
19
|
+
`resend_verification_rate_limit`, `change_password_rate_limit`,
|
|
20
|
+
`change_email_rate_limit`, `confirm_email_change_rate_limit`,
|
|
21
|
+
`delete_account_rate_limit`). Each accepts a slowapi-syntax string
|
|
22
|
+
(`"5/minute"`, `"5/minute;20/hour"`).
|
|
23
|
+
- New constructor argument `RegStack(rate_limiter=...)`. When at
|
|
24
|
+
least one `*_rate_limit` field is set, regstack expects either
|
|
25
|
+
this argument or the `rate_limit` extra; failure to provide one
|
|
26
|
+
raises `RuntimeError` on first access to `regstack.router` —
|
|
27
|
+
failing closed beats silently disabling the protection. Hosts
|
|
28
|
+
remain responsible for `app.state.limiter` and the
|
|
29
|
+
`RateLimitExceeded` exception handler; slowapi owns the 429
|
|
30
|
+
response shape.
|
|
31
|
+
- **`user_logged_out` hook now fires.** The event was listed in
|
|
32
|
+
`KNOWN_EVENTS` since M1 but no router ever emitted it.
|
|
33
|
+
`routers/logout.py` now fires `user_logged_out` (with a `user=`
|
|
34
|
+
kwarg) immediately after the bearer token is revoked.
|
|
35
|
+
|
|
36
|
+
### Changed (security)
|
|
37
|
+
|
|
38
|
+
- **JWT 401 responses no longer leak the pyjwt error reason.**
|
|
39
|
+
Replaced `f"Invalid token: {exc}"` with the static `"Invalid or
|
|
40
|
+
expired token."`. The pyjwt error text disclosed *why* a token was
|
|
41
|
+
rejected (signature mismatch, expired, malformed, audience
|
|
42
|
+
mismatch) — useful signal for an attacker probing the auth
|
|
43
|
+
surface.
|
|
44
|
+
- **OAuth sign-in honours `allow_registration=False`.** `/register`
|
|
45
|
+
already did; the OAuth `_resolve_user` "brand-new account" branch
|
|
46
|
+
did not, so an operator who disabled self-service signup still got
|
|
47
|
+
accounts created via "Sign in with Google". The OAuth callback now
|
|
48
|
+
redirects with `?error=registration_disabled` if no existing
|
|
49
|
+
account matches and registration is disabled.
|
|
50
|
+
- **Admin `DELETE /admin/users/{id}` now cascades
|
|
51
|
+
`oauth_identities`.** Matches the user-initiated `DELETE
|
|
52
|
+
/account` flow; previously left orphan rows that blocked the
|
|
53
|
+
Google subject from re-registering.
|
|
54
|
+
- **`POST /phone/start` and `DELETE /phone` guard against OAuth-only
|
|
55
|
+
users.** Both endpoints previously crashed with HTTP 500 for
|
|
56
|
+
users with `hashed_password=None`. Both now return 400 with a
|
|
57
|
+
message pointing to forgot-password (which doubles as a "set
|
|
58
|
+
initial password" path).
|
|
59
|
+
|
|
60
|
+
### Changed (BREAKING — hook contracts)
|
|
61
|
+
|
|
62
|
+
- **`mfa_login_started` and `phone_setup_started` no longer include
|
|
63
|
+
the raw OTP code in their kwargs.** Hooks are best-effort
|
|
64
|
+
observability and are the documented integration surface for
|
|
65
|
+
analytics / logging / Slack notifications, so a plaintext OTP in
|
|
66
|
+
`**kwargs` is a leak waiting to happen — a host adding
|
|
67
|
+
`logger.info(kw)` to a hook handler is enough to put OTPs in a
|
|
68
|
+
log stream. Hosts that subscribed to either event to take over
|
|
69
|
+
SMS delivery should migrate to a custom `SmsService` subclass
|
|
70
|
+
(the supported delivery override). The other kwargs (`user`,
|
|
71
|
+
`phone`) remain.
|
|
72
|
+
|
|
73
|
+
### Changed (deps)
|
|
74
|
+
|
|
75
|
+
- `pyjwt>=2.12.1` (was `>=2.8`). Picks up CVE-2026-32597 (`crit`
|
|
76
|
+
header bypass, CVSS 7.5).
|
|
77
|
+
- `cryptography>=46.0.7` added explicitly to the `oauth` extra
|
|
78
|
+
(was pulled transitively, unbounded). Picks up CVE-2026-26007
|
|
79
|
+
(ECC subgroup attack on the JWKS code path, CVSS 8.2) plus
|
|
80
|
+
CVE-2026-34073 and CVE-2026-39892.
|
|
81
|
+
- `python-multipart>=0.0.26` (was `>=0.0.9`). Picks up
|
|
82
|
+
CVE-2026-40347 (DoS via oversized multipart preamble).
|
|
83
|
+
- `pypa/gh-action-pypi-publish` in `publish.yml` pinned to a commit
|
|
84
|
+
SHA instead of the mutable `release/v1` branch. The publish job
|
|
85
|
+
holds `id-token: write`, so a tag/branch swap upstream would let
|
|
86
|
+
an attacker push a malicious wheel under our OIDC identity.
|
|
87
|
+
|
|
88
|
+
### Removed
|
|
89
|
+
|
|
90
|
+
- `PasswordHasher.needs_rehash` — called pwdlib's non-existent
|
|
91
|
+
`check_needs_rehash` and would `AttributeError` if anyone invoked
|
|
92
|
+
it. No callers in src or tests. If you were planning to use it,
|
|
93
|
+
call `pwdlib.PasswordHash.verify_and_update` directly.
|
|
94
|
+
|
|
95
|
+
### Internal
|
|
96
|
+
|
|
97
|
+
- 72 `mypy --strict` errors cleared across 35 files. `inv lint` is
|
|
98
|
+
green end-to-end (ruff + mypy). Still local-only — not yet a CI
|
|
99
|
+
gate.
|
|
100
|
+
- Mongo `BlacklistRepo.purge_expired` added (was missing from the
|
|
101
|
+
Mongo impl; SQL impl already had it). Mongo's TTL index still
|
|
102
|
+
reaps automatically; the explicit `delete_many` is for protocol
|
|
103
|
+
parity and for tests that can't wait for the 60-second TTL
|
|
104
|
+
monitor.
|
|
105
|
+
- `KNOWN_EVENTS` reconciled with reality: 7 previously-undeclared
|
|
106
|
+
events added (`verification_requested`, `email_change_requested`,
|
|
107
|
+
`email_changed`, `phone_setup_started`, `mfa_login_started`,
|
|
108
|
+
`mfa_enabled`, `mfa_disabled`).
|
|
109
|
+
- `routers/_helpers.require_password_set` factored out of
|
|
110
|
+
`routers/account.py` and reused in `routers/phone.py`.
|
|
111
|
+
- `AsyncDatabase[MongoDoc]` / `AsyncMongoClient[MongoDoc]`
|
|
112
|
+
parameterized across the Mongo backend so pymongo's typed stubs
|
|
113
|
+
are satisfied.
|
|
114
|
+
|
|
115
|
+
### Notes
|
|
116
|
+
|
|
117
|
+
- `LockoutService` (per-account, sliding-window failure counter) is
|
|
118
|
+
unchanged and continues to defend `/login` against
|
|
119
|
+
credential-stuffing against a single account. Per-route IP limits
|
|
120
|
+
are orthogonal: they defend each endpoint against a single source
|
|
121
|
+
IP spamming requests across many accounts.
|
|
122
|
+
- The previously-reserved `login_max_per_minute` /
|
|
123
|
+
`login_max_per_hour` config fields are kept for back-compat but
|
|
124
|
+
no longer have any effect. Switch to the per-route fields when
|
|
125
|
+
you next touch your config.
|
|
126
|
+
|
|
127
|
+
## 0.5.0 — 2026-05-02
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
|
|
131
|
+
- **Theme designer.** `regstack theme design` opens a native pywebview
|
|
132
|
+
window with controls for every `--rs-*` CSS custom property and a
|
|
133
|
+
real-time preview of the bundled SSR widgets (sign-in form, success
|
|
134
|
+
/ error banners, danger-zone button). Saving writes
|
|
135
|
+
`regstack-theme.css`; the designer round-trips values back into the
|
|
136
|
+
form on next launch so iteration is non-destructive. `--print-only`
|
|
137
|
+
mode takes repeatable `--var NAME=VALUE` pairs (with a `dark:`
|
|
138
|
+
prefix for dark-scheme overrides) and writes the file headlessly.
|
|
139
|
+
Lives in `regstack.wizard.theme_designer`; registered as a lazy
|
|
140
|
+
Click subgroup so `regstack init` / `doctor` don't pay the
|
|
141
|
+
pywebview/uvicorn import cost.
|
|
142
|
+
- "Why use regstack" pitch in `docs/index.md` updated to surface the
|
|
143
|
+
two pywebview tools (`oauth setup` + `theme design`) as a
|
|
144
|
+
distinguishing feature vs. fastapi-users / Auth0 / Keycloak.
|
|
145
|
+
|
|
146
|
+
### Docs
|
|
147
|
+
|
|
148
|
+
- New "About the examples" convention block at the top of
|
|
149
|
+
`docs/index.md`. Every URL, email, smtp host, and admin command
|
|
150
|
+
across the docs now extrapolates from the same fictional app at
|
|
151
|
+
`app.example.com` with `<username>` / `<password>` placeholders —
|
|
152
|
+
no more `user:pw@host/dbname` / `db.internal/myapp` mishmash.
|
|
153
|
+
|
|
6
154
|
## 0.4.0 — 2026-05-02
|
|
7
155
|
|
|
8
156
|
### Added
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# CLI reference
|
|
2
|
+
|
|
3
|
+
`regstack` is the entry point installed by the package. All sub-commands
|
|
4
|
+
share a config-loading model: programmatic kwargs > env vars >
|
|
5
|
+
`regstack.secrets.env` > `regstack.toml` > defaults. The `--config <path>`
|
|
6
|
+
flag overrides where the TOML file is found.
|
|
7
|
+
|
|
8
|
+
## `regstack init`
|
|
9
|
+
|
|
10
|
+
Interactive wizard that writes `regstack.toml` and `regstack.secrets.env`
|
|
11
|
+
in the current directory. Asks which backend to use (SQLite default →
|
|
12
|
+
Postgres → MongoDB), generates a 64-byte JWT secret, runs DNS sanity
|
|
13
|
+
checks if asked, never provisions infrastructure.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv run regstack init
|
|
17
|
+
uv run regstack init --target /etc/app --force
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
|
|
22
|
+
- `--target DIR` — directory to write the config files (default cwd).
|
|
23
|
+
- `--force` — overwrite without confirming.
|
|
24
|
+
|
|
25
|
+
Re-running the wizard prompts before overwriting unless `--force` is
|
|
26
|
+
passed; pre-existing answers aren't kept (the wizard is intentionally
|
|
27
|
+
stateless).
|
|
28
|
+
|
|
29
|
+
## `regstack oauth setup`
|
|
30
|
+
|
|
31
|
+
Opens a guided 12-step wizard in a native webview window that walks you
|
|
32
|
+
through registering a Google OAuth 2.0 client (project selection,
|
|
33
|
+
consent screen, redirect URI, credentials) and merges the result into
|
|
34
|
+
your existing `regstack.toml` and `regstack.secrets.env`. The merge is
|
|
35
|
+
**non-clobbering** — comments, unrelated tables (`[email]`, `[sms]`,
|
|
36
|
+
…), and unrelated top-level keys are preserved. Re-run any time to
|
|
37
|
+
rotate credentials or change the linking policy.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv run regstack oauth setup
|
|
41
|
+
uv run regstack oauth setup --target /etc/app
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
|
|
46
|
+
- `--target DIR` — directory containing (or to receive) `regstack.toml`
|
|
47
|
+
(default cwd).
|
|
48
|
+
- `--api-prefix PREFIX` — router prefix the host mounts regstack under
|
|
49
|
+
(default `/api/auth`). Used to compute the suggested redirect URI.
|
|
50
|
+
- `--port N` — pin the wizard server's TCP port (default: random free
|
|
51
|
+
port on `127.0.0.1`).
|
|
52
|
+
- `--print-only` — skip the GUI; print the TOML + secrets diff that
|
|
53
|
+
*would* be written to stdout, then exit. Useful for headless hosts
|
|
54
|
+
(CI, servers without a webview backend) and dry-run smoke tests.
|
|
55
|
+
Pair with `--client-id`, `--client-secret`, `--base-url`,
|
|
56
|
+
`--auto-link/--no-auto-link`, `--mfa/--no-mfa`.
|
|
57
|
+
|
|
58
|
+
The interactive mode requires a desktop environment with a webview
|
|
59
|
+
backend (WebKit on macOS, GTK / QtWebEngine on Linux, Edge WebView2 on
|
|
60
|
+
Windows). On a headless host it exits with a clear error pointing at
|
|
61
|
+
`--print-only`.
|
|
62
|
+
|
|
63
|
+
The wizard binds to `127.0.0.1` only and authenticates every API call
|
|
64
|
+
with a per-launch random token, so a hostile process on the same host
|
|
65
|
+
can't drive the write endpoint.
|
|
66
|
+
|
|
67
|
+
## `regstack theme design`
|
|
68
|
+
|
|
69
|
+
Opens a live designer for `regstack-theme.css` in a native pywebview
|
|
70
|
+
window. The left pane has controls for every `--rs-*` CSS custom
|
|
71
|
+
property; the right pane renders the bundled SSR widgets (sign-in
|
|
72
|
+
form, success / error messages, danger-zone button) with your changes
|
|
73
|
+
applied in real time. Click **Save** to write the file; **Reset to
|
|
74
|
+
defaults** to start over; **Copy CSS** to put the generated
|
|
75
|
+
stylesheet on the clipboard without writing.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uv run regstack theme design
|
|
79
|
+
uv run regstack theme design --target /var/www/static
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Options:
|
|
83
|
+
|
|
84
|
+
- `--target DIR` — directory to write `regstack-theme.css` into
|
|
85
|
+
(default cwd).
|
|
86
|
+
- `--filename NAME` — output filename
|
|
87
|
+
(default `regstack-theme.css`).
|
|
88
|
+
- `--port N` — pin the designer's TCP port (default: random free
|
|
89
|
+
port on `127.0.0.1`).
|
|
90
|
+
- `--print-only` — skip the GUI; write the file from `--var` pairs
|
|
91
|
+
and emit a JSON summary. For headless / CI use.
|
|
92
|
+
- `--var NAME=VALUE` — repeatable. Used with `--print-only`. Prefix
|
|
93
|
+
with `dark:` to set the dark-scheme value, e.g.
|
|
94
|
+
`--var dark:--rs-accent=#2dd4bf`.
|
|
95
|
+
|
|
96
|
+
Re-running the designer reloads the previous values out of the file,
|
|
97
|
+
so iterating on a theme is non-destructive. Only the `:root` and
|
|
98
|
+
`@media (prefers-color-scheme: dark)` blocks are managed by the
|
|
99
|
+
designer — anything else in the file is left alone.
|
|
100
|
+
|
|
101
|
+
Same security shape as `regstack oauth setup`: binds `127.0.0.1`
|
|
102
|
+
only, every API call requires a per-launch random token.
|
|
103
|
+
|
|
104
|
+
## `regstack create-admin`
|
|
105
|
+
|
|
106
|
+
Create or promote a superuser. Idempotent.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv run regstack create-admin --email admin@app.example.com
|
|
110
|
+
uv run regstack create-admin --email admin@app.example.com --password 'long-strong-password'
|
|
111
|
+
uv run regstack create-admin --email admin@app.example.com --config /etc/app/regstack.toml
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
|
|
116
|
+
- `--email EMAIL` *(required)*.
|
|
117
|
+
- `--password PW` — if omitted, prompts (with confirmation).
|
|
118
|
+
- `--config PATH` — TOML file to load (default: env or cwd).
|
|
119
|
+
|
|
120
|
+
If the user already exists, the command sets `is_superuser=True` and
|
|
121
|
+
keeps the existing password. If they don't exist, it creates the user
|
|
122
|
+
with `is_active=True`, `is_verified=True`, `is_superuser=True` and the
|
|
123
|
+
provided password.
|
|
124
|
+
|
|
125
|
+
## `regstack migrate`
|
|
126
|
+
|
|
127
|
+
Runs the bundled Alembic migrations against the configured
|
|
128
|
+
`database_url`. Idempotent — re-running on a DB already at the target
|
|
129
|
+
revision is a no-op. Use this on SQL backends (SQLite / PostgreSQL)
|
|
130
|
+
to roll the schema forward to a new regstack release.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
uv run regstack migrate
|
|
134
|
+
uv run regstack migrate --target head
|
|
135
|
+
uv run regstack migrate --config /etc/app/regstack.toml --target 0001
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Options:
|
|
139
|
+
|
|
140
|
+
- `--config PATH` — TOML file to load (default: cwd / `$REGSTACK_CONFIG`).
|
|
141
|
+
- `--target REV` — revision to upgrade to (default `head`). Accepts
|
|
142
|
+
any Alembic revision spec: a revision id (`0001`), a relative step
|
|
143
|
+
(`+1`), or `head`.
|
|
144
|
+
|
|
145
|
+
Mongo backends are silently skipped: TTL indexes are installed by
|
|
146
|
+
`RegStack.install_schema()` on every app start, so there's no separate
|
|
147
|
+
migration story to run. Output prints the before / after revision and
|
|
148
|
+
exits non-zero if Alembic raises.
|
|
149
|
+
|
|
150
|
+
## `regstack doctor`
|
|
151
|
+
|
|
152
|
+
Runs read-only validation against the loaded config and reports each
|
|
153
|
+
check on a green/red line. Exit code is the number of failed checks —
|
|
154
|
+
suitable for use in container health checks.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
uv run regstack doctor
|
|
158
|
+
uv run regstack doctor --config /etc/app/regstack.toml
|
|
159
|
+
uv run regstack doctor --check-dns
|
|
160
|
+
uv run regstack doctor --send-test-email alice@app.example.com
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Options:
|
|
164
|
+
|
|
165
|
+
- `--config PATH` — TOML file to load.
|
|
166
|
+
- `--check-dns` — run SPF / DMARC / MX `dig`s on the sender domain.
|
|
167
|
+
Internet-dependent; off by default.
|
|
168
|
+
- `--send-test-email TO` — actually send a probe email through the
|
|
169
|
+
configured backend. Costs real money on SES; off by default.
|
|
170
|
+
|
|
171
|
+
Default checks:
|
|
172
|
+
|
|
173
|
+
| Check | Pass criterion |
|
|
174
|
+
|-------|----------------|
|
|
175
|
+
| `jwt secret` | Present, ≥ 32 chars |
|
|
176
|
+
| `backend` | `Backend.ping()` succeeds (works for any backend) |
|
|
177
|
+
| `schema` | Mongo: required indexes present. SQL: `users` table responds to `count(*)` |
|
|
178
|
+
| `email backend` | `build_email_service(config.email)` instantiates |
|
|
179
|
+
|
|
180
|
+
Optional checks:
|
|
181
|
+
|
|
182
|
+
| Check | Pass criterion |
|
|
183
|
+
|-------|----------------|
|
|
184
|
+
| `dns mx` | At least one MX record on the sender domain |
|
|
185
|
+
| `dns spf` | A TXT record containing `v=spf1` |
|
|
186
|
+
| `dns dmarc` | A TXT record at `_dmarc.<domain>` containing `v=DMARC1` |
|
|
187
|
+
| `email send` | The configured backend's `send()` returned without raising |
|
|
@@ -85,15 +85,17 @@ regstack picks a backend at construction time from the URL scheme of
|
|
|
85
85
|
- Notes
|
|
86
86
|
|
|
87
87
|
* - SQLite
|
|
88
|
-
- `sqlite+aiosqlite:///./
|
|
88
|
+
- Relative file: `sqlite+aiosqlite:///./dbname.db`
|
|
89
89
|
- Default. Bundled in the base install — no extras needed.
|
|
90
|
-
|
|
90
|
+
See [SQLite URL forms](#sqlite-url-forms) below for absolute-path
|
|
91
|
+
and in-memory variants.
|
|
91
92
|
* - Postgres
|
|
92
|
-
- `postgresql+asyncpg
|
|
93
|
+
- `postgresql+asyncpg://<username>:<password>@dbhost.example.com:5432/dbname`
|
|
93
94
|
- Requires the `postgres` extra (pulls in `asyncpg`). The driver is
|
|
94
95
|
pinned to `+asyncpg` — sync drivers won't work.
|
|
95
96
|
* - MongoDB
|
|
96
|
-
- `mongodb
|
|
97
|
+
- `mongodb://<username>:<password>@dbhost.example.com:27017/dbname`
|
|
98
|
+
(or `mongodb+srv://<username>:<password>@app.abc123.mongodb.net/dbname`)
|
|
97
99
|
- Requires the `mongo` extra (pulls in `pymongo`). Database is taken
|
|
98
100
|
from the URL path; falls back to ``mongodb_database`` if absent.
|
|
99
101
|
```
|
|
@@ -102,6 +104,41 @@ The active backend exposes the same five repository protocols on
|
|
|
102
104
|
``RegStack.users``, ``.pending``, ``.blacklist``, ``.attempts``,
|
|
103
105
|
``.mfa_codes``. Routers / hooks never branch on backend kind.
|
|
104
106
|
|
|
107
|
+
### SQLite URL forms
|
|
108
|
+
|
|
109
|
+
SQLite is the default backend and the only one whose URL points at a
|
|
110
|
+
file rather than a network host. The shape is always:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
sqlite+aiosqlite:///PATH
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
…where the prefix is fixed and `PATH` is whatever you want SQLAlchemy
|
|
117
|
+
to open. Three useful values for `PATH`:
|
|
118
|
+
|
|
119
|
+
| `PATH` | Resolves to | When to use it |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `./dbname.db` | `dbname.db` in the process working directory | Local dev and the bundled `examples/` apps. |
|
|
122
|
+
| `/var/lib/app/dbname.db` | the absolute file at that path | Production. Point it at the host's persistent volume. |
|
|
123
|
+
| `:memory:` | per-process in-memory DB | Per-test fixtures only — contents vanish at process exit. |
|
|
124
|
+
|
|
125
|
+
So the three full URLs are:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
sqlite+aiosqlite:///./dbname.db
|
|
129
|
+
sqlite+aiosqlite:////var/lib/app/dbname.db
|
|
130
|
+
sqlite+aiosqlite:///:memory:
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The absolute form looks like it has four slashes, but it's the same
|
|
134
|
+
three-slash prefix as the others — the fourth slash is the leading
|
|
135
|
+
`/` of the absolute path. This is the single most common SQLite-URL
|
|
136
|
+
paper-cut.
|
|
137
|
+
|
|
138
|
+
Both file forms create the file on first connection, so a fresh
|
|
139
|
+
checkout running `uv run regstack init && uv run uvicorn …` works
|
|
140
|
+
with no `mkdir` or `touch` step.
|
|
141
|
+
|
|
105
142
|
## JWT
|
|
106
143
|
|
|
107
144
|
```{list-table}
|
|
@@ -234,14 +271,14 @@ The active backend exposes the same five repository protocols on
|
|
|
234
271
|
```toml
|
|
235
272
|
[email]
|
|
236
273
|
backend = "console" # console | smtp | ses
|
|
237
|
-
from_address = "noreply
|
|
238
|
-
from_name = "
|
|
274
|
+
from_address = "noreply@app.example.com"
|
|
275
|
+
from_name = "Example App"
|
|
239
276
|
|
|
240
277
|
# smtp
|
|
241
|
-
smtp_host = "smtp.example.com"
|
|
278
|
+
smtp_host = "smtp.app.example.com"
|
|
242
279
|
smtp_port = 587
|
|
243
280
|
smtp_starttls = true
|
|
244
|
-
smtp_username = "
|
|
281
|
+
smtp_username = "<username>"
|
|
245
282
|
# smtp_password is a SecretStr — set via REGSTACK_EMAIL__SMTP_PASSWORD
|
|
246
283
|
|
|
247
284
|
# ses
|