use super::{journal, purge, try_exclusive};
use crate::api::{self, ApiError};
use crate::context::Context;
use crate::error::{Error, Result};
use crate::state::{Park, State};
use crate::{park, state, store, time};
use serde_json::Value;
const AHEAD_SECONDS: i64 = 120;
const DEFAULT_SCOPES: [&str; 6] = [
"user:profile",
"user:inference",
"user:sessions:claude_code",
"user:mcp_servers",
"user:file_upload",
"user:plugins",
];
#[derive(Debug)]
pub enum Renewal {
Renewed,
Refused,
Deferred,
Failed(Error),
}
impl Renewal {
pub fn code(&self) -> &'static str {
match self {
Renewal::Renewed => "renewed",
Renewal::Refused => "parked_login_refused",
Renewal::Deferred => "renewal_deferred",
Renewal::Failed(e) => e.code(),
}
}
}
pub fn renew_parked(ctx: &Context) -> Vec<(String, Renewal)> {
let Some(_exclusive) = try_exclusive(ctx) else {
return Vec::new();
};
if journal::pending(ctx) {
return Vec::new();
}
let Ok(mut state) = state::load(ctx) else {
return Vec::new();
};
let now = time::now();
let due: Vec<(String, Park)> = state
.accounts
.iter()
.filter_map(|a| {
let held = a.parked.as_ref()?;
(!held.askable_at(now + AHEAD_SECONDS) && held.restorable_at(now))
.then(|| (a.label.clone(), held.clone()))
})
.collect();
let asked: Vec<(String, Park, Result<Asked>)> = std::thread::scope(|scope| {
let handles: Vec<_> = due
.into_iter()
.map(|(label, held)| {
let ctx = &*ctx;
let handle = scope.spawn({
let label = label.clone();
let held = held.clone();
move || ask(ctx, &label, &held)
});
(label, held, handle)
})
.collect();
handles
.into_iter()
.map(|(label, held, handle)| {
let answer = handle.join().unwrap_or_else(|_| {
Err(Error::RenewalFailed {
label: label.clone(),
detail: "the renewal thread stopped".into(),
})
});
(label, held, answer)
})
.collect()
});
let outcomes = asked
.into_iter()
.map(|(label, held, answer)| {
let outcome =
apply(ctx, &mut state, &label, &held, answer).unwrap_or_else(Renewal::Failed);
(label, outcome)
})
.collect();
purge(ctx, &mut state);
outcomes
}
struct Asked {
oauth: Value,
fresh: Option<api::Renewed>,
refused: bool,
}
fn ask(ctx: &Context, label: &str, held: &Park) -> Result<Asked> {
let oauth = park::load(ctx, label, held)?;
let refresh = oauth["refreshToken"].as_str().unwrap_or_default();
let mut scopes: Vec<String> = oauth["scopes"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect();
if scopes.is_empty() {
scopes = DEFAULT_SCOPES.map(str::to_owned).to_vec();
}
let client_id = oauth["clientId"].as_str();
match api::renew(ctx, refresh, &scopes, client_id) {
Ok(fresh) => Ok(Asked {
oauth,
fresh: Some(fresh),
refused: false,
}),
Err(ApiError::InvalidGrant) => Ok(Asked {
oauth,
fresh: None,
refused: true,
}),
Err(ApiError::Network(_) | ApiError::RateLimited) => Ok(Asked {
oauth,
fresh: None,
refused: false,
}),
Err(e) => Err(Error::RenewalFailed {
label: label.to_string(),
detail: e.to_string(),
}),
}
}
fn apply(
ctx: &Context,
state: &mut State,
label: &str,
held: &Park,
asked: Result<Asked>,
) -> Result<Renewal> {
let asked = asked?;
if asked.refused {
state.discard(&held.service);
state::save(ctx, state)?;
return Ok(Renewal::Refused);
}
let Some(fresh) = asked.fresh else {
return Ok(Renewal::Deferred);
};
let next = park::renewed(&asked.oauth, &fresh, time::now_millis());
let uuid = state
.get(label)
.map(|a| a.account_uuid.clone())
.unwrap_or_default();
let store =
|| park::reserve(ctx, &uuid).and_then(|service| park::store_at(ctx, &service, &next));
let parked = match store().or_else(|_| store()) {
Ok(parked) => parked,
Err(e) => {
state.discard(&held.service);
let _ = state::save(ctx, state);
return Err(Error::RenewalFailed {
label: label.to_string(),
detail: e.to_string(),
});
}
};
state.park(label, parked.clone());
if let Err(e) = state::save(ctx, state) {
let _ = store::vault_delete(ctx, &parked.service);
return Err(e);
}
Ok(Renewal::Renewed)
}