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, 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 outcomes = due
.into_iter()
.map(|(label, held)| {
let outcome = renew(ctx, &mut state, &label, &held).unwrap_or_else(Renewal::Failed);
(label, outcome)
})
.collect();
purge(ctx, &mut state);
outcomes
}
fn renew(ctx: &Context, state: &mut State, label: &str, held: &Park) -> Result<Renewal> {
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();
let fresh = match api::renew(ctx, refresh, &scopes, client_id) {
Ok(fresh) => fresh,
Err(ApiError::InvalidGrant) => {
state.discard(&held.service);
state::save(ctx, state)?;
return Ok(Renewal::Refused);
}
Err(ApiError::Network(_) | ApiError::RateLimited) => return Ok(Renewal::Deferred),
Err(e) => {
return Err(Error::RenewalFailed {
label: label.to_string(),
detail: e.to_string(),
});
}
};
let next = park::renewed(&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 = store().or_else(|_| store())?;
state.park(label, parked);
state::save(ctx, state)?;
Ok(Renewal::Renewed)
}