mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 21:27:14 +01:00
The hook entry was an absolute /home/kuhy/utils path, copied from a config written for ~/.claude where it only ever ran locally. On an Actions runner that is exit code 127 — "No such file or directory" — so gatelock-precommit and crdt-sync-precommit failed on every commit of this refactor while the hook silently checked nothing. Now repo-relative, matching the sibling hooks (`entry: scripts/check_subproject_scope.sh`): pre-commit always runs from the git root, so it resolves the same in both places. Fixing that made the CI configs runnable locally, and `--all-files` then found what per-file runs had missed — ruff pruning imports that exist *for* their re-export. `import crdt_sync` was broken three separate ways (FileSyncStateStore, encode_key, FileCredentialStore). Each affected module now states an explicit __all__ so the autofixer cannot take them again, and the two files that pushed past the cap once __all__ was added are split further: _firebase.py 260 -> 247 + _fberrors 25 _sync.py 257 -> 188 + _syncargs 91 Test patch targets followed the code they patch: gatelock._outputs.shutil -> _xrandr.shutil, gatelock._arbiter.Path -> _claims.Path. gatelock and crdt-sync both pass `pre-commit run --all-files` in full; 389 and 259 tests at 100% branch coverage; the root gate exits 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
71 lines
2.3 KiB
Python
71 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)
|