Skip to main content

cairn_mod/cli/
logout.rs

1//! `cairn logout` — revoke at PDS, then remove local file.
2//!
3//! Per Q3 in the criteria confirmation: if PDS `deleteSession`
4//! fails (network down, 5xx, etc.) we STILL remove the local
5//! session file. The user's intent is "sever access"; refusing
6//! local cleanup when the PDS is unreachable leaves them with both
7//! a live PDS session AND a locally-usable session file — strictly
8//! worse than just a live PDS session. PDS failure is logged at
9//! warn level so operators can follow up.
10
11use std::path::Path;
12
13use super::error::CliError;
14use super::pds::{PdsClient, PdsError};
15use super::session::{self, SessionFile};
16
17/// Outcome of a logout call. Tests assert on this to distinguish
18/// "PDS revoked + local cleaned" from "PDS failed, local cleaned
19/// anyway" from "no session at all."
20#[derive(Debug, PartialEq, Eq)]
21pub enum LogoutOutcome {
22    /// No session file on disk — `cairn logout` is a no-op.
23    NotLoggedIn,
24    /// Happy path: PDS accepted `deleteSession` AND the local
25    /// session file was removed.
26    RevokedAndRemoved,
27    /// Local session file was removed, but the PDS
28    /// `deleteSession` call failed (network, 5xx, stale token).
29    /// Per Q3 decision, local cleanup proceeds regardless; the
30    /// PDS-side session stays live until it expires naturally.
31    RemovedLocalOnlyPdsFailed,
32}
33
34/// Revoke at PDS (best-effort) + remove local session file.
35/// Returns the outcome so `main.rs` can choose the user-facing
36/// message without string-matching.
37pub async fn logout(session_path: &Path) -> Result<LogoutOutcome, CliError> {
38    let session = match SessionFile::load(session_path)? {
39        Some(s) => s,
40        None => return Ok(LogoutOutcome::NotLoggedIn),
41    };
42
43    let pds_ok = match PdsClient::new(&session.pds_url) {
44        Ok(client) => try_revoke(&client, &session).await,
45        Err(_) => false,
46    };
47
48    // Local cleanup regardless of PDS outcome. `session::delete`
49    // is idempotent — an absent file on racy cleanup is fine.
50    session::delete(session_path)?;
51
52    if pds_ok {
53        Ok(LogoutOutcome::RevokedAndRemoved)
54    } else {
55        Ok(LogoutOutcome::RemovedLocalOnlyPdsFailed)
56    }
57}
58
59async fn try_revoke(pds: &PdsClient, session: &SessionFile) -> bool {
60    match pds.delete_session(&session.refresh_jwt).await {
61        Ok(()) => true,
62        Err(e) => {
63            emit_pds_warning(&session.pds_url, &e);
64            false
65        }
66    }
67}
68
69fn emit_pds_warning(pds_url: &str, err: &PdsError) {
70    tracing::warn!(
71        pds = %pds_url,
72        error = %err,
73        "PDS deleteSession failed; local session file will be removed regardless"
74    );
75}