use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use subtle::ConstantTimeEq;
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PricingConfig {
pub credit_cost_multiplier: f64,
#[serde(default = "default_pricing_model")]
pub pricing_model: String,
#[serde(default)]
pub sign_declarations: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effective_from: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effective_until: Option<String>,
}
fn default_pricing_model() -> String {
"per_token".into()
}
impl Default for PricingConfig {
fn default() -> Self {
Self {
credit_cost_multiplier: 1.0,
pricing_model: default_pricing_model(),
sign_declarations: false,
effective_from: None,
effective_until: None,
}
}
}
pub fn sign_body(body: &[u8], secret: &str) -> String {
let mut mac =
HmacSha256::new_from_slice(secret.as_bytes()).expect("hmac accepts any key length");
mac.update(body);
hex_encode(&mac.finalize().into_bytes())
}
pub fn verify_signature(body: &[u8], secret: &str, signature: &str) -> bool {
let expected = sign_body(body, secret);
expected.as_bytes().ct_eq(signature.as_bytes()).into()
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0xf) as usize] as char);
}
out
}
pub fn php_canonical_sign_body(credit_cost_multiplier: f64, pricing_model: &str) -> Vec<u8> {
let num = if credit_cost_multiplier.is_finite()
&& credit_cost_multiplier == credit_cost_multiplier.trunc()
&& credit_cost_multiplier.abs() < 1e15
{
format!("{}", credit_cost_multiplier as i64)
} else {
format!("{credit_cost_multiplier}")
};
let model_json = serde_json::to_string(pricing_model).unwrap_or_else(|_| "\"\"".into());
format!("{{\"credit_cost_multiplier\":{num},\"pricing_model\":{model_json}}}").into_bytes()
}
pub fn build_pricing_block(pricing: &PricingConfig, hmac_key: &str) -> serde_json::Value {
let mut block = serde_json::json!({
"credit_cost_multiplier": pricing.credit_cost_multiplier,
"pricing_model": pricing.pricing_model,
});
if pricing.sign_declarations && !hmac_key.is_empty() {
let body = php_canonical_sign_body(pricing.credit_cost_multiplier, &pricing.pricing_model);
block["declaration_signature"] = serde_json::Value::String(sign_body(&body, hmac_key));
}
if let Some(f) = &pricing.effective_from {
block["effective_from"] = serde_json::Value::String(f.clone());
}
if let Some(u) = &pricing.effective_until {
block["effective_until"] = serde_json::Value::String(u.clone());
}
block
}