use super::{shape, to_body, write_lock};
use crate::context::Context;
use crate::error::Error;
use crate::provider::claude::live;
use crate::provider::{self, ProviderError, ProviderId};
use crate::service::Permit;
use crate::state::{self, Renewing, State};
use crate::store::{self, RawStore};
use crate::{fault, lock, park};
use serde_json::Value;
const SAVE_ROUNDS: u32 = 4;
const COPY: &str = "renewing";
pub(super) struct Refreshing {
legacy: Option<lock::Guard>,
own: lock::Guard,
}
impl Refreshing {
pub(super) fn take(
ctx: &Context,
permit: Permit,
which: ProviderId,
) -> crate::error::Result<Option<Refreshing>> {
let Some((own, legacy)) = provider::of(which).refresh_lock(ctx) else {
return Ok(None);
};
let own = lock::acquire(permit, &own, lock::REFRESH)?;
let legacy = match lock::acquire(permit, &legacy, lock::REFRESH) {
Ok(legacy) => Some(legacy),
Err(lock::LockError::Io(_)) => None,
Err(busy) => return Err(busy.into()),
};
Ok(Some(Refreshing { legacy, own }))
}
pub(super) fn let_go(held: Option<Refreshing>) -> bool {
held.is_some_and(|held| {
held.own.compromised() || held.legacy.as_ref().is_some_and(lock::Guard::compromised)
})
}
}
#[derive(Clone, Copy)]
pub(super) struct Place<'a> {
pub(super) store: &'a dyn RawStore,
pub(super) service: &'a str,
}
#[derive(Debug, Default)]
pub(super) struct Progress {
pub(super) spent: bool,
pub(super) saved: bool,
pub(super) lock_lost: bool,
pub(super) kept: bool,
}
impl Progress {
pub(super) fn lost(&self) -> bool {
self.spent && !self.saved && !self.kept
}
}
pub(super) enum Renewed {
Saved(String),
Refused,
}
pub(super) enum Stop {
Changed,
Unrenewed(ProviderError),
Failed(Error),
}
impl From<Error> for Stop {
fn from(error: Error) -> Stop {
Stop::Failed(error)
}
}
impl From<store::Error> for Stop {
fn from(error: store::Error) -> Stop {
Stop::Failed(error.into())
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn renew(
ctx: &Context,
permit: Permit,
state: &mut State,
which: ProviderId,
place: Place<'_>,
seen: &str,
login: &Value,
progress: &mut Progress,
) -> Result<Renewed, Stop> {
let tool = provider::of(which);
if let Some(price) = place
.store
.cost(place.service, seen)
.filter(|p| p.refused())
{
return Err(Stop::Failed(Error::CredentialTooLarge {
tool: which,
label: which.name().to_owned(),
bytes: price.needs,
limit: price.limit,
}));
}
let writing = write_lock(ctx, permit, which)?;
if place.store.read(place.service)?.as_deref() != Some(seen) {
return Err(Stop::Changed);
}
drop(writing);
fault::point("refresh.checked");
let sent = tool.fingerprint(login);
let copy = reserve(ctx, permit, state, which, place, &sent, login)?;
let fresh = match tool.renew(
ctx,
permit,
&provider::Credential::new(which, login.clone()),
) {
Ok(fresh) => fresh.raw,
Err(error) => {
if let Some(copy) = © {
let_go(ctx, permit, state, copy);
}
return match error {
ProviderError::InvalidGrant { .. } => Ok(Renewed::Refused),
error => Err(Stop::Unrenewed(error)),
};
}
};
progress.spent = true;
let copied = copy.as_ref().is_some_and(|copy| {
!matches!(
store::vault_write(ctx, permit, copy, &to_body(fresh.clone())),
Err(store::Error::Write(_))
)
});
fault::point("refresh.exchanged");
let writing = write_lock_to_save(ctx, permit, which, progress);
let saved = save(permit, which, place, &sent, &fresh);
progress.lock_lost |= writing.as_ref().is_some_and(lock::Guard::compromised);
drop(writing);
if saved.is_ok() {
progress.saved = true;
fault::point("refresh.saved");
}
progress.kept = copied && matches!(saved, Err(Stop::Failed(_)));
if let Some(copy) = copy.filter(|_| !progress.kept) {
let_go(ctx, permit, state, ©);
}
saved.map(Renewed::Saved)
}
pub(super) fn finish(ctx: &Context, permit: Permit, state: &mut State) -> crate::error::Result<()> {
finish_where(ctx, permit, state, |_| true)
}
pub(super) fn finished(
ctx: &Context,
permit: Permit,
state: &mut State,
which: ProviderId,
) -> crate::error::Result<()> {
finish_where(ctx, permit, state, |left| left.tool == which)
}
fn finish_where(
ctx: &Context,
permit: Permit,
state: &mut State,
whose: impl Fn(&Renewing) -> bool,
) -> crate::error::Result<()> {
let mut finished = Ok(());
for left in state.renewing.clone().iter().filter(|left| whose(left)) {
finished = finished.and(save_left(ctx, permit, state, left));
}
finished
}
fn save_left(
ctx: &Context,
permit: Permit,
state: &mut State,
left: &Renewing,
) -> crate::error::Result<()> {
let fresh = store::vault_read(ctx, &left.service)?
.and_then(|held| serde_json::from_str::<Value>(&held).ok());
let Some(fresh) = fresh else {
let_go(ctx, permit, state, &left.service);
return Ok(());
};
if left.tool == ProviderId::Claude && crate::settings::custom_oauth(ctx) {
return Err(Error::CustomOauthEndpoint);
}
let live = super::live_store(ctx, left.tool)?;
if live.service != left.slot {
return Ok(());
}
let kept = store::of_kind(&live.chain, &left.store).ok_or_else(|| {
Error::Store(store::Error::Unreadable(format!(
"{} keeps no login in a {} here",
left.tool.name(),
left.store
)))
})?;
let place = Place {
store: kept,
service: &live.service,
};
let refreshing = Refreshing::take(ctx, permit, left.tool)?;
let saved = write_lock(ctx, permit, left.tool)
.map_err(Stop::from)
.and_then(|_writing| save(permit, left.tool, place, &left.sent, &fresh));
Refreshing::let_go(refreshing);
match saved {
Ok(_) | Err(Stop::Changed) => {
let_go(ctx, permit, state, &left.service);
Ok(())
}
Err(Stop::Failed(error)) => Err(error),
Err(Stop::Unrenewed(error)) => Err(super::unidentified(left.tool, error)),
}
}
fn write_lock_to_save(
ctx: &Context,
permit: Permit,
which: ProviderId,
progress: &mut Progress,
) -> Option<lock::Guard> {
for _ in 0..SAVE_ROUNDS {
match write_lock(ctx, permit, which) {
Ok(writing) => return writing,
Err(Error::Lock(lock::LockError::Busy)) => {}
Err(_) => break,
}
}
progress.lock_lost = true;
None
}
fn save(
permit: Permit,
which: ProviderId,
place: Place<'_>,
sent: &str,
fresh: &Value,
) -> Result<String, Stop> {
let tool = provider::of(which);
let now = place
.store
.read(place.service)?
.map(|now| live::document_in(&now))
.filter(|now| tool.fingerprint(now) == sent)
.ok_or(Stop::Changed)?;
let renewed = to_body(tool.splice(&now, fresh).map_err(|e| shape(which, e))?);
match place.store.write(permit, place.service, &renewed) {
Err(store::Error::Write(_)) => place.store.write(permit, place.service, &renewed)?,
written => written?,
}
Ok(renewed)
}
fn reserve(
ctx: &Context,
permit: Permit,
state: &mut State,
which: ProviderId,
place: Place<'_>,
sent: &str,
login: &Value,
) -> Result<Option<String>, Stop> {
let service = park::free_name(ctx, COPY)?;
if store::vault_cost(ctx, &service, &to_body(login.clone())).is_some_and(store::Cost::over) {
return Ok(None);
}
state.renewing.push(Renewing {
tool: which,
slot: place.service.to_owned(),
store: place.store.kind().name().to_owned(),
sent: sent.to_owned(),
service: service.clone(),
});
if let Err(error) = state::save(ctx, permit, state) {
state.renewing.retain(|left| left.service != service);
return Err(error.into());
}
Ok(Some(service))
}
fn let_go(ctx: &Context, permit: Permit, state: &mut State, service: &str) {
state.renewing.retain(|left| left.service != service);
state.discard(service);
if state::save(ctx, permit, state).is_ok() {
super::purge(ctx, permit, state);
}
}