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:
2026-07-17 07:37:29 -04:00
parent d6c20fc13f
commit 021b82443d
117 changed files with 4052 additions and 19900 deletions
@@ -1646,30 +1646,6 @@ fn verify_check_with_meta_resolves_pending_gate() {
assert!(app.pending_gate_verification.is_none());
}
/// The live check confirmed the block (meta WITH a gate): the paywall
/// shows with the authoritative gate.
#[test]
fn verify_check_with_gated_meta_shows_gate() {
let mut app = test_app();
let _effs = app.impose_gate(test_gate());
let meta = serde_json::to_value(kigi_shell::auth::AuthMeta {
gate: Some(test_gate()),
..Default::default()
})
.unwrap();
dispatch_task_result(
TaskResult::CheckSubscriptionComplete {
verify: Some(app.gate_verify_gen),
meta: Some(meta),
},
&mut app,
);
assert!(!app.has_access(), "verified gate must show");
assert!(app.pending_gate_verification.is_none());
}
/// The verification's own check failed (meta None) while its stale gate
/// was deferred: err on blocking — the deferred gate is promoted.
#[test]
@@ -1857,59 +1833,6 @@ fn gate_verify_timeout_stale_generation_is_ignored() {
);
}
/// A verified gate landing via `CheckSubscriptionComplete` (gated meta while
/// ungated) must arm the 5s paywall auto-check chain — verify-before-paywall
/// paths never went through the login-path chain start.
#[test]
fn verified_gate_via_check_complete_starts_paywall_chain() {
let mut app = test_app();
let _effs = app.impose_gate(test_gate());
let meta = serde_json::to_value(kigi_shell::auth::AuthMeta {
gate: Some(test_gate()),
..Default::default()
})
.unwrap();
let effects = dispatch_task_result(
TaskResult::CheckSubscriptionComplete {
verify: None,
meta: Some(meta),
},
&mut app,
);
assert!(!app.has_access());
assert!(
app.paywall_check_started.is_some(),
"verified gate must arm the paywall auto-check chain"
);
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::SchedulePaywallCheck)),
"verified gate must schedule the 5s chain; got: {effects:?}"
);
// Steady-state paywall-poller responses (already gated) must NOT fan
// out extra timers.
let meta = serde_json::to_value(kigi_shell::auth::AuthMeta {
gate: Some(test_gate()),
..Default::default()
})
.unwrap();
let effects = dispatch_task_result(
TaskResult::CheckSubscriptionComplete {
verify: None,
meta: Some(meta),
},
&mut app,
);
assert!(
effects.is_empty(),
"already-gated check responses must not schedule more timers; got: {effects:?}"
);
}
/// `GateRefreshed` with gate-free settings while a deferred gate awaits
/// verification must drop the pending copy — the fresh settings are newer
/// than the stale snapshot that produced it — and still run the lift