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}