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: Option<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(), cli.worker_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(), cli.worker_url.as_deref());
96
97 let sig = if let Some(kf) = key_file {
99 let key_bytes = crypto::read_key_file(kf)?;
100 let normalized_wallet = if chain.starts_with("eip155:") {
101 wallet.to_lowercase()
102 } else {
103 wallet.clone()
104 };
105 let challenge_msg = format!(
106 "Welcome to Backpac Agent Access.\n\n\
107 Please sign this message to verify ownership of this wallet and establish a session.\n\n\
108 Wallet: {}\nDID: {}\nChain: {}\nNonce: {}",
109 normalized_wallet,
110 did,
111 chain,
112 nonce
113 );
114 if chain.starts_with("eip155:") {
115 crypto::sign_eip4361_message(&challenge_msg, &hex::encode(&key_bytes))?
116 } else {
117 let secret = if key_bytes.len() == 64 {
119 &key_bytes[..32]
120 } else {
121 &key_bytes
122 };
123 let sig_bytes = crypto::ed25519_sign(challenge_msg.as_bytes(), secret)?;
124 bs58::encode(sig_bytes).into_string()
125 }
126 } else if let Some(s) = signature {
127 s.clone()
128 } else {
129 return Err(CairnError::InvalidInput(
130 "Either --signature or --key-file must be provided".to_string(),
131 ));
132 };
133
134 let body = json!({
135 "walletAddress": wallet,
136 "chain": chain,
137 "did": did,
138 "nonce": nonce,
139 "signature": sig
140 });
141 let result = client.post("/v1/agents/connect", &body).await?;
142
143 if let Some(jwt) = result.get("token").and_then(|v| v.as_str()) {
145 let creds = Credentials {
146 jwt: jwt.to_string(),
147 agent_id: result
148 .get("agentId")
149 .and_then(|v| v.as_str())
150 .map(String::from),
151 wallet: Some(wallet.clone()),
152 expires_at: result
153 .get("expires_at")
154 .and_then(|v| v.as_str())
155 .map(String::from),
156 };
157 creds.save()?;
158 }
159
160 output_json(&result, &cli.output);
161 Ok(())
162 }
163
164 AuthCommands::Refresh => {
165 let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref(), cli.worker_url.as_deref());
166 let result = client.post("/v1/agents/refresh", &json!({})).await?;
167
168 if let Some(jwt) = result.get("jwt").and_then(|v| v.as_str()) {
170 if let Some(mut creds) = Credentials::load() {
171 creds.jwt = jwt.to_string();
172 creds.expires_at = result
173 .get("expires_at")
174 .and_then(|v| v.as_str())
175 .map(String::from);
176 creds.save()?;
177 }
178 }
179
180 output_json(&result, &cli.output);
181 Ok(())
182 }
183
184 AuthCommands::Status => {
185 let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref(), None);
186 let result = client.get("/v1/agents/status").await?;
187 output_json(&result, &cli.output);
188 Ok(())
189 }
190 }
191 }
192}