mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-13 07:30:59 +01:00
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
_sync.py 456 -> 245 + _syncstate 131 + _pull 106 + _revisions 34 _firebase_auth 412 -> 242 + _credentials 146 + _autherrors 69 _firebase.py 336 -> 250 + _fbkeys 100 google_id_token 316 -> 232 + tool/_oauth_callback 105 _github.py 255 -> 238 + _gherrors 30 revision_of became its own leaf module because both the push and pull halves need it; without that the two would have imported each other. _fbkeys holds the RTDB key escaping — the escape tables moved with the functions that read them, since the pair has to round-trip exactly or a device silently stops seeing its own writes. crdt-sync/tool/ is outside the coverage source, so the split there has no test net: verified by running it the documented way instead (`python3 -m tool.google_id_token --help` prints its usage, and `from tool.google_id_token import TokenError, fetch_id_token` — what seed_session.py does — still resolves). An AST pass over each pair caught what the suite would not have: _is_revoked needs the error type, so it stayed with it rather than following the credentials; _TERMINAL_AUTH_REASONS and _CONSENT_TIMEOUT_SECONDS had to be carried to their new callers. 259 passed at 100% branch coverage. crdt-sync/tool/seed_session.py and firebase-debug.log are pre-existing local changes and are deliberately not in this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
"""Turning a Firebase auth response into a message, and reading its verdict.
|
|
|
|
Split from :mod:`crdt_sync._firebase_auth`, which keeps the token provider.
|
|
identitytoolkit reports failures as a nested JSON error whose useful part is
|
|
buried; these turn that into something a log line can carry, and decide
|
|
whether the failure is permanent (the refresh token is dead and the user must
|
|
sign in again) or worth retrying.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from crdt_sync._remote import RemoteSyncError
|
|
|
|
if TYPE_CHECKING:
|
|
import requests as _requests
|
|
|
|
|
|
class FirebaseAuthError(RemoteSyncError):
|
|
"""Raised for an authentication failure the caller must not ignore.
|
|
|
|
A :class:`crdt_sync.RemoteSyncError` so callers that only care about
|
|
"sync is broken" can catch one type, while a settings screen can single
|
|
this out to say "your password is wrong" rather than "the network is
|
|
down".
|
|
"""
|
|
|
|
def _reason(response: _requests.Response) -> str:
|
|
"""Return Google's machine-readable error reason, in parentheses.
|
|
|
|
Pulls ``INVALID_PASSWORD`` / ``TOKEN_EXPIRED`` / ``USER_DISABLED`` out of
|
|
the body so the raised message says what actually went wrong rather than
|
|
just reporting a status code.
|
|
"""
|
|
try:
|
|
data = response.json()
|
|
except ValueError:
|
|
# A non-JSON body (a proxy error page, say): the status code is all
|
|
# the detail there is.
|
|
return ""
|
|
if not isinstance(data, dict):
|
|
return ""
|
|
error = data.get("error")
|
|
if isinstance(error, dict):
|
|
return f" ({error.get('message')})"
|
|
if isinstance(error, str):
|
|
return f" ({error})"
|
|
return ""
|
|
|
|
|
|
#: Reasons that mean the refresh token is permanently dead. Only these clear
|
|
#: the session: a network error or a 5xx must not, because a device that signs
|
|
#: itself out whenever the wifi drops needs a manual sign-in to recover, which
|
|
#: is a worse failure than the stale banner this guards against.
|
|
_TERMINAL_AUTH_REASONS = (
|
|
"TOKEN_EXPIRED",
|
|
"USER_DISABLED",
|
|
"USER_NOT_FOUND",
|
|
"INVALID_REFRESH_TOKEN",
|
|
"INVALID_GRANT_TYPE",
|
|
"MISSING_REFRESH_TOKEN",
|
|
)
|
|
|
|
|
|
def _is_revoked(error: FirebaseAuthError) -> bool:
|
|
"""Return whether ``error`` means the refresh token is permanently dead."""
|
|
message = str(error)
|
|
return any(reason in message for reason in _TERMINAL_AUTH_REASONS)
|