regstack 0.5.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.5.0 → regstack-0.5.6}/.github/workflows/publish.yml +6 -1
- {regstack-0.5.0 → regstack-0.5.6}/CHANGELOG.md +81 -0
- {regstack-0.5.0 → regstack-0.5.6}/PKG-INFO +10 -5
- {regstack-0.5.0 → regstack-0.5.6}/docs/architecture.md +7 -3
- {regstack-0.5.0 → regstack-0.5.6}/docs/changelog.md +121 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/configuration.md +38 -2
- {regstack-0.5.0 → regstack-0.5.6}/docs/quickstart.md +8 -3
- {regstack-0.5.0 → regstack-0.5.6}/docs/security.md +50 -3
- {regstack-0.5.0 → regstack-0.5.6}/pyproject.toml +19 -5
- regstack-0.5.6/scripts/ccr_coverage_setup.py +242 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/app.py +55 -2
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/dependencies.py +12 -4
- {regstack-0.5.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.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/backend.py +5 -6
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/client.py +9 -3
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/indexes.py +2 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/blacklist_repo.py +14 -2
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +3 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +2 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +3 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +3 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/pending_repo.py +3 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/user_repo.py +3 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/__init__.py +3 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/env.py +5 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -2
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +4 -6
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/oauth_state_repo.py +3 -3
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/pending_repo.py +4 -6
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/user_repo.py +4 -4
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/types.py +4 -2
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/doctor.py +9 -5
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/config/schema.py +18 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/composer.py +9 -2
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/ses.py +1 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/hooks/events.py +7 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/_objectid.py +1 -1
- {regstack-0.5.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.5.0 → regstack-0.5.6}/src/regstack/routers/account.py +5 -23
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/admin.py +4 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/login.py +12 -5
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/logout.py +3 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/oauth.py +6 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/phone.py +12 -3
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/sns.py +1 -1
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/twilio.py +1 -1
- {regstack-0.5.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.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/writer.py +1 -3
- {regstack-0.5.0 → regstack-0.5.6}/tasks.py +16 -1
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_admin_router.py +39 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_happy_path.py +40 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_mfa.py +110 -0
- {regstack-0.5.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.0 → regstack-0.5.6}/uv.lock +54 -6
- regstack-0.5.0/src/regstack/version.py +0 -1
- {regstack-0.5.0 → regstack-0.5.6}/.github/workflows/test.yml +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/.gitignore +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/.python-version +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/.readthedocs.yaml +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/CLAUDE.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/LICENSE +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/NOTICE +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/README.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/SECURITY.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/_static/.gitkeep +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/_templates/.gitkeep +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/api.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/cli.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/conf.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/embedding.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/index.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/oauth.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/security-reports/README.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/docs/theming.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/_common/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/_common/app.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/mongo/README.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/mongo/branding/theme.css +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/mongo/main.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/mongo/regstack.toml +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/postgres/README.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/postgres/main.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/postgres/regstack.toml +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/sqlite/README.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/sqlite/main.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/examples/sqlite/regstack.toml +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/regstack.toml.example +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/scripts/security-review-prompt.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/clock.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/jwt.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/lockout.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/base.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/factory.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/protocols.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/backend.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/backends/sql/schema.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/__main__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/admin.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/init.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/config/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/config/loader.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/config/secrets.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/base.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/console.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/factory.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/smtp.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/oauth_identity.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/oauth_state.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/base.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/errors.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/providers/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/providers/google.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/oauth/registry.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/password.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/register.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/routers/verify.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/base.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/factory.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/sms/null.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/static/js/regstack.js +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/login.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/me.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/cli.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/routes.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/server.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/validators.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/window.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/oauth_google/writer.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/cli.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/routes.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/server.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/static/designer.css +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/static/designer.js +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/templates/designer.html +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/validators.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/src/regstack/wizard/theme_designer/window.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tasks/oauth-design.md +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/_fake_google/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/_fake_google/provider.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/conftest.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/e2e/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/e2e/conftest.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/e2e/test_theme_designer.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/e2e/test_wizard_oauth_flow.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_account_management.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_indexes.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_login_lockout.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_oauth_repos.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_oauth_ui.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_password_reset.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_sql_migrations.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_ui_router.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/integration/test_verification.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/__init__.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_base_install_imports.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_cli.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_cli_doctor.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_cli_init.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_cli_migrate.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_config_loader.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_jwt.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_lockout.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_mail_composer.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_mfa_code_repo.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_oauth_google.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_password.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_ses_backend.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_sms.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_smtp_backend.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_theme_designer_cli.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_theme_designer_routes.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_theme_designer_validators.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_theme_designer_writer.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_ui_env.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_cli.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_routes.py +0 -0
- {regstack-0.5.0 → regstack-0.5.6}/tests/unit/test_wizard_oauth_validators.py +0 -0
- {regstack-0.5.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regstack
|
|
3
|
-
Version: 0.5.
|
|
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
|
|
@@ -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,127 @@
|
|
|
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
|
+
|
|
6
127
|
## 0.5.0 — 2026-05-02
|
|
7
128
|
|
|
8
129
|
### Added
|
|
@@ -85,9 +85,10 @@ regstack picks a backend at construction time from the URL scheme of
|
|
|
85
85
|
- Notes
|
|
86
86
|
|
|
87
87
|
* - SQLite
|
|
88
|
-
- `sqlite+aiosqlite:///./dbname.db`
|
|
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
93
|
- `postgresql+asyncpg://<username>:<password>@dbhost.example.com:5432/dbname`
|
|
93
94
|
- Requires the `postgres` extra (pulls in `asyncpg`). The driver is
|
|
@@ -103,6 +104,41 @@ The active backend exposes the same five repository protocols on
|
|
|
103
104
|
``RegStack.users``, ``.pending``, ``.blacklist``, ``.attempts``,
|
|
104
105
|
``.mfa_codes``. Routers / hooks never branch on backend kind.
|
|
105
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
|
+
|
|
106
142
|
## JWT
|
|
107
143
|
|
|
108
144
|
```{list-table}
|
|
@@ -91,9 +91,14 @@ app.include_router(regstack.router, prefix=config.api_prefix)
|
|
|
91
91
|
`RegStack` picks the right backend automatically from the URL scheme of
|
|
92
92
|
`config.database_url`:
|
|
93
93
|
|
|
94
|
-
- `sqlite+aiosqlite
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
- `sqlite+aiosqlite:///PATH` → SQLite via SQLAlchemy. `PATH` is the
|
|
95
|
+
filename (e.g. `./dbname.db`) or the literal `:memory:`. See
|
|
96
|
+
[SQLite URL forms](configuration.md#sqlite-url-forms) for the
|
|
97
|
+
absolute-path variant.
|
|
98
|
+
- `postgresql+asyncpg://<username>:<password>@dbhost.example.com:5432/dbname`
|
|
99
|
+
→ Postgres via SQLAlchemy
|
|
100
|
+
- `mongodb://<username>:<password>@dbhost.example.com:27017/dbname`
|
|
101
|
+
(or `mongodb+srv://…`) → MongoDB
|
|
97
102
|
|
|
98
103
|
`install_schema()` is idempotent. On SQL backends it runs Alembic
|
|
99
104
|
migrations to head; on MongoDB it ensures the indexes exist. Calling
|
|
@@ -104,6 +104,51 @@ visible to logged-out users only.
|
|
|
104
104
|
failures.
|
|
105
105
|
- Disabled in tests via `rate_limit_disabled=True`.
|
|
106
106
|
|
|
107
|
+
## Per-route IP rate limits
|
|
108
|
+
|
|
109
|
+
Lockout defends each *account* against credential-stuffing. It does
|
|
110
|
+
nothing for an IP that hammers `/forgot-password`, `/register`, or
|
|
111
|
+
`/verify` against many accounts. For that, regstack supports
|
|
112
|
+
slowapi-backed per-route IP rate limits:
|
|
113
|
+
|
|
114
|
+
- Opt in by installing the `rate_limit` extra (`pip install
|
|
115
|
+
regstack[rate_limit]`) **or** by passing a host-built
|
|
116
|
+
`slowapi.Limiter` to `RegStack(rate_limiter=...)`. Hosts already
|
|
117
|
+
using slowapi should pass their own Limiter so it shares state
|
|
118
|
+
with the rest of the app.
|
|
119
|
+
- Set any of the `*_rate_limit` config fields to a slowapi-syntax
|
|
120
|
+
string. Each empty / unset field means "no limit on this route":
|
|
121
|
+
|
|
122
|
+
```toml
|
|
123
|
+
login_rate_limit = "30/minute;200/hour"
|
|
124
|
+
register_rate_limit = "10/minute;50/hour"
|
|
125
|
+
forgot_password_rate_limit = "5/minute;20/hour"
|
|
126
|
+
reset_password_rate_limit = "5/minute;20/hour"
|
|
127
|
+
verify_rate_limit = "10/minute;60/hour"
|
|
128
|
+
resend_verification_rate_limit = "5/minute;30/hour"
|
|
129
|
+
change_password_rate_limit = "5/minute;20/hour"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- Hosts still own slowapi's app-level wiring:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
136
|
+
from slowapi.errors import RateLimitExceeded
|
|
137
|
+
from slowapi.util import get_remote_address
|
|
138
|
+
|
|
139
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
140
|
+
app.state.limiter = limiter
|
|
141
|
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
142
|
+
|
|
143
|
+
rs = RegStack(config=cfg, rate_limiter=limiter)
|
|
144
|
+
app.include_router(rs.router, prefix="/api/auth")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- Failing closed: if `*_rate_limit` is set but neither a Limiter
|
|
148
|
+
nor the extra is available, `regstack.router` raises
|
|
149
|
+
`RuntimeError` on first access. We never silently disable a
|
|
150
|
+
configured protection.
|
|
151
|
+
|
|
107
152
|
## Email verification (durable, hashed token)
|
|
108
153
|
|
|
109
154
|
- Random 32-byte URL-safe token, SHA-256 hashed in
|
|
@@ -267,9 +312,11 @@ avoids that:
|
|
|
267
312
|
- **Content Security Policy headers.** regstack's SSR layer is
|
|
268
313
|
CSP-friendly but the host emits the `Content-Security-Policy`
|
|
269
314
|
response header.
|
|
270
|
-
- **Rate-limiting beyond the per-account login lockout.**
|
|
271
|
-
|
|
272
|
-
rate limits
|
|
315
|
+
- **Rate-limiting beyond the per-account login lockout.** Per-route
|
|
316
|
+
IP limits ship as the optional `rate_limit` extra (see *Per-route
|
|
317
|
+
IP rate limits* above). Host-level rate limiting (nginx, Cloudfront,
|
|
318
|
+
…) is still the right place to push back broad attack traffic that
|
|
319
|
+
isn't worth letting hit Python at all.
|
|
273
320
|
- **Backups, MongoDB user permissions, network-level isolation** between
|
|
274
321
|
the app and the database.
|
|
275
322
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "regstack"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.6"
|
|
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"
|
|
@@ -21,11 +21,14 @@ dependencies = [
|
|
|
21
21
|
"pydantic>=2.6",
|
|
22
22
|
"pydantic-settings>=2.2",
|
|
23
23
|
"pwdlib[argon2]>=0.2.1",
|
|
24
|
-
|
|
24
|
+
# pyjwt>=2.12.1 includes the fix for CVE-2026-32597 (`crit` header bypass).
|
|
25
|
+
"pyjwt>=2.12.1",
|
|
25
26
|
"jinja2>=3.1",
|
|
26
27
|
"click>=8.1",
|
|
27
28
|
"dnspython>=2.6",
|
|
28
|
-
|
|
29
|
+
# python-multipart>=0.0.26 picks up CVE-2026-40347 (DoS via oversized
|
|
30
|
+
# multipart preamble).
|
|
31
|
+
"python-multipart>=0.0.26",
|
|
29
32
|
"email-validator>=2.1",
|
|
30
33
|
"aiosmtplib>=3.0",
|
|
31
34
|
# SQL stack — bundled by default because SQLite is the default backend.
|
|
@@ -45,7 +48,15 @@ mongo = ["pymongo>=4.9"]
|
|
|
45
48
|
ses = ["aioboto3>=12.3"]
|
|
46
49
|
sns = ["aioboto3>=12.3"]
|
|
47
50
|
twilio = ["twilio>=9.0"]
|
|
48
|
-
|
|
51
|
+
# cryptography>=46.0.7 picks up CVE-2026-26007 (ECC subgroup attack on the
|
|
52
|
+
# JWKS code path) plus CVE-2026-34073 and CVE-2026-39892.
|
|
53
|
+
oauth = ["pyjwt[crypto]>=2.12.1", "cryptography>=46.0.7"]
|
|
54
|
+
# Per-route rate limiting on the auth router. The limiter itself can be
|
|
55
|
+
# host-supplied (so hosts that already use slowapi share the same Limiter
|
|
56
|
+
# state), otherwise regstack constructs an in-memory one.
|
|
57
|
+
rate_limit = [
|
|
58
|
+
"slowapi>=0.1.9",
|
|
59
|
+
]
|
|
49
60
|
docs = [
|
|
50
61
|
"sphinx>=7.3",
|
|
51
62
|
"myst-parser>=3.0",
|
|
@@ -69,9 +80,12 @@ dev = [
|
|
|
69
80
|
"pymongo>=4.9",
|
|
70
81
|
"asyncpg>=0.29",
|
|
71
82
|
# OAuth provider tests need the crypto bits to verify ID tokens.
|
|
72
|
-
"pyjwt[crypto]>=2.
|
|
83
|
+
"pyjwt[crypto]>=2.12.1",
|
|
84
|
+
"cryptography>=46.0.7",
|
|
73
85
|
# E2E tests for the OAuth setup wizard (drives the SPA in headless Chromium).
|
|
74
86
|
"pytest-playwright>=0.5",
|
|
87
|
+
# Rate-limit tests exercise the slowapi integration.
|
|
88
|
+
"slowapi>=0.1.9",
|
|
75
89
|
]
|
|
76
90
|
|
|
77
91
|
[project.scripts]
|