use std::sync::Arc;
use std::time::Duration;
use base64::Engine;
use nostr_sdk::{EventBuilder, Keys, Kind, Tag};
use tokio::sync::RwLock;
use web_time::SystemTime;
use crate::types::Nip98Response;
use crate::{Error, Result};
#[derive(Debug)]
struct CachedToken {
token: String,
expires_at: SystemTime,
}
#[derive(Debug)]
pub struct JwtAuthProvider {
base_url: String,
keys: Keys,
http_client: cdk_common::HttpClient,
cached_token: Arc<RwLock<Option<CachedToken>>>,
}
impl JwtAuthProvider {
pub fn new(base_url: String, keys: Keys) -> Self {
Self {
base_url,
keys,
http_client: cdk_common::HttpClient::new(),
cached_token: Arc::new(RwLock::new(None)),
}
}
async fn ensure_cached_token(&self) -> Result<String> {
if let Some(token) = self.get_valid_cached_token().await {
return Ok(token);
}
let token = self.fetch_fresh_jwt_token().await?;
self.cache_token(&token).await;
Ok(token)
}
async fn get_valid_cached_token(&self) -> Option<String> {
let cache = self.cached_token.read().await;
cache.as_ref().and_then(|cached| {
if cached.expires_at > SystemTime::now() {
Some(cached.token.clone())
} else {
None
}
})
}
async fn fetch_fresh_jwt_token(&self) -> Result<String> {
let auth_url = format!("{}/api/v2/auth/nip98", self.base_url);
let nostr_token = self.create_nip98_token_with_logging(&auth_url)?;
let response = self.send_auth_request(&auth_url, &nostr_token).await?;
self.parse_jwt_response(response).await
}
fn create_nip98_token_with_logging(&self, auth_url: &str) -> Result<String> {
tracing::debug!("Creating NIP-98 token for URL: {}", auth_url);
let nostr_token = self.create_nip98_token(auth_url, "GET")?;
tracing::debug!(
"NIP-98 token created (first 50 chars): {}",
&nostr_token[..50.min(nostr_token.len())]
);
Ok(nostr_token)
}
async fn send_auth_request(
&self,
auth_url: &str,
nostr_token: &str,
) -> Result<cdk_common::RawResponse> {
tracing::debug!("Sending request to: {}", auth_url);
tracing::debug!(
"Authorization header: Nostr {}",
&nostr_token[..50.min(nostr_token.len())]
);
let response = self
.http_client
.get(auth_url)
.header("Authorization", format!("Nostr {nostr_token}"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("User-Agent", "cdk-npubcash/0.13.0")
.send()
.await?;
tracing::debug!("Response status: {}", response.status());
Ok(response)
}
async fn parse_jwt_response(&self, response: cdk_common::RawResponse) -> Result<String> {
let status = response.status();
if !response.is_success() {
let error_text = response.text().await.unwrap_or_default();
tracing::error!("Auth failed - Status: {}, Body: {}", status, error_text);
return Err(Error::Auth(format!(
"Failed to get JWT: {status} - {error_text}"
)));
}
let nip98_response: Nip98Response = response.json().await?;
Ok(nip98_response.data.token)
}
async fn cache_token(&self, token: &str) {
let expires_at = SystemTime::now() + Duration::from_secs(5 * 60);
let mut cache = self.cached_token.write().await;
*cache = Some(CachedToken {
token: token.to_string(),
expires_at,
});
}
fn create_nip98_token(&self, url: &str, method: &str) -> Result<String> {
let u_tag = Tag::custom(
nostr_sdk::TagKind::Custom(std::borrow::Cow::Borrowed("u")),
vec![url],
);
let method_tag = Tag::custom(
nostr_sdk::TagKind::Custom(std::borrow::Cow::Borrowed("method")),
vec![method],
);
let event = EventBuilder::new(Kind::Custom(27235), "")
.tags(vec![u_tag, method_tag])
.sign_with_keys(&self.keys)
.map_err(|e| Error::Nostr(e.to_string()))?;
let json = serde_json::to_string(&event)?;
tracing::debug!("NIP-98 event JSON: {}", json);
let encoded = base64::engine::general_purpose::STANDARD.encode(json);
tracing::debug!("Base64 encoded token length: {}", encoded.len());
Ok(encoded)
}
pub async fn get_auth_token(&self, _url: &str, _method: &str) -> Result<String> {
let token = self.ensure_cached_token().await?;
Ok(format!("Bearer {token}"))
}
pub fn get_nip98_auth_header(&self, url: &str, method: &str) -> Result<String> {
let token = self.create_nip98_token(url, method)?;
Ok(format!("Nostr {token}"))
}
}