M1/F1: Kimi Code OAuth device-code flow
Replace the xAI OAuth stack with the Kimi device authorization grant:
- kimi_oauth.rs wire layer (device_authorization + token poll + refresh
against kigi_env::oauth_host(); client_id per PRD; retryable statuses
429/5xx with backoff; expired_token restarts authorization)
- X-Msh-Device-{Name,Model,Id} headers; device_id minted uuid4-hex at
~/.kigi/device_id (0600)
- Storage: system keyring service `kigi`, entry `oauth/kimi-code`
(macOS/Windows native backends), atomic-file fallback under ~/.kigi;
official client's keyring/~/.kimi never touched
- Refresh manager: 60s tick, threshold max(300, expires_in*0.5),
401-tombstone keyed by rejected refresh token with 300s cooldown and
rotation auto-clear, cross-process lock with sibling-adoption
triple-check, sleep/wake forced refresh
- Deleted xAI machinery: enterprise OIDC (PKCE/JWKS/teams), devbox login,
external auth provider, JWT tier gating + subscription paywall stack,
X-XAI-Token-Auth marker headers, ZDR gates, /user enrichment
- kigi login / TUI /login both drive the device flow; login-host display
now derives from kigi_env::oauth_host()
- 264 auth unit/wiremock tests; live contract probe of
auth.kimi.com/api/oauth/device_authorization matches the wire shapes
Gates: check/clippy --all-targets clean, fmt, deny ok, kigi-shell lib
5131 tests green.
This commit is contained in:
1 parent
d6c20fc13f
commit
021b82443d
117 files changed
+4052
-19900
No files matched your search
@@ -320,14 +320,14 @@ pub struct FeedbackClient {
|
||||
http: reqwest::Client,
|
||||
client: reqwest_middleware::ClientWithMiddleware,
|
||||
base_url: String,
|
||||
credentials: crate::util::grok_auth_credentials::GrokAuthCredentials,
|
||||
credentials: crate::util::kigi_auth_credentials::KigiAuthCredentials,
|
||||
session_id: Option<String>,
|
||||
}
|
||||
|
||||
impl FeedbackClient {
|
||||
pub fn new(base_url: impl Into<String>, user_token: Option<String>) -> Self {
|
||||
let http = crate::http::shared_client();
|
||||
let credentials = crate::util::grok_auth_credentials::GrokAuthCredentials::new(user_token);
|
||||
let credentials = crate::util::kigi_auth_credentials::KigiAuthCredentials::new(user_token);
|
||||
let client = Self::build_middleware_client(&http, &credentials);
|
||||
Self {
|
||||
http,
|
||||
@@ -361,7 +361,7 @@ impl FeedbackClient {
|
||||
base_url: impl Into<String>,
|
||||
user_token: Option<String>,
|
||||
) -> Self {
|
||||
let credentials = crate::util::grok_auth_credentials::GrokAuthCredentials::new(user_token);
|
||||
let credentials = crate::util::kigi_auth_credentials::KigiAuthCredentials::new(user_token);
|
||||
let client = Self::build_middleware_client(&http, &credentials);
|
||||
Self {
|
||||
http,
|
||||
@@ -398,7 +398,7 @@ impl FeedbackClient {
|
||||
|
||||
fn build_middleware_client(
|
||||
http: &reqwest::Client,
|
||||
credentials: &crate::util::grok_auth_credentials::GrokAuthCredentials,
|
||||
credentials: &crate::util::kigi_auth_credentials::KigiAuthCredentials,
|
||||
) -> reqwest_middleware::ClientWithMiddleware {
|
||||
let provider = Self::make_auth_provider(credentials);
|
||||
// max_retries=0: the middleware stamps the auth header but does NOT
|
||||
@@ -415,7 +415,7 @@ impl FeedbackClient {
|
||||
}
|
||||
|
||||
fn make_auth_provider(
|
||||
credentials: &crate::util::grok_auth_credentials::GrokAuthCredentials,
|
||||
credentials: &crate::util::kigi_auth_credentials::KigiAuthCredentials,
|
||||
) -> Arc<dyn kigi_auth::AuthCredentialProvider> {
|
||||
if let Some(am) = credentials.auth_manager() {
|
||||
Arc::new(
|
||||
@@ -1118,7 +1118,7 @@ mod forbidden_tests {
|
||||
#[cfg(test)]
|
||||
mod auth_refresh_tests {
|
||||
use super::*;
|
||||
use crate::auth::{AuthManager, AuthMode, GrokAuth, GrokComConfig};
|
||||
use crate::auth::{AuthManager, AuthMode, KimiAuth, KimiCodeConfig};
|
||||
use axum::{Router, routing::get};
|
||||
use chrono::{Duration, Utc};
|
||||
use std::net::SocketAddr;
|
||||
@@ -1156,14 +1156,14 @@ mod auth_refresh_tests {
|
||||
let (addr, _server) = start_server(router).await;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let am = Arc::new(AuthManager::new(dir.path(), GrokComConfig::default()));
|
||||
am.hot_swap(GrokAuth {
|
||||
let am = Arc::new(AuthManager::new(dir.path(), KimiCodeConfig::default()));
|
||||
am.hot_swap(KimiAuth {
|
||||
key: "fresh-from-auth-manager".into(),
|
||||
auth_mode: AuthMode::ApiKey,
|
||||
create_time: Utc::now(),
|
||||
user_id: "user-42".into(),
|
||||
expires_at: Some(Utc::now() + Duration::hours(1)),
|
||||
..GrokAuth::test_default()
|
||||
..KimiAuth::test_default()
|
||||
});
|
||||
|
||||
let client = FeedbackClient::new(
|
||||
@@ -1208,14 +1208,14 @@ mod auth_refresh_tests {
|
||||
let (addr, _server) = start_server(router).await;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let am = Arc::new(AuthManager::new(dir.path(), GrokComConfig::default()));
|
||||
let fresh = GrokAuth {
|
||||
let am = Arc::new(AuthManager::new(dir.path(), KimiCodeConfig::default()));
|
||||
let fresh = KimiAuth {
|
||||
key: "fresh-from-auth-manager".into(),
|
||||
auth_mode: AuthMode::ApiKey,
|
||||
create_time: Utc::now(),
|
||||
user_id: "user-42".into(),
|
||||
expires_at: Some(Utc::now() + Duration::hours(1)),
|
||||
..GrokAuth::test_default()
|
||||
..KimiAuth::test_default()
|
||||
};
|
||||
am.hot_swap(fresh);
|
||||
|
||||
@@ -1247,16 +1247,14 @@ mod auth_refresh_tests {
|
||||
_reason: crate::auth::refresh::RefreshReason,
|
||||
) -> crate::auth::refresh::RefreshOutcome {
|
||||
self.calls.fetch_add(1, Ordering::SeqCst);
|
||||
crate::auth::refresh::RefreshOutcome::Success(Box::new(GrokAuth {
|
||||
crate::auth::refresh::RefreshOutcome::Success(Box::new(KimiAuth {
|
||||
key: "fresh-from-refresher".into(),
|
||||
auth_mode: AuthMode::Oidc,
|
||||
auth_mode: AuthMode::OAuth,
|
||||
create_time: Utc::now(),
|
||||
user_id: "user-42".into(),
|
||||
refresh_token: Some("rt-fresh".into()),
|
||||
expires_at: Some(Utc::now() + Duration::hours(1)),
|
||||
oidc_issuer: Some("https://issuer.example".into()),
|
||||
oidc_client_id: Some("test-client".into()),
|
||||
..GrokAuth::test_default()
|
||||
..KimiAuth::test_default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1266,34 +1264,30 @@ mod auth_refresh_tests {
|
||||
#[tokio::test]
|
||||
async fn try_refresh_credentials_picks_up_disk_rotation_without_hitting_idp() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let cfg = GrokComConfig::default();
|
||||
let cfg = KimiCodeConfig::default();
|
||||
let scope = cfg.auth_scope();
|
||||
let am = Arc::new(AuthManager::new(dir.path(), cfg));
|
||||
|
||||
// In-memory: stale token (the one the server rejected).
|
||||
am.hot_swap(GrokAuth {
|
||||
am.hot_swap(KimiAuth {
|
||||
key: "stale-rejected".into(),
|
||||
auth_mode: AuthMode::Oidc,
|
||||
auth_mode: AuthMode::OAuth,
|
||||
create_time: Utc::now() - Duration::hours(2),
|
||||
user_id: "user-42".into(),
|
||||
refresh_token: Some("rt-stale".into()),
|
||||
expires_at: Some(Utc::now() + Duration::hours(1)),
|
||||
oidc_issuer: Some("https://issuer.example".into()),
|
||||
oidc_client_id: Some("test-client".into()),
|
||||
..GrokAuth::test_default()
|
||||
..KimiAuth::test_default()
|
||||
});
|
||||
|
||||
// Disk: a sibling already rotated to a fresh token.
|
||||
let disk_auth = GrokAuth {
|
||||
let disk_auth = KimiAuth {
|
||||
key: "fresh-from-sibling-on-disk".into(),
|
||||
auth_mode: AuthMode::Oidc,
|
||||
auth_mode: AuthMode::OAuth,
|
||||
create_time: Utc::now(),
|
||||
user_id: "user-42".into(),
|
||||
refresh_token: Some("rt-fresh".into()),
|
||||
expires_at: Some(Utc::now() + Duration::hours(1)),
|
||||
oidc_issuer: Some("https://issuer.example".into()),
|
||||
oidc_client_id: Some("test-client".into()),
|
||||
..GrokAuth::test_default()
|
||||
..KimiAuth::test_default()
|
||||
};
|
||||
let mut store = std::collections::BTreeMap::new();
|
||||
store.insert(scope, disk_auth);
|
||||
@@ -1329,15 +1323,15 @@ mod auth_refresh_tests {
|
||||
#[tokio::test]
|
||||
async fn try_refresh_credentials_returns_false_on_terminal_failure() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let am = Arc::new(AuthManager::new(dir.path(), GrokComConfig::default()));
|
||||
let am = Arc::new(AuthManager::new(dir.path(), KimiCodeConfig::default()));
|
||||
|
||||
// LegacySession: no refresh_token, no recovery possible.
|
||||
am.hot_swap(GrokAuth {
|
||||
am.hot_swap(KimiAuth {
|
||||
key: "legacy-rejected".into(),
|
||||
auth_mode: AuthMode::WebLogin,
|
||||
auth_mode: AuthMode::OAuth,
|
||||
create_time: Utc::now() - Duration::days(60),
|
||||
user_id: "user-42".into(),
|
||||
..GrokAuth::test_default()
|
||||
..KimiAuth::test_default()
|
||||
});
|
||||
|
||||
let client = FeedbackClient::new("http://example/v1", Some("legacy-rejected".into()))
|
||||
|
||||
Reference in new issue
Block a user