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 Challenge {
21 #[arg(long)]
23 wallet: String,
24
25 #[arg(long)]
27 chain: String,
28
29 #[arg(long)]
31 did: String,
32 },
33
34 Connect {
36 #[arg(long)]
38 wallet: String,
39
40 #[arg(long)]
42 chain: String,
43
44 #[arg(long)]
46 did: String,
47
48 #[arg(long)]
50 nonce: String,
51
52 #[arg(long)]
54 signature: String,
55
56 #[arg(long)]
58 key_file: Option<String>,
59 },
60
61 Refresh,
63
64 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 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 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 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 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}