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 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 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
91pub 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 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}