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, reason = "fallback URL is a hardcoded constant")]
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(
144 missing_debug_implementations,
145 reason = "generic facilitator may not impl Debug"
146)]
147pub struct PaygateBuilder<TFacilitator> {
148 facilitator: TFacilitator,
149 accepts: Vec<v2::PriceTag>,
150 resource: Option<v2::ResourceInfo>,
151}
152
153impl<TFacilitator> PaygateBuilder<TFacilitator> {
154 #[must_use]
156 pub fn accept(mut self, price_tag: v2::PriceTag) -> Self {
157 self.accepts.push(price_tag);
158 self
159 }
160
161 #[must_use]
163 pub fn accepts(mut self, price_tags: impl IntoIterator<Item = v2::PriceTag>) -> Self {
164 self.accepts.extend(price_tags);
165 self
166 }
167
168 #[must_use]
170 pub fn resource(mut self, resource: v2::ResourceInfo) -> Self {
171 self.resource = Some(resource);
172 self
173 }
174
175 pub fn build(self) -> Paygate<TFacilitator> {
179 Paygate {
180 facilitator: self.facilitator,
181 accepts: self.accepts.into(),
182 resource: self.resource.unwrap_or_else(|| v2::ResourceInfo {
183 description: String::new(),
184 mime_type: "application/json".to_owned(),
185 url: String::new(),
186 }),
187 }
188 }
189}
190
191#[allow(
202 missing_debug_implementations,
203 reason = "generic facilitator may not impl Debug"
204)]
205pub struct Paygate<TFacilitator> {
206 pub(crate) facilitator: TFacilitator,
207 pub(crate) accepts: Arc<[v2::PriceTag]>,
208 pub(crate) resource: v2::ResourceInfo,
209}
210
211impl<TFacilitator> Paygate<TFacilitator> {
212 pub const fn builder(facilitator: TFacilitator) -> PaygateBuilder<TFacilitator> {
214 PaygateBuilder {
215 facilitator,
216 accepts: Vec::new(),
217 resource: None,
218 }
219 }
220
221 pub const fn facilitator(&self) -> &TFacilitator {
223 &self.facilitator
224 }
225
226 pub fn accepts(&self) -> &[v2::PriceTag] {
228 &self.accepts
229 }
230
231 pub const fn resource(&self) -> &v2::ResourceInfo {
233 &self.resource
234 }
235
236 #[must_use]
246 #[allow(
247 clippy::expect_used,
248 reason = "infallible JSON/HTTP construction; panic indicates a bug"
249 )]
250 pub fn error_response(&self, err: PaygateError) -> Response {
251 match err {
252 PaygateError::Verification(ve) => {
253 let payment_required = v2::PaymentRequired {
254 error: Some(ve.to_string()),
255 accepts: self
256 .accepts
257 .iter()
258 .map(|pt| pt.requirements.clone())
259 .collect(),
260 x402_version: v2::V2,
261 resource: self.resource.clone(),
262 extensions: None,
263 };
264 let body_bytes =
265 serde_json::to_vec(&payment_required).expect("serialization failed");
266 let header_value =
267 HeaderValue::from_bytes(Base64Bytes::encode(&body_bytes).as_ref())
268 .expect("invalid header value");
269
270 Response::builder()
271 .status(StatusCode::PAYMENT_REQUIRED)
272 .header("Payment-Required", header_value)
273 .header("Content-Type", "application/json")
274 .body(Body::from(body_bytes))
275 .expect("failed to construct response")
276 }
277 PaygateError::Settlement(ref detail) => {
278 #[cfg(feature = "telemetry")]
279 tracing::error!(details = %detail, "Settlement failed");
280 let body = json!({ "error": "Settlement failed", "details": detail }).to_string();
281
282 Response::builder()
283 .status(StatusCode::PAYMENT_REQUIRED)
284 .header("Content-Type", "application/json")
285 .body(Body::from(body))
286 .expect("failed to construct response")
287 }
288 }
289 }
290}
291
292impl<TFacilitator> Paygate<TFacilitator>
293where
294 TFacilitator: Facilitator + Sync,
295{
296 pub async fn enrich_accepts(&mut self) {
298 let capabilities = self.facilitator.supported().await.unwrap_or_default();
299 let accepts: Vec<_> = self
300 .accepts
301 .iter()
302 .cloned()
303 .map(|mut pt| {
304 pt.enrich(&capabilities);
305 pt
306 })
307 .collect();
308 self.accepts = accepts.into();
309 }
310
311 #[cfg_attr(feature = "telemetry", instrument(name = "x402.verify_only", skip_all))]
322 pub async fn verify_only(&self, headers: &HeaderMap) -> Result<VerifiedPayment, PaygateError> {
323 let header_bytes = headers
324 .get(PAYMENT_HEADER)
325 .map(HeaderValue::as_bytes)
326 .ok_or(VerificationError::PaymentHeaderMissing)?;
327
328 let payload: PaymentPayload =
329 decode_payment_payload(header_bytes).ok_or(VerificationError::InvalidPaymentHeader)?;
330
331 let verify_request = build_verify_request(payload, &self.accepts)?;
332
333 let verify_response = self
334 .facilitator
335 .verify(verify_request.clone())
336 .await
337 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
338
339 if let proto::VerifyResponse::Invalid { reason, .. } = verify_response {
340 return Err(VerificationError::VerificationFailed(reason).into());
341 }
342
343 Ok(VerifiedPayment {
344 settle_request: verify_request.into(),
345 })
346 }
347
348 #[cfg_attr(
360 feature = "telemetry",
361 instrument(name = "x402.handle_request", skip_all)
362 )]
363 pub async fn handle_request<
364 ReqBody,
365 ResBody,
366 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
367 >(
368 &self,
369 inner: S,
370 req: http::Request<ReqBody>,
371 ) -> Result<Response, PaygateError>
372 where
373 S::Response: IntoResponse,
374 S::Error: IntoResponse,
375 S::Future: Send,
376 {
377 let verified = self.verify_only(req.headers()).await?;
378
379 let response = match call_inner(inner, req).await {
380 Ok(r) => r,
381 Err(err) => return Ok(err.into_response()),
382 };
383
384 if response.status().is_client_error() || response.status().is_server_error() {
385 return Ok(response.into_response());
386 }
387
388 let settlement = verified.settle(&self.facilitator).await?;
389 let header_value = settlement_to_header(&settlement)?;
390
391 let mut res = response;
392 res.headers_mut().insert("Payment-Response", header_value);
393 Ok(res.into_response())
394 }
395}
396
397impl<TFacilitator> Paygate<TFacilitator>
398where
399 TFacilitator: Facilitator + Clone + Send + Sync + 'static,
400{
401 #[cfg_attr(
415 feature = "telemetry",
416 instrument(name = "x402.handle_request_concurrent", skip_all)
417 )]
418 pub async fn handle_request_concurrent<
419 ReqBody,
420 ResBody,
421 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
422 >(
423 &self,
424 inner: S,
425 req: http::Request<ReqBody>,
426 ) -> Result<Response, PaygateError>
427 where
428 S::Response: IntoResponse,
429 S::Error: IntoResponse,
430 S::Future: Send + 'static,
431 ReqBody: Send + 'static,
432 {
433 let verified = self.verify_only(req.headers()).await?;
434
435 let facilitator = self.facilitator.clone();
436 let settle_handle = tokio::spawn(async move { verified.settle(&facilitator).await });
437
438 let response = match call_inner(inner, req).await {
439 Ok(r) => r,
440 Err(err) => {
441 drop(settle_handle);
442 return Ok(err.into_response());
443 }
444 };
445
446 if response.status().is_client_error() || response.status().is_server_error() {
447 drop(settle_handle);
448 return Ok(response.into_response());
449 }
450
451 let settlement = settle_handle
452 .await
453 .map_err(|e| PaygateError::Settlement(format!("settle task panicked: {e}")))??;
454 let header_value = settlement_to_header(&settlement)?;
455
456 let mut res = response;
457 res.headers_mut().insert("Payment-Response", header_value);
458 Ok(res.into_response())
459 }
460
461 #[cfg_attr(
482 feature = "telemetry",
483 instrument(name = "x402.handle_request_background", skip_all)
484 )]
485 pub async fn handle_request_background<
486 ReqBody,
487 ResBody,
488 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
489 >(
490 &self,
491 inner: S,
492 req: http::Request<ReqBody>,
493 ) -> Result<Response, PaygateError>
494 where
495 S::Response: IntoResponse,
496 S::Error: IntoResponse,
497 S::Future: Send + 'static,
498 ReqBody: Send + 'static,
499 {
500 let verified = self.verify_only(req.headers()).await?;
501
502 let facilitator = self.facilitator.clone();
504 tokio::spawn(async move {
505 if let Err(e) = verified.settle(&facilitator).await {
506 #[cfg(feature = "telemetry")]
507 tracing::error!(error = %e, "background settlement failed");
508 #[cfg(not(feature = "telemetry"))]
509 let _ = e;
510 }
511 });
512
513 match call_inner(inner, req).await {
514 Ok(r) => Ok(r.into_response()),
515 Err(err) => Ok(err.into_response()),
516 }
517 }
518}
519
520#[derive(Debug)]
526pub struct VerifiedPayment {
527 settle_request: proto::SettleRequest,
528}
529
530impl VerifiedPayment {
531 pub async fn settle<F: Facilitator>(
538 self,
539 facilitator: &F,
540 ) -> Result<proto::SettleResponse, PaygateError> {
541 let settlement = facilitator
542 .settle(self.settle_request)
543 .await
544 .map_err(|e| PaygateError::Settlement(format!("{e}")))?;
545
546 if let proto::SettleResponse::Error {
547 reason, message, ..
548 } = &settlement
549 {
550 let detail = message.as_deref().unwrap_or(reason.as_str());
551 return Err(PaygateError::Settlement(detail.to_owned()));
552 }
553
554 Ok(settlement)
555 }
556
557 #[must_use]
559 pub const fn settle_request(&self) -> &proto::SettleRequest {
560 &self.settle_request
561 }
562}
563
564pub fn settlement_to_header(
571 settlement: &proto::SettleResponse,
572) -> Result<HeaderValue, PaygateError> {
573 let encoded = settlement
574 .encode_base64()
575 .ok_or_else(|| PaygateError::Settlement("cannot encode error settlement".to_owned()))?;
576 HeaderValue::from_bytes(encoded.as_ref()).map_err(|e| PaygateError::Settlement(e.to_string()))
577}
578
579async fn call_inner<
581 ReqBody,
582 ResBody,
583 S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
584>(
585 mut inner: S,
586 req: http::Request<ReqBody>,
587) -> Result<http::Response<ResBody>, S::Error>
588where
589 S::Future: Send,
590{
591 #[cfg(feature = "telemetry")]
592 {
593 inner
594 .call(req)
595 .instrument(tracing::info_span!("inner"))
596 .await
597 }
598 #[cfg(not(feature = "telemetry"))]
599 {
600 inner.call(req).await
601 }
602}
603
604fn decode_payment_payload<T: serde::de::DeserializeOwned>(header_bytes: &[u8]) -> Option<T> {
606 let decoded = Base64Bytes::from(header_bytes).decode().ok()?;
607 serde_json::from_slice(decoded.as_ref()).ok()
608}
609
610fn build_verify_request(
613 payload: PaymentPayload,
614 accepts: &[v2::PriceTag],
615) -> Result<proto::VerifyRequest, VerificationError> {
616 let selected = accepts
617 .iter()
618 .find(|pt| **pt == payload.accepted)
619 .ok_or(VerificationError::NoPaymentMatching)?;
620
621 let verify = v2::VerifyRequest {
622 x402_version: v2::V2,
623 payment_payload: payload,
624 payment_requirements: selected.requirements.clone(),
625 };
626
627 let json = serde_json::to_value(&verify)
628 .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
629
630 Ok(proto::VerifyRequest::from(json))
631}