use crate::core::CliError;
const CLERK_BASE: &str = "https://auth.suno.com";
const CLERK_JS_VERSION: &str = "5.117.0";
const CLERK_API_VERSION: &str = "2025-11-10";
fn clerk_client_url() -> String {
format!(
"{CLERK_BASE}/v1/client?__clerk_api_version={CLERK_API_VERSION}&_clerk_js_version={CLERK_JS_VERSION}"
)
}
fn clerk_token_url(session_id: &str) -> String {
format!(
"{CLERK_BASE}/v1/client/sessions/{session_id}/tokens?__clerk_api_version={CLERK_API_VERSION}&_clerk_js_version={CLERK_JS_VERSION}"
)
}
fn apply_clerk_headers(
builder: reqwest::RequestBuilder,
clerk_cookie: &str,
) -> reqwest::RequestBuilder {
builder
.header("authorization", clerk_cookie)
.header("cookie", format!("__client={clerk_cookie}"))
.header("origin", "https://suno.com")
.header("referer", "https://suno.com/")
}
fn response_excerpt(body: &str) -> String {
const MAX: usize = 500;
let body = body.replace(['\n', '\r'], " ");
if body.len() <= MAX {
body
} else {
format!("{}...", body.chars().take(MAX).collect::<String>())
}
}
pub async fn clerk_token_exchange(
client: &reqwest::Client,
clerk_cookie: &str,
) -> Result<(String, String), CliError> {
let resp = apply_clerk_headers(client.get(clerk_client_url()), clerk_cookie)
.send()
.await
.map_err(CliError::Http)?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(CliError::Api {
code: "clerk_exchange_failed",
message: format!(
"Clerk token exchange failed ({status}): {}",
response_excerpt(&body)
),
});
}
let body: serde_json::Value = resp.json().await.map_err(CliError::Http)?;
let session_id = body
.get("response")
.and_then(|r| {
r.get("last_active_session_id")
.and_then(|s| s.as_str())
.or_else(|| {
r.get("sessions")
.and_then(|s| s.as_array())
.and_then(|sessions| sessions.first())
.and_then(|session| session.get("id"))
.and_then(|id| id.as_str())
})
})
.ok_or_else(|| CliError::Api {
code: "no_session",
message: "No active session found - log into suno.com in your browser first".into(),
})?
.to_string();
let jwt = clerk_refresh_jwt(client, clerk_cookie, &session_id).await?;
Ok((session_id, jwt))
}
pub async fn clerk_refresh_jwt(
client: &reqwest::Client,
clerk_cookie: &str,
session_id: &str,
) -> Result<String, CliError> {
let resp = apply_clerk_headers(client.post(clerk_token_url(session_id)), clerk_cookie)
.header("content-type", "application/x-www-form-urlencoded")
.send()
.await
.map_err(CliError::Http)?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(CliError::Api {
code: "clerk_refresh_failed",
message: format!(
"Clerk JWT refresh failed ({status}): {}",
response_excerpt(&body)
),
});
}
let body: serde_json::Value = resp.json().await.map_err(CliError::Http)?;
body.get("jwt")
.and_then(|j| j.as_str())
.map(String::from)
.ok_or_else(|| CliError::Api {
code: "no_jwt",
message: "Clerk returned no JWT - session may have expired, run `sunox login` again"
.into(),
})
}