Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib.parse import parse_qs, urlencode, urlparse
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse

import httpx
import yaml
Expand Down Expand Up @@ -4138,6 +4138,41 @@ def _codex_device_code_login() -> Dict[str, Any]:

# ==================== MiniMax Portal OAuth ====================

# MiniMax's OAuth code endpoint returns a verification_uri pointing at
# https://www.minimax.io/oauth-authorize?... but that page was retired and
# 307-redirects to the marketing homepage, leaving users stranded. The live
# approval UI is on https://platform.minimax.io. This rewrite is a defensive
# client-side workaround; it can be removed once MiniMax updates the server
# response. See issue #19337.
_MINIMAX_STALE_AUTHORIZE_HOST = "www.minimax.io"
_MINIMAX_LIVE_AUTHORIZE_HOST = "platform.minimax.io"


def _minimax_normalize_verification_uri(url: str) -> str:
"""Rewrite MiniMax's stale www.minimax.io/oauth-authorize host to the live
platform.minimax.io host. Returns the URL unchanged for any other host or
path.
"""
try:
parts = urlparse(url)
# parts.hostname / parts.port are properties that re-parse netloc and
# can raise ValueError on malformed authority components (e.g. an
# out-of-range port). Touch them inside the try so any failure falls
# through to returning the input unchanged.
host = parts.hostname
port = parts.port
except (ValueError, TypeError):
return url
if host == _MINIMAX_STALE_AUTHORIZE_HOST and parts.path.startswith(
"/oauth-authorize"
):
netloc = _MINIMAX_LIVE_AUTHORIZE_HOST
if port:
netloc = f"{netloc}:{port}"
return urlunparse(parts._replace(netloc=netloc))
return url


def _minimax_pkce_pair() -> tuple:
"""Generate (code_verifier, code_challenge_S256, state) for MiniMax OAuth."""
import secrets
Expand Down Expand Up @@ -4290,7 +4325,9 @@ def _minimax_oauth_login(
client_id=pconfig.client_id,
code_challenge=challenge, state=state,
)
verification_url = str(code_data["verification_uri"])
verification_url = _minimax_normalize_verification_uri(
str(code_data["verification_uri"])
)
user_code = str(code_data["user_code"])

print()
Expand Down
44 changes: 44 additions & 0 deletions tests/test_minimax_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
MINIMAX_OAUTH_CN_BASE,
MINIMAX_OAUTH_CN_INFERENCE,
MINIMAX_OAUTH_REFRESH_SKEW_SECONDS,
_minimax_normalize_verification_uri,
_minimax_pkce_pair,
_minimax_request_user_code,
_minimax_poll_token,
Expand Down Expand Up @@ -464,3 +465,46 @@ def test_get_minimax_oauth_auth_status_logged_in():

assert status["logged_in"] is True
assert status["region"] == "global"


# ---------------------------------------------------------------------------
# 16. test_normalize_verification_uri rewrites stale www.minimax.io host
# ---------------------------------------------------------------------------

def test_normalize_verification_uri_rewrites_stale_host():
# Real-world payload from issue #19337: server hands back www.minimax.io,
# which 307-redirects to the marketing homepage. Live UI is on
# platform.minimax.io.
stale = "https://www.minimax.io/oauth-authorize?user_code=ABCD&client=OpenClaw"
rewritten = _minimax_normalize_verification_uri(stale)

assert rewritten == (
"https://platform.minimax.io/oauth-authorize"
"?user_code=ABCD&client=OpenClaw"
)


def test_normalize_verification_uri_preserves_unrelated_urls():
# Other hosts pass through unchanged so we don't accidentally rewrite
# a corrected server response or an unrelated URL.
untouched = [
"https://platform.minimax.io/oauth-authorize?user_code=X",
"https://api.minimax.io/oauth/code",
"https://www.minimax.io/about", # different path, not authorize
"https://minimax.io/oauth-authorize?u=1", # apex host, not www
"https://www.minimaxi.com/oauth-authorize", # CN cousin, different brand
]
for url in untouched:
assert _minimax_normalize_verification_uri(url) == url


def test_normalize_verification_uri_handles_bad_input():
# Robust against odd inputs — empty, malformed, out-of-range port.
# A bad port causes urlparse().port to raise ValueError; the helper
# must catch that and return the original string unchanged.
assert _minimax_normalize_verification_uri("") == ""
assert _minimax_normalize_verification_uri("not a url") == "not a url"
assert (
_minimax_normalize_verification_uri("https://www.minimax.io:999999/oauth-authorize")
== "https://www.minimax.io:999999/oauth-authorize"
)
Loading