Skip to main content

inflow_x402_axum/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use axum::response::Response;
4use http::{
5    StatusCode,
6    header::{CACHE_CONTROL, HeaderValue},
7};
8use inflow_x402_seller::{Error, Facilitator, Route};
9use serde::Serialize;
10use serde_json::Value;
11use tower::{Layer, util::MapResponse};
12use x402_axum::{X402Middleware, layer::X402LayerBuilder, paygate::StaticPriceTags};
13use x402_types::{proto::v2::PriceTag, scheme::ExtensionKey};
14
15type UpstreamLayer = X402LayerBuilder<StaticPriceTags<PriceTag>, Facilitator>;
16
17#[derive(Clone)]
18pub struct PaymentLayer(UpstreamLayer);
19
20impl PaymentLayer {
21    pub fn with_description(mut self, description: String) -> Self {
22        self.0 = self.0.with_description(description);
23        self
24    }
25
26    pub fn with_mime_type(mut self, mime: String) -> Self {
27        self.0 = self.0.with_mime_type(mime);
28        self
29    }
30
31    pub fn with_extension<T: ExtensionKey + Serialize>(mut self, extension: T) -> Self {
32        self.0 = self.0.with_extension(extension);
33        self
34    }
35}
36
37impl<S> Layer<S> for PaymentLayer
38where
39    UpstreamLayer: Layer<S>,
40{
41    type Service = MapResponse<<UpstreamLayer as Layer<S>>::Service, fn(Response) -> Response>;
42
43    fn layer(&self, inner: S) -> Self::Service {
44        MapResponse::new(self.0.layer(inner), payment_cache_control)
45    }
46}
47
48fn payment_cache_control(mut response: Response) -> Response {
49    // Upstream owns payment execution; these headers match Node's payment cache policy.
50    if matches!(
51        response.status(),
52        StatusCode::PAYMENT_REQUIRED | StatusCode::PRECONDITION_FAILED
53    ) {
54        response
55            .headers_mut()
56            .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
57    } else if response.headers().contains_key("payment-response") {
58        let private = response
59            .headers()
60            .get_all(CACHE_CONTROL)
61            .iter()
62            .any(|value| {
63                let mut quoted = false;
64                let mut escaped = false;
65                value
66                    .as_bytes()
67                    .split(|byte| {
68                        // Quoted extension values can contain commas and escaped quotes.
69                        if escaped {
70                            escaped = false;
71                            return false;
72                        }
73                        match byte {
74                            b'\\' if quoted => escaped = true,
75                            b'"' => quoted = !quoted,
76                            _ => {}
77                        }
78                        *byte == b',' && !quoted
79                    })
80                    .any(|directive| directive.trim_ascii().eq_ignore_ascii_case(b"private"))
81            });
82        if !private {
83            response
84                .headers_mut()
85                .append(CACHE_CONTROL, HeaderValue::from_static("private"));
86        }
87    }
88    response
89}
90
91/// Uses upstream verification, handler execution and settlement, in that order.
92pub fn payment_layer(
93    facilitator: Facilitator,
94    route: Route,
95    resource: &str,
96) -> Result<PaymentLayer, Error> {
97    let resource = url::Url::parse(resource)
98        .map_err(|_| invalid("resource must be an absolute HTTP or HTTPS URL"))?;
99    if !matches!(resource.scheme(), "http" | "https")
100        || resource.host_str().is_none()
101        || !resource.username().is_empty()
102        || resource.password().is_some()
103    {
104        return Err(invalid(
105            "resource must be an HTTP or HTTPS URL without credentials",
106        ));
107    }
108    // Upstream settles the authorized ceiling; it cannot consume measured usage from the handler.
109    if route.accepts.iter().any(|offer| offer.scheme == "upto") {
110        return Err(invalid(
111            "automatic metered routes are unsupported; use explicit verification and settlement",
112        ));
113    }
114    let mut offers = route.accepts.into_iter().map(|requirements| PriceTag {
115        requirements,
116        enricher: None,
117    });
118    let first = offers
119        .next()
120        .ok_or_else(|| invalid("protected route requires at least one payment offer"))?;
121    let mut layer = X402Middleware::from_facilitator(facilitator)
122        .with_price_tag(first)
123        .with_resource(resource);
124    for offer in offers {
125        layer = layer.with_price_tag(offer);
126    }
127    layer = layer.with_extension(Identifier(inflow_x402::identifier_declaration()));
128    for (key, value) in route.extensions {
129        layer = match key.as_str() {
130            "eip2612GasSponsoring" => layer.with_extension(Eip2612(value)),
131            "inflowEip7702GasSponsoring" => layer.with_extension(Eip7702(value)),
132            _ => {
133                return Err(invalid(
134                    "attach custom extensions using the returned layer's with_extension method",
135                ));
136            }
137        };
138    }
139    Ok(PaymentLayer(layer))
140}
141
142#[derive(Serialize)]
143#[serde(transparent)]
144struct Identifier(Value);
145impl ExtensionKey for Identifier {
146    const EXTENSION_KEY: &'static str = "payment-identifier";
147}
148#[derive(Serialize)]
149#[serde(transparent)]
150struct Eip2612(Value);
151impl ExtensionKey for Eip2612 {
152    const EXTENSION_KEY: &'static str = "eip2612GasSponsoring";
153}
154#[derive(Serialize)]
155#[serde(transparent)]
156struct Eip7702(Value);
157impl ExtensionKey for Eip7702 {
158    const EXTENSION_KEY: &'static str = "inflowEip7702GasSponsoring";
159}
160
161fn invalid(message: &str) -> Error {
162    Error::new("INVALID_X402_ROUTE", message)
163}