"""gitea_auth.py — canonical Gitea API authentication (handoff 2026-09-11 §H). One deterministic helper for authenticated Gitea API access. Never prints credentials. Fails clearly when unavailable. Credential ladder: 1. GITEA_TOKEN environment variable (preferred for CI/deployment) 2. private token file (GITEA_TOKEN_FILE, default ~/.config/opencode/secrets/gitea-token, must be mode 0600) 3. git credential fill (uses the credential helper / store) Never parses credentials from git remote URLs. """ from __future__ import annotations import json import os import stat import subprocess import urllib.request from pathlib import Path from typing import Any DEFAULT_SERVER = "https://gitea.thingscouldgetdicey.online" DEFAULT_REPO = "conductor/rune" DEFAULT_TOKEN_FILE = Path("~/.config/opencode/secrets/gitea-token").expanduser() GITEA_HOST = "gitea.thingscouldgetdicey.online" class GiteaCredentialError(RuntimeError): """Raised when no Gitea credential is available. Message never contains one.""" def _token_from_file() -> str: path = Path(os.environ.get("GITEA_TOKEN_FILE", DEFAULT_TOKEN_FILE)).expanduser() if not path.is_file(): return "" mode = stat.S_IMODE(path.stat().st_mode) if mode & 0o077: raise PermissionError(f"Gitea token file must be mode 0600: {path}") return path.read_text(encoding="utf-8").strip() def _token_from_credential_helper() -> str: try: result = subprocess.run( ["git", "credential", "fill"], input=f"protocol=https\nhost={GITEA_HOST}\nusername=conductor\n\n", capture_output=True, text=True, timeout=10, ) for line in result.stdout.splitlines(): if line.startswith("password="): return line[len("password=") :] except (subprocess.TimeoutExpired, OSError): pass return "" def has_token() -> bool: """True if any rung of the credential ladder answers. Never raises.""" try: get_token() return True except Exception: # noqa: BLE001 — probe-only: any failure means "no credential available" return False def get_token() -> str: """Obtain the Gitea API token. Never prints the token.""" token = os.environ.get("GITEA_TOKEN", "").strip() if token: return token token = _token_from_file() if token: return token token = _token_from_credential_helper() if token: return token raise GiteaCredentialError( "Gitea credentials unavailable: set GITEA_TOKEN, create a mode-0600 " f"token file at {DEFAULT_TOKEN_FILE}, or configure a git credential " f"helper for {GITEA_HOST}" ) def api( path: str, method: str = "GET", data: dict | None = None, server: str = DEFAULT_SERVER, repo: str = DEFAULT_REPO, ) -> tuple[int, Any]: """Make an authenticated Gitea API request. Returns (status_code, parsed_body).""" token = get_token() url = f"{server}/api/v1/repos/{repo}{path}" req = urllib.request.Request( url, data=json.dumps(data).encode() if data is not None else None, headers={"Authorization": f"token {token}", "Content-Type": "application/json"}, method=method, ) try: with urllib.request.urlopen(req, timeout=30) as r: body, status = r.read(), r.status except urllib.error.HTTPError as e: body, status = e.read(), e.code try: return status, (json.loads(body) if body else {}) except json.JSONDecodeError: return status, {"_raw": body.decode("utf-8", "replace")[:200]}