1use std::convert::Infallible;
31use std::future::Future;
32use std::pin::Pin;
33use std::sync::Arc;
34use std::task::{Context, Poll};
35use std::time::Duration;
36
37use axum_core::extract::Request;
38use axum_core::response::Response;
39use http::{HeaderMap, Uri};
40use r402_core::facilitator::Facilitator;
41use r402_core::wire;
42use tower::util::BoxCloneSyncService;
43use tower::{Layer, Service};
44use url::Url;
45
46use super::facilitator::FacilitatorClient;
47use super::hooks::{DynPaygateHooks, PaygateHooks, ProtectedRequestOutcome};
48use super::paygate::{Paygate, ResourceTemplate};
49use super::pricing::{DynamicPriceTags, PriceTagSource, StaticPriceTags};
50
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
72pub enum SettlementMode {
73 #[default]
75 Sequential,
76 Concurrent,
78 Background,
80}
81
82pub struct X402Middleware<F> {
87 facilitator: F,
88 base_url: Option<Url>,
89}
90
91impl<F: Clone> Clone for X402Middleware<F> {
92 fn clone(&self) -> Self {
93 Self {
94 facilitator: self.facilitator.clone(),
95 base_url: self.base_url.clone(),
96 }
97 }
98}
99
100impl<F: std::fmt::Debug> std::fmt::Debug for X402Middleware<F> {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("X402Middleware")
103 .field("facilitator", &self.facilitator)
104 .field("base_url", &self.base_url)
105 .finish()
106 }
107}
108
109impl<F> X402Middleware<F> {
110 #[must_use]
115 pub const fn from_facilitator(facilitator: F) -> Self {
116 Self {
117 facilitator,
118 base_url: None,
119 }
120 }
121
122 pub const fn facilitator(&self) -> &F {
124 &self.facilitator
125 }
126}
127
128impl X402Middleware<Arc<FacilitatorClient>> {
129 #[must_use]
135 #[allow(
136 clippy::expect_used,
137 reason = "constructor panics on invalid URL by design"
138 )]
139 pub fn new(url: &str) -> Self {
140 let facilitator = FacilitatorClient::try_from(url).expect("Invalid facilitator URL");
141 Self {
142 facilitator: Arc::new(facilitator),
143 base_url: None,
144 }
145 }
146
147 pub fn try_new(url: &str) -> Result<Self, Box<dyn std::error::Error>> {
153 let facilitator = FacilitatorClient::try_from(url)?;
154 Ok(Self {
155 facilitator: Arc::new(facilitator),
156 base_url: None,
157 })
158 }
159
160 #[must_use]
162 pub fn facilitator_url(&self) -> &Url {
163 self.facilitator.base_url()
164 }
165
166 #[must_use]
171 pub fn with_supported_cache_ttl(&self, ttl: Duration) -> Self {
172 let inner = Arc::unwrap_or_clone(Arc::clone(&self.facilitator));
173 let facilitator = Arc::new(inner.with_supported_cache_ttl(ttl));
174 Self {
175 facilitator,
176 base_url: self.base_url.clone(),
177 }
178 }
179
180 #[must_use]
188 pub fn with_facilitator_timeout(&self, timeout: Duration) -> Self {
189 let inner = Arc::unwrap_or_clone(Arc::clone(&self.facilitator));
190 let facilitator = Arc::new(inner.with_timeout(timeout));
191 Self {
192 facilitator,
193 base_url: self.base_url.clone(),
194 }
195 }
196}
197
198impl TryFrom<&str> for X402Middleware<Arc<FacilitatorClient>> {
199 type Error = Box<dyn std::error::Error>;
200
201 fn try_from(value: &str) -> Result<Self, Self::Error> {
202 Self::try_new(value)
203 }
204}
205
206impl TryFrom<String> for X402Middleware<Arc<FacilitatorClient>> {
207 type Error = Box<dyn std::error::Error>;
208
209 fn try_from(value: String) -> Result<Self, Self::Error> {
210 Self::try_new(&value)
211 }
212}
213
214impl<F> X402Middleware<F>
215where
216 F: Clone,
217{
218 #[must_use]
225 pub fn with_base_url(&self, base_url: Url) -> Self {
226 let mut this = self.clone();
227 this.base_url = Some(base_url);
228 this
229 }
230}
231
232impl<TFacilitator> X402Middleware<TFacilitator>
233where
234 TFacilitator: Clone,
235{
236 #[must_use]
241 pub fn with_price_tag(
242 &self,
243 price_tag: wire::PriceTag,
244 ) -> X402LayerBuilder<StaticPriceTags, TFacilitator> {
245 X402LayerBuilder {
246 facilitator: self.facilitator.clone(),
247 price_source: StaticPriceTags::new(vec![price_tag]),
248 base_url: self.base_url.clone().map(Arc::new),
249 resource: Arc::new(ResourceTemplate::default()),
250 settlement_mode: SettlementMode::default(),
251 hooks: None,
252 }
253 }
254
255 #[must_use]
262 pub fn with_price_tags(
263 &self,
264 price_tags: Vec<wire::PriceTag>,
265 ) -> X402LayerBuilder<StaticPriceTags, TFacilitator> {
266 X402LayerBuilder {
267 facilitator: self.facilitator.clone(),
268 price_source: StaticPriceTags::new(price_tags),
269 base_url: self.base_url.clone().map(Arc::new),
270 resource: Arc::new(ResourceTemplate::default()),
271 settlement_mode: SettlementMode::default(),
272 hooks: None,
273 }
274 }
275
276 #[must_use]
281 pub fn with_dynamic_price<F, Fut>(
282 &self,
283 callback: F,
284 ) -> X402LayerBuilder<DynamicPriceTags, TFacilitator>
285 where
286 F: Fn(&HeaderMap, &Uri, Option<&Url>) -> Fut + Send + Sync + 'static,
287 Fut: Future<Output = Vec<wire::PriceTag>> + Send + 'static,
288 {
289 X402LayerBuilder {
290 facilitator: self.facilitator.clone(),
291 price_source: DynamicPriceTags::new(callback),
292 base_url: self.base_url.clone().map(Arc::new),
293 resource: Arc::new(ResourceTemplate::default()),
294 settlement_mode: SettlementMode::default(),
295 hooks: None,
296 }
297 }
298}
299
300#[derive(Clone)]
305#[allow(
306 missing_debug_implementations,
307 reason = "generic types may not impl Debug"
308)]
309pub struct X402LayerBuilder<TSource, TFacilitator> {
310 facilitator: TFacilitator,
311 base_url: Option<Arc<Url>>,
312 price_source: TSource,
313 resource: Arc<ResourceTemplate>,
314 settlement_mode: SettlementMode,
315 hooks: Option<Arc<dyn DynPaygateHooks>>,
316}
317
318impl<TFacilitator> X402LayerBuilder<StaticPriceTags, TFacilitator> {
319 #[must_use]
325 pub fn with_price_tag(mut self, price_tag: wire::PriceTag) -> Self {
326 self.price_source = self.price_source.with_price_tag(price_tag);
327 self
328 }
329}
330
331#[allow(
332 missing_debug_implementations,
333 reason = "generic types may not impl Debug"
334)]
335impl<TSource, TFacilitator> X402LayerBuilder<TSource, TFacilitator> {
336 #[must_use]
340 pub fn with_description(mut self, description: String) -> Self {
341 let mut new_resource = (*self.resource).clone();
342 new_resource.description = description;
343 self.resource = Arc::new(new_resource);
344 self
345 }
346
347 #[must_use]
351 pub fn with_mime_type(mut self, mime: String) -> Self {
352 let mut new_resource = (*self.resource).clone();
353 new_resource.mime_type = mime;
354 self.resource = Arc::new(new_resource);
355 self
356 }
357
358 #[must_use]
363 #[allow(
364 clippy::needless_pass_by_value,
365 reason = "Url consumed via to_string()"
366 )]
367 pub fn with_resource(mut self, resource: Url) -> Self {
368 let mut new_resource = (*self.resource).clone();
369 new_resource.url = Some(resource.to_string());
370 self.resource = Arc::new(new_resource);
371 self
372 }
373
374 #[must_use]
385 pub const fn with_settlement_mode(mut self, mode: SettlementMode) -> Self {
386 self.settlement_mode = mode;
387 self
388 }
389
390 #[must_use]
396 pub fn with_hooks<H>(mut self, hooks: H) -> Self
397 where
398 H: PaygateHooks + 'static,
399 {
400 self.hooks = Some(Arc::new(hooks));
401 self
402 }
403}
404
405impl<S, TSource, TFacilitator> Layer<S> for X402LayerBuilder<TSource, TFacilitator>
406where
407 S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + Sync + 'static,
408 S::Future: Send + 'static,
409 TFacilitator: Facilitator + Clone,
410 TSource: PriceTagSource,
411{
412 type Service = X402MiddlewareService<TSource, TFacilitator>;
413
414 fn layer(&self, inner: S) -> Self::Service {
415 X402MiddlewareService {
416 facilitator: self.facilitator.clone(),
417 base_url: self.base_url.clone(),
418 price_source: self.price_source.clone(),
419 resource: Arc::clone(&self.resource),
420 settlement_mode: self.settlement_mode,
421 hooks: self.hooks.clone(),
422 inner: BoxCloneSyncService::new(inner),
423 }
424 }
425}
426
427#[derive(Clone)]
432#[allow(
433 missing_debug_implementations,
434 reason = "BoxCloneSyncService does not impl Debug"
435)]
436pub struct X402MiddlewareService<TSource, TFacilitator> {
437 facilitator: TFacilitator,
439 base_url: Option<Arc<Url>>,
441 price_source: TSource,
443 resource: Arc<ResourceTemplate>,
445 settlement_mode: SettlementMode,
447 hooks: Option<Arc<dyn DynPaygateHooks>>,
449 inner: BoxCloneSyncService<Request, Response, Infallible>,
451}
452
453impl<TSource, TFacilitator> Service<Request> for X402MiddlewareService<TSource, TFacilitator>
454where
455 TSource: PriceTagSource,
456 TFacilitator: Facilitator + Clone + Send + Sync + 'static,
457{
458 type Response = Response;
459 type Error = Infallible;
460 type Future = Pin<Box<dyn Future<Output = Result<Response, Infallible>> + Send>>;
461
462 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
464 self.inner.poll_ready(cx)
465 }
466
467 #[allow(
469 clippy::excessive_nesting,
470 reason = "async move + match inside Box::pin is the idiomatic Service::call shape"
471 )]
472 fn call(&mut self, req: Request) -> Self::Future {
473 let price_source = self.price_source.clone();
474 let facilitator = self.facilitator.clone();
475 let base_url = self.base_url.clone();
476 let resource_builder = Arc::clone(&self.resource);
477 let settlement_mode = self.settlement_mode;
478 let hooks = self.hooks.clone();
479 let mut inner = self.inner.clone();
480
481 Box::pin(async move {
482 let mut req = req;
486 if let Some(h) = hooks.as_ref() {
487 match h.on_protected_request(&req).await {
488 ProtectedRequestOutcome::Continue => {}
489 ProtectedRequestOutcome::GrantAccess => return inner.call(req).await,
490 ProtectedRequestOutcome::Abort { status, body } => {
491 return Ok(build_abort_response(status, body));
492 }
493 }
494 }
495
496 let accepts = price_source
498 .resolve(req.headers(), req.uri(), base_url.as_deref())
499 .await;
500
501 if accepts.is_empty() {
503 return inner.call(req).await;
504 }
505
506 let resource = resource_builder.resolve(base_url.as_deref(), &req);
507
508 let mut gate_builder = Paygate::builder(facilitator)
509 .accepts(accepts)
510 .resource(resource);
511 if let Some(h) = hooks.as_ref() {
512 gate_builder = gate_builder.hooks_dyn(Arc::clone(h));
513 }
514 let mut gate = gate_builder.build();
515 gate.enrich_accepts().await;
516
517 if let Some(h) = hooks.as_ref() {
521 h.on_payment_verified(&mut req).await;
522 }
523
524 let result = match settlement_mode {
525 SettlementMode::Sequential => gate.handle_request(inner, req).await,
526 SettlementMode::Concurrent => gate.handle_request_concurrent(inner, req).await,
527 SettlementMode::Background => gate.handle_request_background(inner, req).await,
528 };
529 Ok(result.unwrap_or_else(|err| gate.error_response(err)))
530 })
531 }
532}
533
534fn build_abort_response(status: http::StatusCode, body: Option<String>) -> Response {
536 let mut response = Response::new(axum_core::body::Body::from(body.unwrap_or_default()));
537 *response.status_mut() = status;
538 if let Ok(ct) = http::HeaderValue::from_str("text/plain; charset=utf-8") {
539 let _ = response
540 .headers_mut()
541 .insert(http::header::CONTENT_TYPE, ct);
542 }
543 super::cors::ensure_expose_headers(response.headers_mut());
544 response
545}