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                    "walletAddress": 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: extract secret key (first 32 bytes for Solana 64-byte keypairs)
113                        let secret = if key_bytes.len() == 64 {
114                            &key_bytes[..32]
115                        } else {
116                            &key_bytes
117                        };
118                        let sig_bytes = crypto::ed25519_sign(challenge_msg.as_bytes(), secret)?;
119                        bs58::encode(sig_bytes).into_string()
120                    }
121                } else {
122                    signature.clone()
123                };
124
125                let body = json!({
126                    "walletAddress": wallet,
127                    "chain": chain,
128                    "did": did,
129                    "nonce": nonce,
130                    "signature": sig
131                });
132                let result = client.post("/v1/agents/connect", &body).await?;
133
134                // Persist credentials
135                if let Some(jwt) = result.get("token").and_then(|v| v.as_str()) {
136                    let creds = Credentials {
137                        jwt: jwt.to_string(),
138                        agent_id: result
139                            .get("agentId")
140                            .and_then(|v| v.as_str())
141                            .map(String::from),
142                        wallet: Some(wallet.clone()),
143                        expires_at: result
144                            .get("expires_at")
145                            .and_then(|v| v.as_str())
146                            .map(String::from),
147                    };
148                    creds.save()?;
149                }
150
151                output_json(&result, &cli.output);
152                Ok(())
153            }
154
155            AuthCommands::Refresh => {
156                let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
157                let result = client.post("/v1/agents/refresh", &json!({})).await?;
158
159                // Update saved credentials with new JWT
160                if let Some(jwt) = result.get("jwt").and_then(|v| v.as_str()) {
161                    if let Some(mut creds) = Credentials::load() {
162                        creds.jwt = jwt.to_string();
163                        creds.expires_at = result
164                            .get("expires_at")
165                            .and_then(|v| v.as_str())
166                            .map(String::from);
167                        creds.save()?;
168                    }
169                }
170
171                output_json(&result, &cli.output);
172                Ok(())
173            }
174
175            AuthCommands::Status => {
176                let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
177                let result = client.get("/v1/agents/status").await?;
178                output_json(&result, &cli.output);
179                Ok(())
180            }
181        }
182    }
183}