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:
@@ -168,21 +168,36 @@ edges stay deterministic Rust. The harness appends a terminal
|
||||
are built generically from advertised methods (`AuthMethodKind::
|
||||
ApiKeyPlatform`), so new registry rows appear in the picker with no TUI
|
||||
changes.
|
||||
- Refreshable-OAuth providers beyond Kimi Code use a GENERIC device-code
|
||||
(RFC-8628) path, NOT Kimi's bespoke wire. A `uses_oauth` platform carrying
|
||||
`oauth: Some(&OAuthConfig)` (client id / auth host / device+token paths /
|
||||
scope / `scope_key` / optional extra device field) drives
|
||||
`auth::oauth_device` (plain kigi UA, no X-Msh headers) + a scope-keyed
|
||||
`AuthManager::new_oauth_provider` + `refresh::GenericDeviceRefresher`
|
||||
(selected by `build_refresher` via `oauth_config_for_scope_key`). Kimi Code
|
||||
keeps `oauth: None` and its bespoke path unchanged. First such provider:
|
||||
`xai-grok` (`scope_key oauth/xai`, base `api.x.ai/v1`, same wire as the
|
||||
API-key `xai` row) — an INTERACTIVE login row advertised right after
|
||||
`kimi-code` (`AuthMethodKind::OAuthPlatform`). Its `authenticate` arm runs
|
||||
the generic device flow under its own scope; the catalog fetch resolves each
|
||||
such platform's OWN session token (`resolve_generic_oauth_tokens`, refreshed
|
||||
on expiry) and routes `platform.oauth().is_some()` → `platform.base_url()`
|
||||
(kimi-code alone → `proxy_url()`). Tokens are NEVER logged.
|
||||
- Refreshable-OAuth providers beyond Kimi Code use a GENERIC path, NOT Kimi's
|
||||
bespoke wire. A `uses_oauth` platform carrying `oauth: Some(&OAuthConfig)`
|
||||
(client id / auth host / start+token paths / `token_host` / `scope` /
|
||||
`scope_key` / optional extra device field / `flow` / `token_body`) drives a
|
||||
scope-keyed `AuthManager::new_oauth_provider` +
|
||||
`refresh::GenericDeviceRefresher` (selected by `build_refresher` via
|
||||
`oauth_config_for_scope_key`; the refresher dispatches the refresh body by
|
||||
`token_body`: form → `auth::oauth_device`, JSON → `auth::oauth_pkce`). Kimi
|
||||
Code keeps `oauth: None` and its bespoke path unchanged. The interactive
|
||||
login is dispatched by `OAuthConfig.flow` (in `run_oauth_provider_flow`):
|
||||
- `OAuthFlow::DeviceCode` → `auth::oauth_device` (RFC-8628 device-code, plain
|
||||
kigi UA, no X-Msh headers). Provider: `xai-grok` (`scope_key oauth/xai`,
|
||||
base `api.x.ai/v1`, form token body, same wire as the API-key `xai` row).
|
||||
- `OAuthFlow::PkceLocalhost { redirect_port }` → `auth::oauth_pkce`
|
||||
(authorization-code + PKCE S256, `127.0.0.1:redirect_port/callback` loopback
|
||||
with STRICT `state` validation + manual-paste fallback, JSON token body,
|
||||
authorize host ≠ token host). Provider: `claude-pro-max` (`scope_key
|
||||
oauth/claude-pro-max`, base `api.anthropic.com/v1`, Anthropic Messages +
|
||||
listing wire reached with an OAuth `sk-ant-oat…` Bearer). Its Messages
|
||||
requests take the OAuth adaptation — `anthropic-beta claude-code-…,oauth-…`
|
||||
+ `claude-cli` UA + `x-app cli` + the required "You are Claude Code…" system
|
||||
prefix — gated on `SamplerConfig.anthropic_oauth` (claude-pro-max only), so
|
||||
API-key `anthropic`/`minimax` Messages requests stay byte-identical. Its
|
||||
`/v1/models` listing rides the same Bearer + oauth-beta headers.
|
||||
Both are INTERACTIVE login rows advertised right after `kimi-code`
|
||||
(`AuthMethodKind::OAuthPlatform`, in `PlatformId::ALL` order: `xai-grok` then
|
||||
`claude-pro-max`). The catalog fetch resolves each such platform's OWN session
|
||||
token (`resolve_generic_oauth_tokens`, refreshed on expiry) and routes
|
||||
`platform.oauth().is_some()` → `platform.base_url()` (kimi-code alone →
|
||||
`proxy_url()`). Tokens/codes/verifiers are NEVER logged.
|
||||
- Model metadata (context window, thinking levels) comes from the provider
|
||||
wire when served; metadata-poor listings are enriched from models.dev
|
||||
(`kigi-models/src/enrichment.rs` — bundled raw snapshot regenerated by
|
||||
|
||||
Reference in New Issue
Block a user