feat(providers): add Claude Pro/Max subscription OAuth (PKCE-localhost)

27th registry variant, 2nd subscription-OAuth provider. Log in with a Claude
Pro/Max subscription via PKCE authorization-code + S256 (loopback callback on
127.0.0.1:53692, with a manual code-paste fallback), then use it against
api.anthropic.com — reusing the existing Anthropic Messages wire + Anthropic
listing + the multi-provider OAuth foundation (dbce6bf). Sourced from Pi
(earendil-works/pi auth/oauth/anthropic.ts): client 9d1c250a..., authorize
claude.ai/oauth/authorize, token platform.claude.com/v1/oauth/token, scope
'…user:inference user:sessions:claude_code…'.

New machinery (foundation handles token routing — claude-pro-max is a
uses_oauth platform so its bearer/refresh/api_key already route to its own
pooled manager, never Kimi):
- OAuthConfig gains flow{DeviceCode|PkceLocalhost} + token_host + token_body
  {Form|JSON}; xai/kimi rows unchanged (DeviceCode/Form).
- auth/oauth_pkce.rs: PKCE S256 wire — loopback listener with STRICT state
  validation (CSRF, fail-closed), manual-paste fallback, JSON code→token
  exchange + rotating-refresh. Never logs code/verifier/tokens.
- Messages OAuth adaptation gated on SamplerConfig.anthropic_oauth (true only
  for a claude-pro-max managed key): Authorization: Bearer + anthropic-beta
  oauth + user-agent claude-cli + x-app cli, and the required 'You are Claude
  Code' system prefix. API-key anthropic/minimax Messages requests are
  BYTE-IDENTICAL (regression-guarded).
- Live /models under the OAuth Bearer + oauth-beta headers (Anthropic listing,
  enriched from models.dev anthropic); persistent 401 → 0 models + WARN, NO
  hardcoded fallback list (honest failure).

Adversarial review: no blocking findings (secret handling, CSRF/state, the
anthropic_oauth gate, token routing, non-regression all CONFIRMED). Full gate
green. Registry at 27; picker updated. Residual (unverifiable without a real
Claude Pro/Max account): whether GET /v1/models accepts the OAuth bearer, and
the real endpoint's acceptance of the OAuth Messages request.
This commit is contained in:
2026-07-22 02:53:25 -04:00
parent dbce6bf305
commit 5a9183b08b
25 changed files with 1527 additions and 73 deletions
@@ -595,6 +595,11 @@ mod tests {
);
assert_eq!(
ids[kimi_pos + 2],
"claude-pro-max",
"claude-pro-max is the next interactive OAuth login, after xai-grok"
);
assert_eq!(
ids[kimi_pos + 3],
MOONSHOT_CN_METHOD_ID,
"the api-key rows follow the generic oauth logins"
);
@@ -628,6 +633,29 @@ mod tests {
assert_eq!(kind.auth_error_message(), AUTH_ERROR_SESSION_EXPIRED);
}
/// claude-pro-max classifies as an interactive OAuth login too (the
/// authenticate handler dispatches it to the PKCE-localhost flow by the
/// config's `flow`): session-based, needs a browser, never api-key, and
/// `oauth_platform()` returns ClaudeProMax.
#[test]
fn claude_pro_max_is_an_interactive_oauth_login() {
let id = acp::AuthMethodId::new("claude-pro-max");
let kind = AuthMethodKind::from_id(&id);
assert_eq!(
kind,
AuthMethodKind::OAuthPlatform(kigi_models::PlatformId::ClaudeProMax)
);
assert!(kind.needs_interactive_login());
assert!(kind.is_session_based());
assert!(!kind.is_api_key());
assert_eq!(
kind.oauth_platform(),
Some(kigi_models::PlatformId::ClaudeProMax)
);
// Never an API-key picker target (keeps it out of the paste-box path).
assert_eq!(platform_for_method_id(&id), None);
}
/// The OAuth platform id must never resolve as an API-key platform
/// method — `platform_for_method_id`'s `uses_oauth` filter is what keeps
/// the generic `authenticate` arm from hijacking the device login.
@@ -721,6 +749,7 @@ mod tests {
XAI_API_KEY_METHOD_ID,
KIMI_CODE_METHOD_ID,
"xai-grok",
"claude-pro-max",
MOONSHOT_CN_METHOD_ID,
MOONSHOT_AI_METHOD_ID,
"openai",
@@ -770,6 +799,7 @@ mod tests {
CACHED_TOKEN_AUTH_METHOD_ID,
KIMI_CODE_METHOD_ID,
"xai-grok",
"claude-pro-max",
MOONSHOT_CN_METHOD_ID,
MOONSHOT_AI_METHOD_ID,
"openai",
@@ -812,6 +842,7 @@ mod tests {
CACHED_TOKEN_AUTH_METHOD_ID,
KIMI_CODE_METHOD_ID,
"xai-grok",
"claude-pro-max",
MOONSHOT_CN_METHOD_ID,
MOONSHOT_AI_METHOD_ID,
"openai",
@@ -857,6 +888,7 @@ mod tests {
vec![
KIMI_CODE_METHOD_ID,
"xai-grok",
"claude-pro-max",
MOONSHOT_CN_METHOD_ID,
MOONSHOT_AI_METHOD_ID,
"openai",