cairn_mod/cli/auth.rs
1//! Shared CLI auth helpers (#28).
2//!
3//! Centralizes the `acquire_service_auth` orchestration — get a
4//! Cairn-bound service-auth token from the operator's PDS, with
5//! one-shot refresh-and-retry on a 401 from `getServiceAuth`. The
6//! refresh path also persists the rotated tokens back to the
7//! session file on disk so the next CLI invocation starts with
8//! current credentials (§5.3).
9//!
10//! Callers (cli/audit.rs, cli/report.rs, cli/retention.rs,
11//! cli/trust_chain.rs) used to carry byte-identical local copies of
12//! this function. Factoring threshold per session N3 was 6+
13//! identical copies; the trust-chain CLI (#37) brought the count
14//! to 8 callsites across 4 modules and tripped the rule.
15
16use std::path::Path;
17
18use super::error::CliError;
19use super::pds::{PdsClient, PdsError};
20use super::session::SessionFile;
21
22/// Acquire a service-auth token bound to the given lexicon method,
23/// refreshing the moderator session file in-place on a 401.
24///
25/// Behavior contract preserved from the pre-factor copies:
26///
27/// 1. Call `getServiceAuth(access_jwt, cairn_service_did, lxm)`.
28/// 2. On `Unauthorized { context: "getServiceAuth" }`, refresh
29/// the session via `refreshSession(refresh_jwt)`, write the
30/// rotated tokens back to `session_path` (mode 0600 owner
31/// invariants per §5.3), then retry `getServiceAuth` once.
32/// 3. Any other PDS error propagates as `CliError::Pds`.
33///
34/// One-shot retry — a second 401 is propagated, not chained into
35/// another refresh.
36pub(super) async fn acquire_service_auth(
37 pds: &PdsClient,
38 session: &mut SessionFile,
39 session_path: &Path,
40 lxm: &str,
41) -> Result<String, CliError> {
42 match pds
43 .get_service_auth(&session.access_jwt, &session.cairn_service_did, lxm)
44 .await
45 {
46 Ok(t) => Ok(t),
47 Err(PdsError::Unauthorized {
48 context: "getServiceAuth",
49 ..
50 }) => {
51 let refreshed = pds.refresh_session(&session.refresh_jwt).await?;
52 session.access_jwt = refreshed.access_jwt;
53 session.refresh_jwt = refreshed.refresh_jwt;
54 session.save(session_path)?;
55 Ok(pds
56 .get_service_auth(&session.access_jwt, &session.cairn_service_did, lxm)
57 .await?)
58 }
59 Err(other) => Err(other.into()),
60 }
61}