Replace repository with grok-reg-protocol-cpa project

Overwrite previous grok-keygen contents with the local registration/CPA protocol codebase (secrets and runtime data excluded).
This commit is contained in:
chaos committed 2026-07-11 17:25:37 +08:00
commit 6f7c949681
33 files changed
+11125

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
"""CPA xAI (Grok Build free) auth helpers for the register machine.
Produce CLIProxyAPI-compatible ``xai-<email>.json`` credentials.
"""
from .accounts import AccountLine, existing_cpa_emails, parse_accounts_file
from .mint import mint_and_export
from .probe import probe_mini_response, probe_models
from .protocol_mint import (
ProtocolMintError,
extract_sso_from_cookies,
mint_with_sso_protocol,
)
from .schema import (
CLIENT_ID,
DEFAULT_BASE_URL,
DEFAULT_CLIENT_HEADERS,
DEFAULT_REDIRECT_URI,
DEFAULT_TOKEN_ENDPOINT,
build_cpa_xai_auth,
credential_file_name,
expired_from_access_token,
)
from .writer import write_cpa_xai_auth
# CLIENT_ID lives in oauth_device; re-export from schema if present
try:
from .oauth_device import CLIENT_ID as OAUTH_CLIENT_ID
except Exception: # pragma: no cover
OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"
__all__ = [
"AccountLine",
"CLIENT_ID",
"DEFAULT_BASE_URL",
"DEFAULT_CLIENT_HEADERS",
"DEFAULT_REDIRECT_URI",
"DEFAULT_TOKEN_ENDPOINT",
"OAUTH_CLIENT_ID",
"ProtocolMintError",
"build_cpa_xai_auth",
"credential_file_name",
"existing_cpa_emails",
"expired_from_access_token",
"extract_sso_from_cookies",
"mint_and_export",
"mint_with_sso_protocol",
"parse_accounts_file",
"probe_mini_response",
"probe_models",
"write_cpa_xai_auth",
]
+58
View File
@@ -0,0 +1,58 @@
"""Parse register machine accounts_cli.txt lines."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
@dataclass
class AccountLine:
email: str
password: str
sso: str
raw: str
line_no: int
def parse_accounts_file(path: str | Path) -> list[AccountLine]:
path = Path(path)
out: list[AccountLine] = []
if not path.is_file():
return out
for i, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
s = line.strip()
if not s or s.startswith("#"):
continue
parts = s.split("----")
if len(parts) < 2:
continue
email = parts[0].strip()
password = parts[1].strip()
sso = parts[2].strip() if len(parts) > 2 else ""
if not email or not password:
continue
out.append(AccountLine(email=email, password=password, sso=sso, raw=s, line_no=i))
return out
def existing_cpa_emails(auth_dir: str | Path) -> set[str]:
"""Emails already present as xai-*.json in auth_dir."""
auth_dir = Path(auth_dir)
found: set[str] = set()
if not auth_dir.is_dir():
return found
for p in auth_dir.glob("xai-*.json"):
name = p.name[len("xai-") : -len(".json")]
if name:
found.add(name.lower())
try:
import json
d = json.loads(p.read_text(encoding="utf-8"))
em = str(d.get("email") or "").strip().lower()
if em:
found.add(em)
except Exception:
continue
return found
File diff suppressed because it is too large. Load diff
+199
View File
@@ -0,0 +1,199 @@
"""High-level: mint CPA xai-*.json for one free registered account."""
from __future__ import annotations
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]
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
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)")
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,
)
log("mint protocol SUCCESS")
except ProtocolMintError as e:
protocol_err = str(e)
log(f"mint protocol failed: {e}")
if protocol_only:
return {
"ok": False,
"email": email,
"error": f"protocol_only: {e}",
"mint_method": "protocol",
}
log("mint fallback → browser")
except Exception as e: # noqa: BLE001
protocol_err = str(e)
log(f"mint protocol exception: {e}")
if protocol_only:
return {
"ok": False,
"email": email,
"error": f"protocol_only: {e}",
"mint_method": "protocol",
}
log("mint fallback → browser")
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
+314
View File
@@ -0,0 +1,314 @@
"""xAI OAuth device-code grant (Grok CLI / CPA client).
Endpoints from https://auth.x.ai/.well-known/openid-configuration
"""
from __future__ import annotations
import json
import ssl
import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any, Callable
from .proxyutil import resolve_proxy
# Keep in sync with CLIProxyAPI internal/auth/xai/types.go
CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"
ISSUER = "https://auth.x.ai"
DEVICE_CODE_URL = "https://auth.x.ai/oauth2/device/code"
TOKEN_URL = "https://auth.x.ai/oauth2/token"
SCOPE = "openid profile email offline_access grok-cli:access api:access"
LogFn = Callable[[str], None]
def _noop_log(_: str) -> None:
return None
def _proxy_handler(proxy: str | None = None) -> urllib.request.ProxyHandler | None:
p = resolve_proxy(proxy)
if not p:
return None
return urllib.request.ProxyHandler({"http": p, "https": p})
def _ssl_context() -> ssl.SSLContext | None:
"""Use certifi's CA bundle when available.
macOS Framework Python / uv venv can have an empty or incomplete OpenSSL
trust store, which makes stdlib urllib fail with
CERTIFICATE_VERIFY_FAILED even when curl/browser requests work.
"""
try:
import certifi # type: ignore
return ssl.create_default_context(cafile=certifi.where())
except Exception:
return None
def _opener(proxy: str | None = None) -> urllib.request.OpenerDirector:
handlers: list[Any] = []
ctx = _ssl_context()
if ctx is not None:
handlers.append(urllib.request.HTTPSHandler(context=ctx))
ph = _proxy_handler(proxy)
if ph is not None:
handlers.append(ph)
return urllib.request.build_opener(*handlers) if handlers else urllib.request.build_opener()
def _is_transient_net_error(exc: BaseException) -> bool:
"""Proxy/TLS blips that should not kill an already-approved device flow."""
if isinstance(exc, (TimeoutError, BrokenPipeError, ConnectionResetError, ConnectionAbortedError, ConnectionRefusedError)):
return True
if isinstance(exc, urllib.error.URLError):
reason = getattr(exc, "reason", None)
if isinstance(reason, BaseException) and _is_transient_net_error(reason):
return True
msg = str(exc).lower()
needles = (
"broken pipe",
"connection reset",
"connection aborted",
"timed out",
"timeout",
"temporarily unavailable",
"network is unreachable",
"name or service not known",
"unexpected_eof",
"eof occurred",
"ssl",
"handshake",
"remote end closed",
"bad gateway",
"connection refused",
)
return any(n in msg for n in needles)
# ssl.SSLError and generic OSError (errno 32 Broken pipe, 104 reset, etc.)
try:
import ssl
if isinstance(exc, ssl.SSLError):
return True
except Exception:
pass
if isinstance(exc, OSError):
if getattr(exc, "errno", None) in {32, 104, 110, 111, 113, 101}:
return True
msg = str(exc).lower()
return any(n in msg for n in ("broken pipe", "timed out", "connection reset", "ssl"))
return False
def _post_form(
url: str,
form: dict[str, str],
timeout: float = 30.0,
*,
proxy: str | None = None,
retries: int = 0,
retry_sleep: float = 1.5,
) -> tuple[int, dict[str, Any] | str]:
data = urllib.parse.urlencode(form).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
method="POST",
headers={
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
"User-Agent": "grok-reg-cpa-xai-minter/1.0",
},
)
last: BaseException | None = None
attempts = max(int(retries), 0) + 1
for i in range(attempts):
opener = _opener(proxy)
try:
with opener.open(req, timeout=timeout) as resp:
body = resp.read().decode("utf-8", errors="replace")
status = getattr(resp, "status", 200) or 200
try:
return int(status), json.loads(body)
except json.JSONDecodeError:
return int(status), body
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
try:
return int(e.code), json.loads(body)
except json.JSONDecodeError:
return int(e.code), body
except BaseException as e: # noqa: BLE001
last = e
if not _is_transient_net_error(e) or i + 1 >= attempts:
raise
time.sleep(retry_sleep * (i + 1))
assert last is not None
raise last
@dataclass
class DeviceCodeSession:
device_code: str
user_code: str
verification_uri: str
verification_uri_complete: str
expires_in: int
interval: int
raw: dict[str, Any]
@dataclass
class TokenResult:
access_token: str
refresh_token: str
id_token: str | None
token_type: str
expires_in: int
raw: dict[str, Any]
class OAuthDeviceError(RuntimeError):
pass
def request_device_code(
*,
client_id: str = CLIENT_ID,
scope: str = SCOPE,
timeout: float = 30.0,
proxy: str | None = None,
) -> DeviceCodeSession:
status, body = _post_form(
DEVICE_CODE_URL,
{"client_id": client_id, "scope": scope},
timeout=timeout,
proxy=proxy,
retries=2,
retry_sleep=1.0,
)
if status != 200 or not isinstance(body, dict):
raise OAuthDeviceError(f"device code request failed HTTP {status}: {body!r}")
device_code = str(body.get("device_code") or "").strip()
user_code = str(body.get("user_code") or "").strip()
if not device_code or not user_code:
raise OAuthDeviceError(f"device code response missing fields: {body}")
vuri = str(body.get("verification_uri") or "https://accounts.x.ai/oauth2/device").strip()
vcomplete = str(
body.get("verification_uri_complete") or f"{vuri}?user_code={user_code}"
).strip()
expires_in = int(body.get("expires_in") or 1800)
interval = max(int(body.get("interval") or 5), 1)
return DeviceCodeSession(
device_code=device_code,
user_code=user_code,
verification_uri=vuri,
verification_uri_complete=vcomplete,
expires_in=expires_in,
interval=interval,
raw=body,
)
def poll_device_token(
device_code: str,
*,
client_id: str = CLIENT_ID,
interval: int = 5,
expires_in: int = 1800,
timeout: float = 30.0,
log: LogFn | None = None,
cancel: Callable[[], bool] | None = None,
proxy: str | None = None,
) -> TokenResult:
"""Poll token endpoint until authorized or expired.
Transient proxy/TLS errors (Broken pipe, SSL EOF, timeouts) are retried
until the device-code deadline so a successful browser consent is not
wasted by a single flaky poll.
"""
log = log or _noop_log
deadline = time.time() + max(expires_in - 5, 30)
sleep_for = max(interval, 1)
net_streak = 0
max_net_streak = 20
while time.time() < deadline:
if cancel and cancel():
raise OAuthDeviceError("cancelled")
try:
status, body = _post_form(
TOKEN_URL,
{
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_code,
"client_id": client_id,
},
timeout=timeout,
proxy=proxy,
retries=2,
retry_sleep=1.0,
)
net_streak = 0
except BaseException as e: # noqa: BLE001
if not _is_transient_net_error(e):
raise
net_streak += 1
wait = min(sleep_for + min(net_streak, 5), 20)
log(
f"oauth poll network blip ({net_streak}/{max_net_streak}): {e} "
f"— retry in {wait}s"
)
if net_streak >= max_net_streak:
raise OAuthDeviceError(
f"device auth aborted after {net_streak} network errors: {e}"
) from e
time.sleep(wait)
continue
if status == 200 and isinstance(body, dict) and body.get("access_token"):
access = str(body["access_token"]).strip()
refresh = str(body.get("refresh_token") or "").strip()
if not refresh:
raise OAuthDeviceError("token response missing refresh_token")
return TokenResult(
access_token=access,
refresh_token=refresh,
id_token=(str(body["id_token"]).strip() if body.get("id_token") else None),
token_type=str(body.get("token_type") or "Bearer"),
expires_in=int(body.get("expires_in") or 21600),
raw=body,
)
err = ""
desc = ""
if isinstance(body, dict):
err = str(body.get("error") or "")
desc = str(body.get("error_description") or "")
if err in ("authorization_pending", "slow_down"):
if err == "slow_down":
sleep_for = min(sleep_for + 5, 30)
log(f"oauth poll: {err} (sleep {sleep_for}s)")
time.sleep(sleep_for)
continue
if err in ("expired_token", "access_denied"):
raise OAuthDeviceError(f"device auth failed: {err}: {desc}")
if status == 400 and err:
raise OAuthDeviceError(f"device auth token error: {err}: {desc or body}")
# 5xx / empty / proxy HTML — treat as soft error and keep polling
if status >= 500 or status in (502, 503, 504) or not isinstance(body, dict):
net_streak += 1
wait = min(sleep_for + 2, 20)
log(f"oauth poll soft HTTP {status}: {body!r} — retry in {wait}s")
if net_streak >= max_net_streak:
raise OAuthDeviceError(
f"device auth aborted after soft HTTP failures status={status}"
)
time.sleep(wait)
continue
log(f"oauth poll unexpected HTTP {status}: {body!r}")
time.sleep(sleep_for)
raise OAuthDeviceError("device auth timed out waiting for user approval")
+130
View File
@@ -0,0 +1,130 @@
"""Probe free Grok 4.5 via cli-chat-proxy with a CPA access_token."""
from __future__ import annotations
import json
import ssl
import urllib.error
import urllib.request
from typing import Any
from .proxyutil import resolve_proxy
from .schema import DEFAULT_BASE_URL, DEFAULT_CLIENT_HEADERS
def _ssl_context() -> ssl.SSLContext | None:
try:
import certifi # type: ignore
return ssl.create_default_context(cafile=certifi.where())
except Exception:
return None
def _opener(proxy: str | None = None) -> urllib.request.OpenerDirector:
p = resolve_proxy(proxy)
handlers: list[Any] = []
ctx = _ssl_context()
if ctx is not None:
handlers.append(urllib.request.HTTPSHandler(context=ctx))
if p:
handlers.append(urllib.request.ProxyHandler({"http": p, "https": p}))
return urllib.request.build_opener(*handlers) if handlers else urllib.request.build_opener()
def probe_models(
access_token: str,
*,
base_url: str = DEFAULT_BASE_URL,
timeout: float = 30.0,
proxy: str | None = None,
) -> dict[str, Any]:
base = base_url.rstrip("/")
url = f"{base}/models"
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json",
**DEFAULT_CLIENT_HEADERS,
}
opener = _opener(proxy)
req = urllib.request.Request(url, headers=headers, method="GET")
try:
with opener.open(req, timeout=timeout) as resp:
body = json.loads(resp.read().decode("utf-8"))
ids = [x.get("id") for x in body.get("data") or [] if isinstance(x, dict)]
return {
"ok": True,
"status": getattr(resp, "status", 200),
"model_ids": ids,
"has_grok_45": any(i == "grok-4.5" for i in ids),
}
except urllib.error.HTTPError as e:
return {
"ok": False,
"status": e.code,
"error": e.read().decode("utf-8", errors="replace")[:500],
"model_ids": [],
"has_grok_45": False,
}
except Exception as e: # noqa: BLE001
return {
"ok": False,
"status": 0,
"error": str(e),
"model_ids": [],
"has_grok_45": False,
}
def probe_mini_response(
access_token: str,
*,
base_url: str = DEFAULT_BASE_URL,
timeout: float = 60.0,
proxy: str | None = None,
) -> dict[str, Any]:
base = base_url.rstrip("/")
url = f"{base}/responses"
payload = {
"model": "grok-4.5",
"stream": False,
"input": "Reply with exactly MINT_OK",
"reasoning": {"effort": "low"},
}
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"Accept": "application/json",
**DEFAULT_CLIENT_HEADERS,
}
opener = _opener(proxy)
req = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers=headers,
method="POST",
)
try:
with opener.open(req, timeout=timeout) as resp:
body = json.loads(resp.read().decode("utf-8"))
texts: list[str] = []
for item in body.get("output") or []:
if item.get("type") == "message":
for c in item.get("content") or []:
if c.get("type") == "output_text":
texts.append(c.get("text") or "")
return {
"ok": True,
"status": getattr(resp, "status", 200),
"model": body.get("model"),
"text": "\n".join(texts),
"usage": body.get("usage"),
}
except urllib.error.HTTPError as e:
return {
"ok": False,
"status": e.code,
"error": e.read().decode("utf-8", errors="replace")[:800],
}
except Exception as e: # noqa: BLE001
return {"ok": False, "status": 0, "error": str(e)}
+281
View File
@@ -0,0 +1,281 @@
"""Pure-HTTP SSO cookie → OIDC tokens (device flow, no browser).
Uses curl_cffi (Chrome TLS fingerprint) + SSO cookie to:
1. Validate session on accounts.x.ai
2. Request device code (stdlib / oauth_device)
3. GET verification_uri_complete
4. POST /oauth2/device/verify
5. POST /oauth2/device/approve
6. Poll token endpoint
On any failure raise ProtocolMintError so callers can fall back to browser mint.
"""
from __future__ import annotations
from typing import Any, Callable
from urllib.parse import urlparse
from .oauth_device import (
CLIENT_ID,
ISSUER,
OAuthDeviceError,
SCOPE,
poll_device_token,
request_device_code,
)
from .proxyutil import proxy_log_label, resolve_proxy, set_runtime_proxy
LogFn = Callable[[str], None]
VERIFY_URL = f"{ISSUER}/oauth2/device/verify"
APPROVE_URL = f"{ISSUER}/oauth2/device/approve"
class ProtocolMintError(RuntimeError):
"""Protocol path failed; caller may fall back to browser mint."""
def _noop_log(_: str) -> None:
return None
def extract_sso_from_cookies(cookies: Any) -> str:
"""Pull sso / sso-rw value from a cookie list/dict."""
if not cookies:
return ""
if isinstance(cookies, str):
return cookies.strip()
if isinstance(cookies, dict):
for name in ("sso", "sso-rw"):
v = cookies.get(name)
if v:
return str(v).strip()
return ""
if isinstance(cookies, (list, tuple)):
# Prefer bare "sso" over "sso-rw"
found_rw = ""
for c in cookies:
if not isinstance(c, dict):
continue
name = str(c.get("name") or c.get("Name") or "")
value = c.get("value") if "value" in c else c.get("Value")
if not value:
continue
if name == "sso":
return str(value).strip()
if name == "sso-rw" and not found_rw:
found_rw = str(value).strip()
return found_rw
return ""
def _session(proxy: str | None, log: LogFn):
try:
from curl_cffi import requests as cf_requests
except ImportError as e:
raise ProtocolMintError(
"curl_cffi not installed; cannot run protocol mint"
) from e
s = cf_requests.Session()
resolved = resolve_proxy(proxy)
if resolved:
s.proxies = {"http": resolved, "https": resolved}
log(f"protocol proxy={proxy_log_label(resolved)}")
return s
def _set_sso_cookie(session: Any, sso_cookie: str) -> None:
sso_cookie = (sso_cookie or "").strip()
if not sso_cookie:
raise ProtocolMintError("empty sso cookie")
for domain in (".x.ai", "accounts.x.ai", "auth.x.ai", ".accounts.x.ai"):
try:
session.cookies.set("sso", sso_cookie, domain=domain)
except Exception:
try:
session.cookies.set("sso", sso_cookie, domain=domain, path="/")
except Exception:
pass
try:
session.cookies.set("sso-rw", sso_cookie, domain=domain)
except Exception:
pass
def _url_path(url: str) -> str:
try:
return urlparse(url or "").path or ""
except Exception:
return url or ""
def mint_with_sso_protocol(
*,
sso_cookie: str,
email: str = "",
proxy: str | None = None,
timeout: float = 30.0,
poll_timeout_sec: float = 90.0,
log: LogFn | None = None,
cancel: Callable[[], bool] | None = None,
) -> dict[str, Any]:
"""SSO cookie → OIDC token dict (access/refresh/id/expires_in).
Raises ProtocolMintError on failure.
"""
log = log or _noop_log
sso_cookie = (sso_cookie or "").strip()
if not sso_cookie:
raise ProtocolMintError("missing sso cookie")
resolved = resolve_proxy(proxy)
set_runtime_proxy(resolved or None)
session = _session(resolved or None, log)
_set_sso_cookie(session, sso_cookie)
# 1) Validate SSO
try:
r = session.get(
"https://accounts.x.ai/",
impersonate="chrome",
timeout=timeout,
allow_redirects=True,
)
except Exception as e: # noqa: BLE001
raise ProtocolMintError(f"accounts.x.ai network error: {e}") from e
final_url = getattr(r, "url", "") or ""
if "sign-in" in final_url or "sign-up" in final_url:
raise ProtocolMintError(f"sso invalid (landed {final_url[:120]})")
log(f"protocol sso valid url={final_url[:120]}")
# 2) Device code
try:
sess = request_device_code(proxy=resolved or None, timeout=timeout)
except OAuthDeviceError as e:
raise ProtocolMintError(f"device code: {e}") from e
except Exception as e: # noqa: BLE001
raise ProtocolMintError(f"device code: {e}") from e
log(f"protocol user_code={sess.user_code}")
if cancel and cancel():
raise ProtocolMintError("cancelled")
# 3) Open verification URI (sets session state / CSRF)
try:
r = session.get(
sess.verification_uri_complete,
impersonate="chrome",
timeout=timeout,
allow_redirects=True,
)
log(f"protocol verify-uri status={getattr(r, 'status_code', '?')} url={getattr(r, 'url', '')[:140]}")
except Exception as e: # noqa: BLE001
raise ProtocolMintError(f"verification_uri get failed: {e}") from e
if cancel and cancel():
raise ProtocolMintError("cancelled")
# 4) POST device/verify
try:
r = session.post(
VERIFY_URL,
data={"user_code": sess.user_code},
headers={"Content-Type": "application/x-www-form-urlencoded"},
impersonate="chrome",
timeout=timeout,
allow_redirects=True,
)
except Exception as e: # noqa: BLE001
raise ProtocolMintError(f"device/verify exception: {e}") from e
verify_url = getattr(r, "url", "") or ""
status = getattr(r, "status_code", 0)
path = _url_path(verify_url)
body_snip = ""
try:
body_snip = (r.text or "")[:200]
except Exception:
pass
if "consent" not in verify_url and "consent" not in path:
# Some flows land on consent via relative redirect body; accept soft signals
soft_ok = (
"consent" in (body_snip or "").lower()
or "authorize grok build" in (body_snip or "").lower()
or "授权 grok build" in (body_snip or "").lower()
)
if not soft_ok:
raise ProtocolMintError(
f"device/verify failed status={status} url={verify_url[:160]}"
)
log(f"protocol verify ok status={status} url={verify_url[:140]}")
if cancel and cancel():
raise ProtocolMintError("cancelled")
# 5) POST device/approve
try:
r = session.post(
APPROVE_URL,
data={
"user_code": sess.user_code,
"action": "allow",
"principal_type": "User",
"principal_id": "",
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
impersonate="chrome",
timeout=timeout,
allow_redirects=True,
)
except Exception as e: # noqa: BLE001
raise ProtocolMintError(f"device/approve exception: {e}") from e
approve_url = getattr(r, "url", "") or ""
status = getattr(r, "status_code", 0)
if "done" not in approve_url and "device/done" not in _url_path(approve_url):
# Accept success page markers in body
try:
text = r.text or ""
except Exception:
text = ""
if "设备已授权" not in text and "device authorized" not in text.lower() and "done" not in text.lower():
raise ProtocolMintError(
f"device/approve failed status={status} url={approve_url[:160]}"
)
log(f"protocol approve ok status={status} url={approve_url[:140]}")
# 6) Poll tokens
poll_expires = min(int(sess.expires_in), max(int(poll_timeout_sec), 30))
try:
tr = poll_device_token(
sess.device_code,
interval=max(int(sess.interval), 2),
expires_in=poll_expires,
timeout=timeout,
log=log,
cancel=cancel,
proxy=resolved or None,
)
except OAuthDeviceError as e:
raise ProtocolMintError(f"token poll: {e}") from e
except Exception as e: # noqa: BLE001
raise ProtocolMintError(f"token poll: {e}") from e
log(
f"protocol token ok expires_in={tr.expires_in}"
+ (f" email={email}" if email else "")
)
return {
"access_token": tr.access_token,
"refresh_token": tr.refresh_token,
"id_token": tr.id_token,
"token_type": tr.token_type,
"expires_in": tr.expires_in,
"user_code": sess.user_code,
"mint_method": "protocol",
}
+71
View File
@@ -0,0 +1,71 @@
"""Resolve outbound proxy for CPA mint HTTP + browser.
Priority (highest first):
1. explicit argument
2. thread-local runtime pin (set_runtime_proxy)
3. environment https_proxy / HTTPS_PROXY / http_proxy / HTTP_PROXY
Thread-local pin avoids cross-talk when multiple mint workers run with
different proxies in the same process.
"""
from __future__ import annotations
import os
import threading
from urllib.parse import urlparse
_thread = threading.local()
def set_runtime_proxy(proxy: str | None) -> None:
"""Pin proxy for the *current thread*. Empty clears pin."""
p = (proxy or "").strip()
_thread.proxy = p or None
def get_runtime_proxy() -> str | None:
return getattr(_thread, "proxy", None)
def resolve_proxy(explicit: str | None = None) -> str:
for cand in (
(explicit or "").strip(),
(get_runtime_proxy() or "").strip(),
(os.environ.get("https_proxy") or "").strip(),
(os.environ.get("HTTPS_PROXY") or "").strip(),
(os.environ.get("http_proxy") or "").strip(),
(os.environ.get("HTTP_PROXY") or "").strip(),
):
if cand:
return cand
return ""
def proxy_for_chromium(proxy: str) -> str:
"""Chromium --proxy-server cannot embed user:pass; host:port only."""
p = (proxy or "").strip()
if not p:
return ""
u = urlparse(p if "://" in p else f"http://{p}")
host = u.hostname or ""
if not host:
return ""
port = u.port or (443 if (u.scheme or "http") == "https" else 80)
scheme = u.scheme or "http"
return f"{scheme}://{host}:{port}"
def proxy_log_label(proxy: str) -> str:
"""Redact userinfo for logs."""
p = (proxy or "").strip()
if not p:
return ""
try:
u = urlparse(p if "://" in p else f"http://{p}")
host = u.hostname or "?"
port = u.port or ""
auth = "user:***@" if u.username else ""
return f"{u.scheme or 'http'}://{auth}{host}{(':' + str(port)) if port else ''}"
except Exception:
return "(proxy)"
+146
View File
@@ -0,0 +1,146 @@
"""CPA xAI auth JSON schema aligned with CLIProxyAPI internal/auth/xai."""
from __future__ import annotations
import base64
import json
import re
from datetime import datetime, timezone
from typing import Any
# Must match CLIProxyAPI internal/auth/xai/types.go
CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828"
ISSUER = "https://auth.x.ai"
DEFAULT_TOKEN_ENDPOINT = "https://auth.x.ai/oauth2/token"
DEFAULT_REDIRECT_URI = "http://127.0.0.1:56121/callback"
# Free Build promo path (NOT api.x.ai)
DEFAULT_BASE_URL = "https://cli-chat-proxy.grok.com/v1"
DEFAULT_CLIENT_HEADERS: dict[str, str] = {
"x-grok-client-version": "0.2.93",
"x-xai-token-auth": "xai-grok-cli",
"x-authenticateresponse": "authenticate-response",
"x-grok-client-identifier": "grok-shell",
"User-Agent": "grok-shell/0.2.93 (linux; x86_64)",
}
def _sanitize_file_segment(value: str) -> str:
"""Mirror CPA CredentialFileName sanitize rules."""
value = (value or "").strip()
if not value:
return ""
out: list[str] = []
for ch in value:
if (
("a" <= ch <= "z")
or ("A" <= ch <= "Z")
or ("0" <= ch <= "9")
or ch in {"@", ".", "_", "-"}
):
out.append(ch)
else:
out.append("-")
return "".join(out).strip("-")
def credential_file_name(email: str = "", sub: str = "") -> str:
"""Return CPA auth filename: xai-<email>.json."""
email_s = _sanitize_file_segment(email)
if email_s:
return f"xai-{email_s}.json"
sub_s = _sanitize_file_segment(sub)
if sub_s:
return f"xai-{sub_s}.json"
ts = int(datetime.now(tz=timezone.utc).timestamp() * 1000)
return f"xai-{ts}.json"
def jwt_payload(token: str) -> dict[str, Any]:
parts = token.split(".")
if len(parts) < 2:
raise ValueError("not a JWT")
pad = "=" * (-len(parts[1]) % 4)
return json.loads(base64.urlsafe_b64decode(parts[1] + pad))
def expired_from_access_token(access_token: str) -> tuple[str, int, str]:
"""Return (expired_rfc3339_z, expires_in, sub)."""
pl = jwt_payload(access_token)
exp = int(pl["exp"])
iat = int(pl["iat"]) if pl.get("iat") is not None else exp - 21600
expired = datetime.fromtimestamp(exp, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
sub = str(pl.get("sub") or pl.get("principal_id") or "").strip()
return expired, max(exp - iat, 0), sub
def build_cpa_xai_auth(
*,
email: str,
access_token: str,
refresh_token: str,
sub: str | None = None,
id_token: str | None = None,
expires_in: int | None = None,
expired: str | None = None,
last_refresh: str | None = None,
base_url: str = DEFAULT_BASE_URL,
token_endpoint: str = DEFAULT_TOKEN_ENDPOINT,
redirect_uri: str = DEFAULT_REDIRECT_URI,
headers: dict[str, str] | None = None,
disabled: bool = False,
extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build a CPA-importable xAI OAuth auth object."""
access_token = (access_token or "").strip()
refresh_token = (refresh_token or "").strip()
if not access_token:
raise ValueError("access_token is required")
if not refresh_token:
raise ValueError("refresh_token is required (CPA cannot renew without it)")
try:
exp_s, exp_in, sub_jwt = expired_from_access_token(access_token)
except Exception:
exp_s, exp_in, sub_jwt = "", 21600, ""
if not expired:
expired = exp_s
if expires_in is None:
expires_in = exp_in or 21600
if not sub:
sub = sub_jwt
if not last_refresh:
last_refresh = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# Free promo must hit cli-chat-proxy; refuse silent api.x.ai default for free path.
base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
if not re.search(r"/v1$", base_url):
# CPA joins baseURL + "/responses"
if base_url.endswith("cli-chat-proxy.grok.com"):
base_url = base_url + "/v1"
payload: dict[str, Any] = {
"type": "xai",
"auth_kind": "oauth",
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "Bearer",
"expires_in": int(expires_in),
"expired": expired,
"last_refresh": last_refresh,
"email": (email or "").strip(),
"sub": (sub or "").strip(),
"base_url": base_url,
"token_endpoint": token_endpoint,
"redirect_uri": redirect_uri,
"disabled": bool(disabled),
"headers": dict(headers or DEFAULT_CLIENT_HEADERS),
}
if id_token:
payload["id_token"] = id_token.strip()
if extra:
for k, v in extra.items():
if k not in payload:
payload[k] = v
return payload
+50
View File
@@ -0,0 +1,50 @@
"""Atomic write of CPA xAI auth files (mode 0600)."""
from __future__ import annotations
import json
import os
import tempfile
from pathlib import Path
from typing import Any
from .schema import credential_file_name
def write_cpa_xai_auth(
auth_dir: str | Path,
payload: dict[str, Any],
*,
filename: str | None = None,
) -> Path:
"""Write payload to auth_dir/xai-<email>.json atomically. Returns final path."""
auth_dir = Path(auth_dir).expanduser().resolve()
auth_dir.mkdir(parents=True, exist_ok=True)
if not filename:
filename = credential_file_name(
str(payload.get("email") or ""),
str(payload.get("sub") or ""),
)
if not filename.endswith(".json"):
filename = filename + ".json"
dest = auth_dir / filename
data = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
fd, tmp_name = tempfile.mkstemp(prefix=".xai-", suffix=".tmp", dir=str(auth_dir))
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(data)
f.flush()
os.fsync(f.fileno())
os.chmod(tmp_name, 0o600)
os.replace(tmp_name, dest)
os.chmod(dest, 0o600)
finally:
if os.path.exists(tmp_name):
try:
os.unlink(tmp_name)
except OSError:
pass
return dest