regstack 0.8.2__tar.gz → 0.8.3__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.8.2 → regstack-0.8.3}/CHANGELOG.md +55 -0
- {regstack-0.8.2 → regstack-0.8.3}/PKG-INFO +1 -1
- {regstack-0.8.2 → regstack-0.8.3}/pyproject.toml +4 -1
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/auth/clock.py +13 -4
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +2 -1
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/protocols.py +5 -0
- regstack-0.8.3/src/regstack/backends/sql/migrations/versions/0003_oauth_state_result_was_new.py +44 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/oauth_state_repo.py +3 -1
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/schema.py +1 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/models/oauth_state.py +7 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/oauth/providers/google.py +24 -2
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/oauth.py +4 -2
- regstack-0.8.3/src/regstack/version.py +1 -0
- regstack-0.8.2/src/regstack/version.py +0 -1
- {regstack-0.8.2 → regstack-0.8.3}/.gitignore +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/LICENSE +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/NOTICE +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/README.md +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/SECURITY.md +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/regstack.toml.example +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/app.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/auth/dependencies.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/auth/jwt.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/auth/lockout.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/auth/password.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/auth/rate_limit.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/base.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/factory.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/backend.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/client.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/indexes.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/backend.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/migrations/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/migrations/env.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/types.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/__main__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/_paths.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/_results.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/admin.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/doctor.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/init.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/capture.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/cli.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/http.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/logtail.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/phases/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/phases/account.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/phases/cleanup.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/phases/core_auth.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/phases/feature_discover.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/phases/oauth.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/phases/password_reset.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/phases/reachability.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/phases/sms_mfa.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/report.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/cli/validate/runner.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/config/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/config/loader.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/config/schema.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/config/secrets.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/base.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/composer.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/console.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/factory.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/ses.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/smtp.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/hooks/events.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/models/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/models/_objectid.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/models/oauth_identity.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/models/user.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/oauth/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/oauth/base.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/oauth/errors.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/oauth/providers/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/oauth/registry.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/_helpers.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/account.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/admin.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/login.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/logout.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/password.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/phone.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/register.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/routers/verify.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/sms/base.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/sms/factory.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/sms/null.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/sms/sns.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/sms/twilio.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/pages.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/static/js/regstack.js +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/auth/_token_handoff.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/auth/login.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/auth/me.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/oauth_google/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/oauth_google/cli.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/oauth_google/routes.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/oauth_google/server.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/oauth_google/validators.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/oauth_google/window.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/oauth_google/writer.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/_aws.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/cli.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/routes.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/server.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/static/wizard.css +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/static/wizard.js +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/templates/wizard.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/validators.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/window.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/ses/writer.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/__init__.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/cli.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/routes.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/server.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/static/designer.css +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/static/designer.js +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/templates/designer.html +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/validators.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/window.py +0 -0
- {regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/writer.py +0 -0
|
@@ -7,6 +7,61 @@ Sphinx docs.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.8.3 — 2026-06-12
|
|
11
|
+
|
|
12
|
+
Closes out the 2026-05-15 → 2026-05-22 daily security-review series,
|
|
13
|
+
fixes `was_new_account` on the OAuth exchange (new migration `0003`),
|
|
14
|
+
and stops the test suite leaking MongoDB databases.
|
|
15
|
+
|
|
16
|
+
**Fixed: Google JWKS fetch now has a timeout.** `PyJWKClient` was
|
|
17
|
+
constructed without a `timeout`, so the synchronous `urllib` fetch
|
|
18
|
+
(offloaded to `asyncio.to_thread`) could pin a worker thread
|
|
19
|
+
indefinitely during a Google JWKS outage and, under sustained load,
|
|
20
|
+
exhaust the bounded asyncio thread pool. Added a 5-second
|
|
21
|
+
`JWKS_FETCH_TIMEOUT_SECONDS`. (Daily security review 2026-05-22 · W-1.)
|
|
22
|
+
|
|
23
|
+
**Fixed: Google OAuth token-exchange error no longer logs the full
|
|
24
|
+
provider response body at WARNING.** On a non-200 token exchange the
|
|
25
|
+
provider's response body is now logged at DEBUG; the raised
|
|
26
|
+
`OAuthTokenExchangeError` (which the router logs at WARNING) carries only
|
|
27
|
+
`HTTP <status>`. (Daily security review 2026-05-22 · I-3.)
|
|
28
|
+
|
|
29
|
+
**Fixed: `/oauth/exchange` now reports `was_new_account` accurately.**
|
|
30
|
+
The field was hardcoded `False`; the callback computed whether it created
|
|
31
|
+
a brand-new account but had nowhere to persist it. Added
|
|
32
|
+
`oauth_states.result_was_new` (migration `0003`) which the callback sets
|
|
33
|
+
and the exchange endpoint reads. (Daily security review 2026-05-22 · I-1.)
|
|
34
|
+
|
|
35
|
+
**Documented: `phone_number` exposure in the admin user listing.**
|
|
36
|
+
`docs/security.md` now spells out that `UserPublic.phone_number` is
|
|
37
|
+
returned in plaintext on `GET /admin/users` (regulated PII in some
|
|
38
|
+
jurisdictions) and that hosts wanting to mask/omit it should wrap the
|
|
39
|
+
admin listing in their own response model. (Daily security review
|
|
40
|
+
2026-05-21 · I-2.)
|
|
41
|
+
|
|
42
|
+
**Fixed: `FrozenClock` now defaults to the far future (2125-01-01).**
|
|
43
|
+
MongoDB's TTL monitor uses real wall-clock time, so the old 2025-01-01
|
|
44
|
+
default meant every TTL-indexed test row was born already-expired and
|
|
45
|
+
could be reaped mid-test when a ~60s TTL sweep landed — a rare,
|
|
46
|
+
mongo-only parallel flake. A far-future pin keeps the reaper out of
|
|
47
|
+
reach while staying deterministic.
|
|
48
|
+
|
|
49
|
+
**Fixed: test runs no longer leak MongoDB databases.** Tests using the
|
|
50
|
+
`make_client` factory bypassed the only fixture that dropped the
|
|
51
|
+
per-test database, leaking one `regstack_test_*` DB per test per run.
|
|
52
|
+
A teardown fixture wired into `config` now drops the DB on every path,
|
|
53
|
+
a run-token-scoped `pytest_sessionfinish` sweep catches anything a
|
|
54
|
+
crashed worker leaves behind, and `inv clean-test-dbs` purges leftovers
|
|
55
|
+
from hard-killed runs.
|
|
56
|
+
|
|
57
|
+
**Triaged: CVE-2026-2978 / CVE-2026-2979 not applicable.** Flagged for
|
|
58
|
+
monitoring in the 2026-05-22 review; published NVD details show both
|
|
59
|
+
affect the third-party "FastApiAdmin" project, not the `fastapi`
|
|
60
|
+
library. regstack has no `fastapi-admin` dependency and no fastapi-core
|
|
61
|
+
advisories affect versions ≥ 0.120.0 (our floor). Triage note recorded
|
|
62
|
+
beside the floor in `pyproject.toml`. (Daily security review
|
|
63
|
+
2026-05-22 · I-2.)
|
|
64
|
+
|
|
10
65
|
## 0.8.0 — 2026-05-19
|
|
11
66
|
|
|
12
67
|
`regstack ses setup` guided wizard, plus two security fixes from
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regstack
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.3
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "regstack"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.3"
|
|
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"
|
|
@@ -19,6 +19,9 @@ classifiers = [
|
|
|
19
19
|
dependencies = [
|
|
20
20
|
# fastapi>=0.120.0 picks up CVE-2025-62727 (Starlette DoS via large
|
|
21
21
|
# request bodies after multipart processing).
|
|
22
|
+
# CVE-2026-2978 / CVE-2026-2979 (flagged for monitoring in the
|
|
23
|
+
# 2026-05-22 security review, I-2) affect the third-party
|
|
24
|
+
# "FastApiAdmin" project, not fastapi itself — no floor bump needed.
|
|
22
25
|
"fastapi>=0.120.0",
|
|
23
26
|
"pydantic>=2.6",
|
|
24
27
|
"pydantic-settings>=2.2",
|
|
@@ -42,14 +42,23 @@ class FrozenClock:
|
|
|
42
42
|
"""
|
|
43
43
|
|
|
44
44
|
def __init__(self, start: datetime | None = None) -> None:
|
|
45
|
-
"""Pin the clock at ``start`` (default
|
|
45
|
+
"""Pin the clock at ``start`` (default 2125-01-01 UTC).
|
|
46
|
+
|
|
47
|
+
The default is deliberately ~100 years in the *future*. MongoDB's
|
|
48
|
+
TTL monitor deletes documents by comparing their ``expires_at``
|
|
49
|
+
against real wall-clock time on a ~60s cycle — it knows nothing
|
|
50
|
+
about this injected clock. A frozen "now" in the past would give
|
|
51
|
+
every TTL-indexed test row (oauth_states, pending_registrations,
|
|
52
|
+
mfa_codes, login_attempts, blacklist) an already-elapsed
|
|
53
|
+
``expires_at``, and any test straddling a TTL sweep would have
|
|
54
|
+
its rows reaped mid-flight. A far-future pin keeps the reaper
|
|
55
|
+
permanently out of reach while staying deterministic.
|
|
46
56
|
|
|
47
57
|
Args:
|
|
48
58
|
start: The initial timestamp. Should be tz-aware. Defaults
|
|
49
|
-
to ``
|
|
50
|
-
memorable.
|
|
59
|
+
to ``2125-01-01T00:00:00Z``.
|
|
51
60
|
"""
|
|
52
|
-
self._now = start or datetime(
|
|
61
|
+
self._now = start or datetime(2125, 1, 1, tzinfo=UTC)
|
|
53
62
|
|
|
54
63
|
def now(self) -> datetime:
|
|
55
64
|
"""Return the currently-pinned timestamp."""
|
{regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/oauth_state_repo.py
RENAMED
|
@@ -31,8 +31,9 @@ class MongoOAuthStateRepo:
|
|
|
31
31
|
token: str,
|
|
32
32
|
*,
|
|
33
33
|
new_expires_at: datetime | None = None,
|
|
34
|
+
was_new: bool = False,
|
|
34
35
|
) -> None:
|
|
35
|
-
updates: dict[str, Any] = {"result_token": token}
|
|
36
|
+
updates: dict[str, Any] = {"result_token": token, "result_was_new": was_new}
|
|
36
37
|
if new_expires_at is not None:
|
|
37
38
|
updates["expires_at"] = new_expires_at
|
|
38
39
|
await self._collection.update_one(
|
|
@@ -295,6 +295,7 @@ class OAuthStateRepoProtocol(Protocol):
|
|
|
295
295
|
token: str,
|
|
296
296
|
*,
|
|
297
297
|
new_expires_at: datetime | None = None,
|
|
298
|
+
was_new: bool = False,
|
|
298
299
|
) -> None:
|
|
299
300
|
"""Stash the session JWT after a successful callback so the
|
|
300
301
|
SPA can pick it up via :meth:`consume`.
|
|
@@ -305,6 +306,10 @@ class OAuthStateRepoProtocol(Protocol):
|
|
|
305
306
|
``oauth.state_ttl_seconds`` (covering the round-trip with
|
|
306
307
|
the provider) to ``oauth.completion_ttl_seconds`` (covering
|
|
307
308
|
only the SPA's exchange call after the callback lands).
|
|
309
|
+
|
|
310
|
+
``was_new`` records whether the callback created a brand-new
|
|
311
|
+
account, so the exchange response can report
|
|
312
|
+
``was_new_account`` accurately.
|
|
308
313
|
"""
|
|
309
314
|
...
|
|
310
315
|
|
regstack-0.8.3/src/regstack/backends/sql/migrations/versions/0003_oauth_state_result_was_new.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Add oauth_states.result_was_new.
|
|
2
|
+
|
|
3
|
+
Revision ID: 0003
|
|
4
|
+
Revises: 0002
|
|
5
|
+
Create Date: 2026-06-04
|
|
6
|
+
|
|
7
|
+
The OAuth callback computes whether it created a brand-new account
|
|
8
|
+
(vs. signing an existing one in) but had nowhere to persist it, so the
|
|
9
|
+
``POST /oauth/exchange`` response always reported
|
|
10
|
+
``was_new_account=False``. This adds the column the callback writes and
|
|
11
|
+
the exchange endpoint reads (Security review 2026-05-22 · I-1).
|
|
12
|
+
|
|
13
|
+
The column is NOT NULL with a ``False`` server default so the in-flight
|
|
14
|
+
state rows that exist at migration time (if any) get a sensible value
|
|
15
|
+
without a backfill. ``batch_alter_table`` keeps the ADD COLUMN safe on
|
|
16
|
+
SQLite.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import sqlalchemy as sa
|
|
22
|
+
from alembic import op
|
|
23
|
+
|
|
24
|
+
revision: str = "0003"
|
|
25
|
+
down_revision: str | None = "0002"
|
|
26
|
+
branch_labels: tuple[str, ...] | None = None
|
|
27
|
+
depends_on: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def upgrade() -> None:
|
|
31
|
+
with op.batch_alter_table("oauth_states") as batch_op:
|
|
32
|
+
batch_op.add_column(
|
|
33
|
+
sa.Column(
|
|
34
|
+
"result_was_new",
|
|
35
|
+
sa.Boolean(),
|
|
36
|
+
nullable=False,
|
|
37
|
+
server_default=sa.false(),
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def downgrade() -> None:
|
|
43
|
+
with op.batch_alter_table("oauth_states") as batch_op:
|
|
44
|
+
batch_op.drop_column("result_was_new")
|
{regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/oauth_state_repo.py
RENAMED
|
@@ -34,6 +34,7 @@ class SqlOAuthStateRepo:
|
|
|
34
34
|
"created_at": state.created_at,
|
|
35
35
|
"expires_at": state.expires_at,
|
|
36
36
|
"result_token": state.result_token,
|
|
37
|
+
"result_was_new": state.result_was_new,
|
|
37
38
|
}
|
|
38
39
|
async with self._engine.begin() as conn:
|
|
39
40
|
await conn.execute(self._t.insert().values(values))
|
|
@@ -50,8 +51,9 @@ class SqlOAuthStateRepo:
|
|
|
50
51
|
token: str,
|
|
51
52
|
*,
|
|
52
53
|
new_expires_at: datetime | None = None,
|
|
54
|
+
was_new: bool = False,
|
|
53
55
|
) -> None:
|
|
54
|
-
values: dict[str, Any] = {"result_token": token}
|
|
56
|
+
values: dict[str, Any] = {"result_token": token, "result_was_new": was_new}
|
|
55
57
|
if new_expires_at is not None:
|
|
56
58
|
values["expires_at"] = new_expires_at
|
|
57
59
|
stmt = update(self._t).where(self._t.c.id == state_id).values(**values)
|
|
@@ -162,6 +162,7 @@ def _oauth_states(table_name: str) -> Table:
|
|
|
162
162
|
Column("created_at", UtcDateTime(), nullable=False),
|
|
163
163
|
Column("expires_at", UtcDateTime(), nullable=False),
|
|
164
164
|
Column("result_token", Text, nullable=True),
|
|
165
|
+
Column("result_was_new", Boolean, nullable=False, default=False),
|
|
165
166
|
Index("ix_oauth_states_expires_at", "expires_at"),
|
|
166
167
|
CheckConstraint("mode IN ('signin', 'link')", name="mode_valid"),
|
|
167
168
|
)
|
|
@@ -77,6 +77,13 @@ class OAuthState(BaseModel):
|
|
|
77
77
|
``id`` for this value via ``POST /oauth/exchange``; the row is
|
|
78
78
|
deleted on exchange."""
|
|
79
79
|
|
|
80
|
+
result_was_new: bool = False
|
|
81
|
+
"""Whether the callback created a brand-new account (vs. signing in
|
|
82
|
+
an existing one). Set alongside ``result_token`` so the
|
|
83
|
+
``POST /oauth/exchange`` response can surface ``was_new_account``
|
|
84
|
+
accurately — the callback computes this but had no field to persist
|
|
85
|
+
it on before (Security review 2026-05-22 · I-1)."""
|
|
86
|
+
|
|
80
87
|
def to_mongo(self) -> dict[str, Any]:
|
|
81
88
|
data = self.model_dump(by_alias=True, exclude_none=True)
|
|
82
89
|
return data
|
|
@@ -17,6 +17,7 @@ installed and turns ``enable_oauth`` on.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import asyncio
|
|
20
|
+
import logging
|
|
20
21
|
from typing import TYPE_CHECKING, Any
|
|
21
22
|
from urllib.parse import urlencode
|
|
22
23
|
|
|
@@ -30,6 +31,8 @@ from regstack.oauth.errors import OAuthIdTokenError, OAuthTokenExchangeError
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
31
32
|
from collections.abc import Iterable
|
|
32
33
|
|
|
34
|
+
log = logging.getLogger("regstack.oauth.google")
|
|
35
|
+
|
|
33
36
|
GOOGLE_ISSUER = "https://accounts.google.com"
|
|
34
37
|
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
|
35
38
|
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
|
@@ -37,6 +40,13 @@ GOOGLE_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs"
|
|
|
37
40
|
|
|
38
41
|
DEFAULT_SCOPES: tuple[str, ...] = ("openid", "email", "profile")
|
|
39
42
|
|
|
43
|
+
# Bound the synchronous JWKS fetch so a slow or unreachable Google JWKS
|
|
44
|
+
# endpoint can't pin a worker thread indefinitely. The fetch is offloaded
|
|
45
|
+
# to `asyncio.to_thread`, but `urllib` defaults to no timeout — under a
|
|
46
|
+
# sustained JWKS outage that would let cache-miss requests exhaust the
|
|
47
|
+
# bounded asyncio thread pool. (Security review 2026-05-22 · W-1.)
|
|
48
|
+
JWKS_FETCH_TIMEOUT_SECONDS = 5
|
|
49
|
+
|
|
40
50
|
|
|
41
51
|
class GoogleProvider(OAuthProvider):
|
|
42
52
|
"""OIDC provider for Google.
|
|
@@ -80,7 +90,9 @@ class GoogleProvider(OAuthProvider):
|
|
|
80
90
|
self._owns_http = http is None
|
|
81
91
|
self._issuer = issuer
|
|
82
92
|
self._scopes = tuple(scopes)
|
|
83
|
-
self._jwks_client = PyJWKClient(
|
|
93
|
+
self._jwks_client = PyJWKClient(
|
|
94
|
+
jwks_url, cache_keys=True, timeout=JWKS_FETCH_TIMEOUT_SECONDS
|
|
95
|
+
)
|
|
84
96
|
|
|
85
97
|
@property
|
|
86
98
|
def name(self) -> str:
|
|
@@ -141,8 +153,18 @@ class GoogleProvider(OAuthProvider):
|
|
|
141
153
|
if self._owns_http and client is not self._http:
|
|
142
154
|
await client.aclose()
|
|
143
155
|
if response.status_code != 200:
|
|
156
|
+
# Keep the provider's response body at DEBUG only. It doesn't
|
|
157
|
+
# carry our client secret or the auth code, but Google's error
|
|
158
|
+
# bodies are verbose and there's no reason to put them in
|
|
159
|
+
# production WARNING logs — the status code is the actionable
|
|
160
|
+
# signal. (Security review 2026-05-22 · I-3.)
|
|
161
|
+
log.debug(
|
|
162
|
+
"google token exchange response body (HTTP %s): %s",
|
|
163
|
+
response.status_code,
|
|
164
|
+
response.text,
|
|
165
|
+
)
|
|
144
166
|
raise OAuthTokenExchangeError(
|
|
145
|
-
f"google token exchange failed: {response.status_code}
|
|
167
|
+
f"google token exchange failed: HTTP {response.status_code}"
|
|
146
168
|
)
|
|
147
169
|
body: dict[str, Any] = response.json()
|
|
148
170
|
try:
|
|
@@ -168,7 +168,7 @@ def build_oauth_router(rs: RegStack) -> APIRouter:
|
|
|
168
168
|
if _is_mfa_pending_token(state.result_token):
|
|
169
169
|
return ExchangeResponse(
|
|
170
170
|
redirect_to=state.redirect_to,
|
|
171
|
-
was_new_account=
|
|
171
|
+
was_new_account=state.result_was_new,
|
|
172
172
|
expires_in=rs.config.mfa_pending_token_ttl_seconds,
|
|
173
173
|
mfa_required=True,
|
|
174
174
|
mfa_pending_token=state.result_token,
|
|
@@ -176,7 +176,7 @@ def build_oauth_router(rs: RegStack) -> APIRouter:
|
|
|
176
176
|
return ExchangeResponse(
|
|
177
177
|
access_token=state.result_token,
|
|
178
178
|
redirect_to=state.redirect_to,
|
|
179
|
-
was_new_account=
|
|
179
|
+
was_new_account=state.result_was_new,
|
|
180
180
|
expires_in=rs.config.jwt_ttl_seconds,
|
|
181
181
|
)
|
|
182
182
|
|
|
@@ -361,6 +361,7 @@ def build_oauth_router(rs: RegStack) -> APIRouter:
|
|
|
361
361
|
pending_token,
|
|
362
362
|
new_expires_at=rs.clock.now()
|
|
363
363
|
+ timedelta(seconds=rs.config.mfa_pending_token_ttl_seconds),
|
|
364
|
+
was_new=was_new,
|
|
364
365
|
)
|
|
365
366
|
else:
|
|
366
367
|
# Mint the session JWT, stash it on the state row for the
|
|
@@ -376,6 +377,7 @@ def build_oauth_router(rs: RegStack) -> APIRouter:
|
|
|
376
377
|
token,
|
|
377
378
|
new_expires_at=rs.clock.now()
|
|
378
379
|
+ timedelta(seconds=rs.config.oauth.completion_ttl_seconds),
|
|
380
|
+
was_new=was_new,
|
|
379
381
|
)
|
|
380
382
|
|
|
381
383
|
await rs.hooks.fire(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.3"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/blacklist_repo.py
RENAMED
|
File without changes
|
{regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/login_attempt_repo.py
RENAMED
|
File without changes
|
|
File without changes
|
{regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/migrations/versions/0002_oauth.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/login_attempt_repo.py
RENAMED
|
File without changes
|
|
File without changes
|
{regstack-0.8.2 → regstack-0.8.3}/src/regstack/backends/sql/repositories/oauth_identity_repo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{regstack-0.8.2 → regstack-0.8.3}/src/regstack/wizard/theme_designer/templates/designer.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|