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 "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 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 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 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 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}