use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::Sha256;
use crate::error::Error;
use crate::jcs::canonical_json;
pub const VERSION: &str = "agent-pay/0.1";
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPayload {
pub v: String,
pub payment_hash: String,
pub expires_at: String,
}
fn b64url(data: &[u8]) -> String {
URL_SAFE_NO_PAD.encode(data)
}
fn b64url_decode(s: &str) -> Result<Vec<u8>, Error> {
URL_SAFE_NO_PAD
.decode(s)
.map_err(|e| Error::Token(format!("base64url: {e}")))
}
pub async fn issue_token(
payment_hash: &str,
expires_at: &str,
secret: &[u8],
) -> Result<String, Error> {
let payload = json!({
"v": VERSION,
"payment_hash": payment_hash,
"expires_at": expires_at,
});
let payload_bytes = canonical_json(&payload)?;
let mut mac =
HmacSha256::new_from_slice(secret).map_err(|e| Error::Token(format!("hmac key: {e}")))?;
mac.update(&payload_bytes);
let sig = mac.finalize().into_bytes();
Ok(format!("{}.{}", b64url(&payload_bytes), b64url(&sig)))
}
pub async fn verify_token(token: &str, secret: &[u8]) -> Result<TokenPayload, Error> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 2 {
return Err(Error::Token("token must have 2 parts".into()));
}
let payload_bytes = b64url_decode(parts[0])?;
let got = b64url_decode(parts[1])?;
let mut mac =
HmacSha256::new_from_slice(secret).map_err(|e| Error::Token(format!("hmac key: {e}")))?;
mac.update(&payload_bytes);
mac.verify_slice(&got)
.map_err(|_| Error::Token("token HMAC verification failed".into()))?;
let payload: TokenPayload = serde_json::from_slice(&payload_bytes)
.map_err(|e| Error::Token(format!("invalid token payload: {e}")))?;
if payload.v != VERSION {
return Err(Error::Token(format!(
"unsupported token version: {}",
payload.v
)));
}
Ok(payload)
}