1use std::sync::Arc;
20
21use axum_core::body::Body;
22use axum_core::extract::Request;
23use axum_core::response::{IntoResponse, Response};
24use http::{HeaderMap, HeaderValue, StatusCode};
25use r402::facilitator::Facilitator;
26use r402::proto;
27use r402::proto::Base64Bytes;
28use r402::proto::v2;
29use serde_json::json;
30use tower::Service;
31#[cfg(feature = "telemetry")]
32use tracing::{Instrument, instrument};
33use url::Url;
34
35const PAYMENT_HEADER: &str = "Payment-Signature";
36
37#[derive(Debug, thiserror::Error)]
39pub enum VerificationError {
40 #[error("Payment-Signature header is required")]
42 PaymentHeaderMissing,
43 #[error("Invalid or malformed payment header")]
45 InvalidPaymentHeader,
46 #[error("Unable to find matching payment requirements")]
48 NoPaymentMatching,
49 #[error("Verification failed: {0}")]
51 VerificationFailed(String),
52}
53
54#[derive(Debug, thiserror::Error)]
56pub enum PaygateError {
57 #[error(transparent)]
59 Verification(#[from] VerificationError),
60 #[error("Settlement failed: {0}")]
62 Settlement(String),
63}
64
65type PaymentPayload = v2::PaymentPayload<v2::PaymentRequirements, serde_json::Value>;
66
67#[derive(Debug, Clone)]
72pub struct ResourceTemplate {
73 pub description: String,
75 pub mime_type: String,
77 pub url: Option<String>,
79}
80
81impl Default for ResourceTemplate {
82 fn default() -> Self {
83 Self {
84 description: String::new(),
85 mime_type: "application/json".to_owned(),
86 url: None,
87 }
88 }
89}
90
91impl ResourceTemplate {
92 #[allow(clippy::unwrap_used)]
103 pub fn resolve(&self, base_url: Option<&Url>, req: &Request) -> v2::ResourceInfo {
104 let url = self.url.clone().unwrap_or_else(|| {
105 let mut url = base_url.cloned().unwrap_or_else(|| {
106 let host = req
107 .headers()
108 .get("host")
109 .and_then(|h| h.to_str().ok())
110 .unwrap_or("localhost");
111 let origin = format!("http://{host}");
112 let url =
113 Url::parse(&origin).unwrap_or_else(|_| Url::parse("http://localhost").unwrap());
114 #[cfg(feature = "telemetry")]
115 tracing::warn!(
116 "X402Middleware base_url is not configured; \
117 using {url} as origin for resource resolution"
118 );
119 url
120 });
121 url.set_path(req.uri().path());
122 url.set_query(req.uri().query());
123 url.to_string()
124 });
125 v2::ResourceInfo {
126 description: self.description.clone(),
127 mime_type: self.mime_type.clone(),
128 url,
129 }
130 }
131}
132
133#[allow(missing_debug_implementations)]
144pub struct PaygateBuilder<TFacilitator> {
145 facilitator: TFacilitator,
146 accepts: Vec<v2::PriceTag>,
147 resource: Option<v2::ResourceInfo>,
148}
149
150impl<TFacilitator> PaygateBuilder<TFacilitator> {
151 #[must_use]
153 pub fn accept(mut self, price_tag: v2::PriceTag) -> Self {
154 self.accepts.push(price_tag);
155 self
156 }
157
158 #[must_use]
160 pub fn accepts(mut self, price_tags: impl IntoIterator<Item = v2::PriceTag>) -> Self {
161 self.accepts.extend(price_tags);
162 self
163 }
164
165 #[must_use]
167 pub fn resource(mut self, resource: v2::ResourceInfo) -> Self {
168 self.resource = Some(resource);
169 self
170 }
171
172 pub fn build(self) -> Paygate<TFacilitator> {
176 Paygate {
177 facilitator: self.facilitator,
178 accepts: Arc::new(self.accepts),
179 resource: self.resource.unwrap_or_else(|| v2::ResourceInfo {
180 description: String::new(),
181 mime_type: "application/json".to_owned(),
182 url: String::new(),
183 }),
184 }
185 }
186}
187
188#[allow(missing_debug_implementations)]
199pub struct Paygate<TFacilitator> {
200 pub(crate) facilitator: TFacilitator,
201 pub(crate) accepts: Arc<Vec<v2::PriceTag>>,
202 pub(crate) resource: v2::ResourceInfo,
203}
204
205impl<TFacilitator> Paygate<TFacilitator> {
206 pub const fn builder(facilitator: TFacilitator) -> PaygateBuilder<TFacilitator> {
208 PaygateBuilder {
209 facilitator,
210 accepts: Vec::new(),
211 resource: None,
212 }
213 }
214
215 pub const fn facilitator(&self) -> &TFacilitator {
217 &self.facilitator
218 }
219
220 pub fn accepts(&self) -> &[v2::PriceTag] {
222 &self.accepts
223 }
224
225 pub const fn resource(&self) -> &v2::ResourceInfo {
227 &self.resource
228 }
229
230 #[must_use]
240 pub fn error_response(&self, err: PaygateError) -> Response {
241 match err {
242 PaygateError::Verification(ve) => {
243 let payment_required = v2::PaymentRequired {
244 error: Some(ve.to_string()),
245 accepts: self
246 .accepts
247 .iter()
248 .map(|pt| pt.requirements.clone())
249 .collect(),
250 x402_version: v2::V2,
251 resource: self.resource.clone(),
252 extensions: None,
253 };
254 let body_bytes =
255 serde_json::to_vec(&payment_required).expect("serialization failed");
256 let header_value =
257 HeaderValue::from_bytes(Base64Bytes::encode(&body_bytes).as_ref())
258 .expect("invalid header value");
259
260 Response::builder()
261 .status(StatusCode::PAYMENT_REQUIRED)
262 .header("Payment-Required", header_value)
263 .header("Content-Type", "application/json")
264 .body(Body::from(body_bytes))
265 .expect("failed to construct response")
266 }
267 PaygateError::Settlement(ref detail) => {
268 #[cfg(feature = "telemetry")]
269 tracing::error!(details = %detail, "Settlement failed");
270 let body = json!({ "error": "Settlement failed", "details": detail }).to_string();
271
272 Response::builder()
273 .status(StatusCode::PAYMENT_REQUIRED)
274 .header("Content-Type", "application/json")
275 .body(Body::from(body))
276 .expect("failed to construct response")
277 }
278 }
279 }
280}
281
282impl<TFacilitator> Paygate<TFacilitator>
283where
284 TFacilitator: Facilitator + Sync,
285{
286 pub async fn enrich_accepts(&mut self) {
288 let capabilities = self.facilitator.supported().await.unwrap_or_default();
289 let accepts = (*self.accepts)
290 .clone()
291 .into_iter()
292 .map(|mut pt| {
293 pt.enrich(&capabilities);
294 pt
295 })
296 .collect();
297 self.accepts = Arc::new(accepts);
298 }
299
300 #[cfg_attr(feature = "telemetry", instrument(name = "x402.verify_only", skip_all))]
311 pub async fn verify_only(&self, headers: &HeaderMap) -> Result<VerifiedPayment, PaygateError> {
312 let header_bytes = headers
313 .get(PAYMENT_HEADER)
314 .map(HeaderValue::as_bytes)
315 .ok_or(VerificationError::PaymentHeaderMissing)?;
316
317 let payload: PaymentPayload =
318 decode_payment_payload(header_bytes).ok_or(VerificationError::InvalidPaymentHeader)?;
319
320 let verify_request = build_verify_request(payload, &self.accepts)?;
321
322 let verify_response = self
323 .facilitator
324 .verify(verify_request.clone())
325 .await
326 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
327
328 if let proto::VerifyResponse::Invalid { reason, .. } = verify_response {
329 return Err(VerificationError::VerificationFailed(reason).into());
330 }
331
332 Ok(VerifiedPayment {
333 settle_request: verify_request.into(),
334 })
335 }
336
337 #[cfg_attr(
349 feature = "telemetry",
350 instrument(name = "x402.handle_request", skip_all)
351 )]
352 pub async fn handle_request<
353 ReqBody,
354 ResBody,
355 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
356 >(
357 &self,
358 inner: S,
359 req: http::Request<ReqBody>,
360 ) -> Result<Response, PaygateError>
361 where
362 S::Response: IntoResponse,
363 S::Error: IntoResponse,
364 S::Future: Send,
365 {
366 let verified = self.verify_only(req.headers()).await?;
367
368 let response = match call_inner(inner, req).await {
369 Ok(r) => r,
370 Err(err) => return Ok(err.into_response()),
371 };
372
373 if response.status().is_client_error() || response.status().is_server_error() {
374 return Ok(response.into_response());
375 }
376
377 let settlement = verified.settle(&self.facilitator).await?;
378 let header_value = settlement_to_header(&settlement)?;
379
380 let mut res = response;
381 res.headers_mut().insert("Payment-Response", header_value);
382 Ok(res.into_response())
383 }
384}
385
386impl<TFacilitator> Paygate<TFacilitator>
387where
388 TFacilitator: Facilitator + Clone + Send + Sync + 'static,
389{
390 #[cfg_attr(
404 feature = "telemetry",
405 instrument(name = "x402.handle_request_concurrent", skip_all)
406 )]
407 pub async fn handle_request_concurrent<
408 ReqBody,
409 ResBody,
410 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
411 >(
412 &self,
413 inner: S,
414 req: http::Request<ReqBody>,
415 ) -> Result<Response, PaygateError>
416 where
417 S::Response: IntoResponse,
418 S::Error: IntoResponse,
419 S::Future: Send + 'static,
420 ReqBody: Send + 'static,
421 {
422 let verified = self.verify_only(req.headers()).await?;
423
424 let facilitator = self.facilitator.clone();
425 let settle_handle = tokio::spawn(async move { verified.settle(&facilitator).await });
426
427 let response = match call_inner(inner, req).await {
428 Ok(r) => r,
429 Err(err) => {
430 drop(settle_handle);
431 return Ok(err.into_response());
432 }
433 };
434
435 if response.status().is_client_error() || response.status().is_server_error() {
436 drop(settle_handle);
437 return Ok(response.into_response());
438 }
439
440 let settlement = settle_handle
441 .await
442 .map_err(|e| PaygateError::Settlement(format!("settle task panicked: {e}")))??;
443 let header_value = settlement_to_header(&settlement)?;
444
445 let mut res = response;
446 res.headers_mut().insert("Payment-Response", header_value);
447 Ok(res.into_response())
448 }
449
450 #[cfg_attr(
471 feature = "telemetry",
472 instrument(name = "x402.handle_request_background", skip_all)
473 )]
474 pub async fn handle_request_background<
475 ReqBody,
476 ResBody,
477 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
478 >(
479 &self,
480 inner: S,
481 req: http::Request<ReqBody>,
482 ) -> Result<Response, PaygateError>
483 where
484 S::Response: IntoResponse,
485 S::Error: IntoResponse,
486 S::Future: Send + 'static,
487 ReqBody: Send + 'static,
488 {
489 let verified = self.verify_only(req.headers()).await?;
490
491 let facilitator = self.facilitator.clone();
493 tokio::spawn(async move {
494 if let Err(e) = verified.settle(&facilitator).await {
495 #[cfg(feature = "telemetry")]
496 tracing::error!(error = %e, "background settlement failed");
497 #[cfg(not(feature = "telemetry"))]
498 let _ = e;
499 }
500 });
501
502 match call_inner(inner, req).await {
503 Ok(r) => Ok(r.into_response()),
504 Err(err) => Ok(err.into_response()),
505 }
506 }
507}
508
509#[derive(Debug)]
515pub struct VerifiedPayment {
516 settle_request: proto::SettleRequest,
517}
518
519impl VerifiedPayment {
520 pub async fn settle<F: Facilitator>(
527 self,
528 facilitator: &F,
529 ) -> Result<proto::SettleResponse, PaygateError> {
530 let settlement = facilitator
531 .settle(self.settle_request)
532 .await
533 .map_err(|e| PaygateError::Settlement(format!("{e}")))?;
534
535 if let proto::SettleResponse::Error {
536 reason, message, ..
537 } = &settlement
538 {
539 let detail = message.as_deref().unwrap_or(reason.as_str());
540 return Err(PaygateError::Settlement(detail.to_owned()));
541 }
542
543 Ok(settlement)
544 }
545
546 #[must_use]
548 pub const fn settle_request(&self) -> &proto::SettleRequest {
549 &self.settle_request
550 }
551}
552
553pub fn settlement_to_header(
560 settlement: &proto::SettleResponse,
561) -> Result<HeaderValue, PaygateError> {
562 let encoded = settlement
563 .encode_base64()
564 .ok_or_else(|| PaygateError::Settlement("cannot encode error settlement".to_owned()))?;
565 HeaderValue::from_bytes(encoded.as_ref()).map_err(|e| PaygateError::Settlement(e.to_string()))
566}
567
568async fn call_inner<
570 ReqBody,
571 ResBody,
572 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
573>(
574 mut inner: S,
575 req: http::Request<ReqBody>,
576) -> Result<http::Response<ResBody>, S::Error>
577where
578 S::Future: Send,
579{
580 #[cfg(feature = "telemetry")]
581 {
582 inner
583 .call(req)
584 .instrument(tracing::info_span!("inner"))
585 .await
586 }
587 #[cfg(not(feature = "telemetry"))]
588 {
589 inner.call(req).await
590 }
591}
592
593fn decode_payment_payload<T: serde::de::DeserializeOwned>(header_bytes: &[u8]) -> Option<T> {
595 let decoded = Base64Bytes::from(header_bytes).decode().ok()?;
596 serde_json::from_slice(decoded.as_ref()).ok()
597}
598
599fn build_verify_request(
602 payload: PaymentPayload,
603 accepts: &[v2::PriceTag],
604) -> Result<proto::VerifyRequest, VerificationError> {
605 let selected = accepts
606 .iter()
607 .find(|pt| **pt == payload.accepted)
608 .ok_or(VerificationError::NoPaymentMatching)?;
609
610 let verify = v2::VerifyRequest {
611 x402_version: v2::V2,
612 payment_payload: payload,
613 payment_requirements: selected.requirements.clone(),
614 };
615
616 let json = serde_json::to_value(&verify)
617 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
618
619 Ok(proto::VerifyRequest::from(json))
620}