use axum::body::Body;
use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use serde_json::json;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MppConfig {
pub enabled: bool,
pub amount: String,
pub currency: String,
pub recipient: String,
pub method: Option<String>,
}
impl MppConfig {
#[must_use]
pub fn is_configured(&self) -> bool {
self.enabled
&& !self.amount.trim().is_empty()
&& !self.currency.trim().is_empty()
&& !self.recipient.trim().is_empty()
}
}
#[must_use]
pub fn has_payment_credential(headers: &axum::http::HeaderMap) -> bool {
headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.is_some_and(|v| v.trim_start().starts_with("Payment "))
}
#[must_use]
pub fn payment_required(config: &MppConfig, path: &str) -> Response {
let challenge = challenge_header(config, path);
let mut resp = (
StatusCode::PAYMENT_REQUIRED,
axum::Json(json!({
"type": "error",
"error": {
"type": "payment_required",
"message": "Payment required for OpenAI-compatible endpoint",
"protocol": "mpp",
"intent": "charge",
"amount": config.amount,
"currency": config.currency,
"recipient": config.recipient,
"method": config.method,
"resource": path,
}
})),
)
.into_response();
resp.headers_mut()
.insert("content-type", HeaderValue::from_static("application/json"));
if let Ok(value) = HeaderValue::from_str(&challenge) {
resp.headers_mut().insert("www-authenticate", value);
}
resp
}
fn challenge_header(config: &MppConfig, path: &str) -> String {
let mut params = vec![
("protocol", "mpp".to_string()),
("intent", "charge".to_string()),
("amount", config.amount.clone()),
("currency", config.currency.clone()),
("recipient", config.recipient.clone()),
("resource", path.to_string()),
];
if let Some(method) = config.method.as_ref().filter(|s| !s.trim().is_empty()) {
params.push(("method", method.clone()));
}
let encoded = params
.into_iter()
.map(|(k, v)| format!(r#"{k}="{}""#, escape_header_value(&v)))
.collect::<Vec<_>>()
.join(", ");
format!("Payment {encoded}")
}
fn escape_header_value(value: &str) -> String {
value.replace('\\', r"\\").replace('"', r#"\""#)
}
#[must_use]
pub fn unsupported_payment_verification() -> Response {
let mut resp = Response::new(Body::from(
json!({
"type": "error",
"error": {
"type": "payment_verification_unavailable",
"message": "MPP payment credential verification is not configured"
}
})
.to_string(),
));
*resp.status_mut() = StatusCode::NOT_IMPLEMENTED;
resp.headers_mut()
.insert("content-type", HeaderValue::from_static("application/json"));
resp
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::HeaderMap;
fn config() -> MppConfig {
MppConfig {
enabled: true,
amount: "0.01".into(),
currency: "USD".into(),
recipient: "acct_123".into(),
method: Some("stripe".into()),
}
}
#[test]
fn challenge_response_uses_http_402_and_payment_auth_scheme() {
let resp = payment_required(&config(), "/v1/chat/completions");
assert_eq!(resp.status(), StatusCode::PAYMENT_REQUIRED);
let header = resp
.headers()
.get("www-authenticate")
.and_then(|v| v.to_str().ok())
.expect("challenge header");
assert!(header.starts_with("Payment "));
assert!(header.contains(r#"protocol="mpp""#));
assert!(header.contains(r#"intent="charge""#));
assert!(header.contains(r#"resource="/v1/chat/completions""#));
}
#[test]
fn detects_payment_authorization_credentials() {
let mut headers = HeaderMap::new();
assert!(!has_payment_credential(&headers));
headers.insert(
"authorization",
HeaderValue::from_static("Payment credential-token"),
);
assert!(has_payment_credential(&headers));
}
#[test]
fn requires_enabled_and_charge_details() {
assert!(config().is_configured());
let mut missing = config();
missing.recipient.clear();
assert!(!missing.is_configured());
}
}