Skip to main content

cairn_cli/cli/
auth.rs

1use clap::{Args, Subcommand};
2use serde_json::json;
3
4use crate::client::BackpacClient;
5use crate::config::Credentials;
6use crate::crypto;
7use crate::errors::CairnError;
8
9use super::{output_json, Cli};
10
11#[derive(Args, Debug)]
12pub struct AuthArgs {
13    #[command(subcommand)]
14    pub command: AuthCommands,
15}
16
17#[derive(Subcommand, Debug)]
18pub enum AuthCommands {
19    /// Request a cryptographic challenge for wallet signing.
20    Challenge {
21        /// Wallet address to authenticate
22        #[arg(long)]
23        wallet: String,
24
25        /// Chain family (e.g., eip155:1, solana:mainnet)
26        #[arg(long)]
27        chain: String,
28
29        /// DID identifier for the agent
30        #[arg(long)]
31        did: String,
32    },
33
34    /// Submit signed challenge to receive a JWT.
35    Connect {
36        /// Wallet address
37        #[arg(long)]
38        wallet: String,
39
40        /// Chain family
41        #[arg(long)]
42        chain: String,
43
44        /// DID identifier
45        #[arg(long)]
46        did: String,
47
48        /// Nonce from the challenge response
49        #[arg(long)]
50        nonce: String,
51
52        /// Signature (hex or base58) of the challenge message
53        #[arg(long)]
54        signature: String,
55
56        /// Path to private key file for auto-signing (optional)
57        #[arg(long)]
58        key_file: Option<String>,
59    },
60
61    /// Refresh the current JWT token.
62    Refresh,
63
64    /// Check the status of the current authentication session.
65    Status,
66}
67
68impl AuthArgs {
69    pub async fn execute(&self, cli: &Cli) -> Result<(), CairnError> {
70        match &self.command {
71            AuthCommands::Challenge {
72                wallet,
73                chain,
74                did,
75            } => {
76                let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
77                let body = json!({
78                    "wallet_address": wallet,
79                    "chain": chain,
80                    "did": did
81                });
82                let result = client.post("/v1/agents/challenge", &body).await?;
83                output_json(&result, &cli.output);
84                Ok(())
85            }
86
87            AuthCommands::Connect {
88                wallet,
89                chain,
90                did,
91                nonce,
92                signature,
93                key_file,
94            } => {
95                let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
96
97                // If key_file is provided, auto-sign the challenge
98                let sig = if let Some(kf) = key_file {
99                    let key_bytes = crypto::read_key_file(kf)?;
100                    let challenge_msg = format!(
101                        "Welcome to Backpac Agent Access.\n\n\
102                         Please sign this message to verify ownership of this wallet and establish a session.\n\n\
103                         Wallet: {}\nDID: {}\nChain: {}\nNonce: {}",
104                        wallet.to_lowercase(),
105                        did,
106                        chain,
107                        nonce
108                    );
109                    if chain.starts_with("eip155:") {
110                        crypto::sign_eip4361_message(&challenge_msg, &hex::encode(&key_bytes))?
111                    } else {
112                        // Ed25519: sign and base58 encode
113                        let sig_bytes = crypto::ed25519_sign(challenge_msg.as_bytes(), &key_bytes)?;
114                        bs58::encode(sig_bytes).into_string()
115                    }
116                } else {
117                    signature.clone()
118                };
119
120                let body = json!({
121                    "wallet_address": wallet,
122                    "chain": chain,
123                    "did": did,
124                    "nonce": nonce,
125                    "signature": sig
126                });
127                let result = client.post("/v1/agents/connect", &body).await?;
128
129                // Persist credentials
130                if let Some(jwt) = result.get("jwt").and_then(|v| v.as_str()) {
131                    let creds = Credentials {
132                        jwt: jwt.to_string(),
133                        agent_id: result
134                            .get("agent_id")
135                            .and_then(|v| v.as_str())
136                            .map(String::from),
137                        wallet: Some(wallet.clone()),
138                        expires_at: result
139                            .get("expires_at")
140                            .and_then(|v| v.as_str())
141                            .map(String::from),
142                    };
143                    creds.save()?;
144                }
145
146                output_json(&result, &cli.output);
147                Ok(())
148            }
149
150            AuthCommands::Refresh => {
151                let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
152                let result = client.post("/v1/agents/refresh", &json!({})).await?;
153
154                // Update saved credentials with new JWT
155                if let Some(jwt) = result.get("jwt").and_then(|v| v.as_str()) {
156                    if let Some(mut creds) = Credentials::load() {
157                        creds.jwt = jwt.to_string();
158                        creds.expires_at = result
159                            .get("expires_at")
160                            .and_then(|v| v.as_str())
161                            .map(String::from);
162                        creds.save()?;
163                    }
164                }
165
166                output_json(&result, &cli.output);
167                Ok(())
168            }
169
170            AuthCommands::Status => {
171                let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
172                let result = client.get("/v1/agents/status").await?;
173                output_json(&result, &cli.output);
174                Ok(())
175            }
176        }
177    }
178}