"""High-level: mint CPA xai-*.json for one free registered account.""" from __future__ import annotations import threading import time from pathlib import Path from typing import Any, Callable from .browser_confirm import mint_with_browser from .probe import probe_mini_response, probe_models from .protocol_mint import ProtocolMintError, extract_sso_from_cookies, mint_with_sso_protocol from .proxyutil import proxy_log_label, resolve_proxy, set_runtime_proxy from .schema import DEFAULT_BASE_URL, build_cpa_xai_auth from .writer import write_cpa_xai_auth LogFn = Callable[[str], None] # Serialize/spacing protocol mints to reduce accounts.x.ai rate_limited. _PROTOCOL_MINT_LOCK = threading.Lock() _PROTOCOL_LAST_OK_AT = 0.0 _PROTOCOL_MIN_INTERVAL_SEC = 1.5 def _noop(_: str) -> None: return None def mint_and_export( *, email: str, password: str, auth_dir: str | Path, page: Any | None = None, proxy: str | None = None, headless: bool = False, base_url: str = DEFAULT_BASE_URL, probe: bool = True, probe_chat: bool = False, browser_timeout_sec: float = 240.0, force_standalone: bool = True, cookies: Any | None = None, sso: str | None = None, reuse_browser: bool = True, recycle_every: int = 15, prefer_protocol: bool = True, protocol_only: bool = False, protocol_poll_timeout_sec: float = 90.0, log: LogFn | None = None, cancel: Callable[[], bool] | None = None, ) -> dict[str, Any]: """Full pipeline: (protocol SSO device-flow |) browser device-auth → write CPA → probe. Protocol path (curl_cffi + sso cookie) is tried first when prefer_protocol and an sso cookie is available. On failure, falls back to browser mint unless protocol_only=True. Returns dict with keys: ok, path, email, probe, error?, mint_method? """ log = log or _noop global _PROTOCOL_LAST_OK_AT email = (email or "").strip() if not email or not password: # Protocol can work with sso alone; password only required for browser fallback if not email: return {"ok": False, "email": email, "error": "missing email"} if not (sso or extract_sso_from_cookies(cookies)): return {"ok": False, "email": email, "error": "missing email/password"} # Config/explicit proxy wins over shell https_proxy (common 7890 trap). # Thread-local pin — safe under concurrent mint workers. resolved = resolve_proxy(proxy) set_runtime_proxy(resolved or None) log(f"mint start: {email} proxy={proxy_log_label(resolved) or '(none)'}") sso_val = (sso or "").strip() or extract_sso_from_cookies(cookies) tokens: dict[str, Any] | None = None protocol_err: str | None = None if prefer_protocol and sso_val: log("mint try protocol (SSO HTTP device flow)") max_protocol_attempts = 3 for attempt in range(1, max_protocol_attempts + 1): if cancel and cancel(): return {"ok": False, "email": email, "error": "cancelled", "mint_method": "protocol"} # Throttle protocol calls across mint workers with _PROTOCOL_MINT_LOCK: gap = time.time() - _PROTOCOL_LAST_OK_AT if _PROTOCOL_LAST_OK_AT > 0 and gap < _PROTOCOL_MIN_INTERVAL_SEC: wait = _PROTOCOL_MIN_INTERVAL_SEC - gap log(f"protocol throttle sleep {wait:.1f}s") time.sleep(wait) try: tokens = mint_with_sso_protocol( sso_cookie=sso_val, email=email, proxy=resolved or None, poll_timeout_sec=protocol_poll_timeout_sec, log=log, cancel=cancel, ) with _PROTOCOL_MINT_LOCK: _PROTOCOL_LAST_OK_AT = time.time() log("mint protocol SUCCESS") protocol_err = None break except ProtocolMintError as e: protocol_err = str(e) log(f"mint protocol failed (try {attempt}/{max_protocol_attempts}): {e}") rate_limited = "rate_limited" in protocol_err.lower() if rate_limited and attempt < max_protocol_attempts: backoff = 5.0 * attempt log(f"protocol rate_limited → backoff {backoff:.0f}s then retry") time.sleep(backoff) continue if protocol_only: return { "ok": False, "email": email, "error": f"protocol_only: {e}", "mint_method": "protocol", } log("mint fallback → browser") break except Exception as e: # noqa: BLE001 protocol_err = str(e) log(f"mint protocol exception (try {attempt}/{max_protocol_attempts}): {e}") if protocol_only: return { "ok": False, "email": email, "error": f"protocol_only: {e}", "mint_method": "protocol", } log("mint fallback → browser") break elif prefer_protocol and not sso_val: log("mint protocol skipped (no sso cookie) → browser") if protocol_only: return { "ok": False, "email": email, "error": "protocol_only but no sso cookie", "mint_method": "protocol", } elif not prefer_protocol: log("mint protocol disabled → browser") if tokens is None: if not password: return { "ok": False, "email": email, "error": protocol_err or "protocol failed and no password for browser fallback", "protocol_error": protocol_err, } try: tokens = mint_with_browser( email=email, password=password, page=None if force_standalone else page, proxy=resolved or None, headless=headless, browser_timeout_sec=browser_timeout_sec, force_standalone=force_standalone, cookies=cookies, reuse_browser=reuse_browser, recycle_every=recycle_every, poll_log=log, cancel=cancel, ) tokens["mint_method"] = "browser" if protocol_err: tokens["protocol_error"] = protocol_err except Exception as e: # noqa: BLE001 log(f"mint failed: {e}") err = str(e) if protocol_err: err = f"{err} (protocol: {protocol_err})" return { "ok": False, "email": email, "error": err, "protocol_error": protocol_err, } payload = build_cpa_xai_auth( email=email, access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], id_token=tokens.get("id_token"), expires_in=tokens.get("expires_in"), base_url=base_url, ) path = write_cpa_xai_auth(auth_dir, payload) log(f"wrote {path}") result: dict[str, Any] = { "ok": True, "email": email, "path": str(path), "user_code": tokens.get("user_code"), "base_url": base_url, "proxy": proxy_log_label(resolved), "mint_method": tokens.get("mint_method") or "browser", } if protocol_err and result["mint_method"] != "protocol": result["protocol_error"] = protocol_err if probe: pr = probe_models(tokens["access_token"], base_url=base_url, proxy=resolved or None) result["probe_models"] = pr log( f"probe models: ok={pr.get('ok')} status={pr.get('status')} " f"has_grok_45={pr.get('has_grok_45')} ids={pr.get('model_ids')} " f"error={str(pr.get('error') or '')[:200]}" ) if not pr.get("has_grok_45"): result["ok"] = False result["error"] = "token ok but grok-4.5 not listed" if probe_chat and pr.get("has_grok_45"): ch = probe_mini_response( tokens["access_token"], base_url=base_url, proxy=resolved or None ) result["probe_chat"] = ch log(f"probe chat: ok={ch.get('ok')} model={ch.get('model')} text={ch.get('text')!r}") if not ch.get("ok"): result["ok"] = False result["error"] = f"chat probe failed: {ch.get('error') or ch.get('status')}" return result