Skip to main content

r402_protocol/
extension.rs

1//! Extension trait and registry.
2//!
3//! An extension attaches extra fields to `PaymentRequired.extensions`,
4//! `PaymentPayload.extensions`, and verify/settle `extensions`. Facilitator
5//! sidechannel outcomes live on response variants as `extension_responses`
6//! and are never merged into `extensions`.
7
8use std::collections::HashMap;
9use std::fmt::{self, Debug, Formatter};
10use std::future::Future;
11use std::pin::Pin;
12use std::sync::Arc;
13
14use compact_str::CompactString;
15
16use crate::payment::{
17    ExtensionEntry, Extensions, PaymentPayload, PaymentRequirements, ResourceInfo, SettleResponse,
18    VerifyResponse,
19};
20
21/// Boxed future used by [`DynExtension`].
22pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
23
24/// Context passed to [`Extension::advertise`] / [`Extension::enrich_payment_required`].
25#[derive(Debug)]
26#[non_exhaustive]
27pub struct AdvertiseContext<'a> {
28    /// Requirement being advertised. `None` for top-level `PaymentRequired.extensions`.
29    pub requirement: Option<&'a PaymentRequirements>,
30    /// Resource metadata from the in-progress 402 body.
31    pub resource: Option<&'a ResourceInfo>,
32    /// `accepts[]` from the in-progress 402 body.
33    pub accepts: &'a [PaymentRequirements],
34    /// Existing declaration already on the 402 body for this extension, if any.
35    pub existing: Option<&'a ExtensionEntry>,
36}
37
38impl<'a> AdvertiseContext<'a> {
39    /// Constructs an advertise context with no 402 body.
40    #[must_use]
41    pub const fn new(requirement: Option<&'a PaymentRequirements>) -> Self {
42        Self {
43            requirement,
44            resource: None,
45            accepts: &[],
46            existing: None,
47        }
48    }
49
50    /// Context for enriching a `PaymentRequired` body.
51    #[must_use]
52    pub const fn for_payment_required(
53        resource: &'a ResourceInfo,
54        accepts: &'a [PaymentRequirements],
55        existing: Option<&'a ExtensionEntry>,
56    ) -> Self {
57        Self {
58            requirement: None,
59            resource: Some(resource),
60            accepts,
61            existing,
62        }
63    }
64}
65
66/// Context passed to [`Extension::on_verify`].
67#[non_exhaustive]
68pub struct VerifyContext<'a> {
69    /// Decoded payment payload as generic JSON.
70    pub payload: &'a PaymentPayload<serde_json::Value, serde_json::Value>,
71    /// Matched requirements.
72    pub requirements: &'a PaymentRequirements,
73    /// In-flight verify response.
74    pub response: &'a VerifyResponse,
75}
76
77impl Debug for VerifyContext<'_> {
78    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
79        f.debug_struct("VerifyContext")
80            .field("requirements", &self.requirements)
81            .finish_non_exhaustive()
82    }
83}
84
85impl<'a> VerifyContext<'a> {
86    /// Constructs a verify context.
87    #[must_use]
88    pub const fn new(
89        payload: &'a PaymentPayload<serde_json::Value, serde_json::Value>,
90        requirements: &'a PaymentRequirements,
91        response: &'a VerifyResponse,
92    ) -> Self {
93        Self {
94            payload,
95            requirements,
96            response,
97        }
98    }
99}
100
101/// Context passed to [`Extension::on_settle`].
102#[non_exhaustive]
103pub struct SettleContext<'a> {
104    /// Decoded payment payload.
105    pub payload: &'a PaymentPayload<serde_json::Value, serde_json::Value>,
106    /// Matched requirements.
107    pub requirements: &'a PaymentRequirements,
108    /// In-flight settle response.
109    pub response: &'a SettleResponse,
110    /// Resource URL from the 402 / request, when known.
111    pub resource_url: Option<&'a str>,
112    /// 402 advertised extensions (declaration + enriched info).
113    pub advertised: Option<&'a Extensions>,
114}
115
116impl Debug for SettleContext<'_> {
117    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
118        f.debug_struct("SettleContext")
119            .field("requirements", &self.requirements)
120            .finish_non_exhaustive()
121    }
122}
123
124impl<'a> SettleContext<'a> {
125    /// Constructs a settle context.
126    #[must_use]
127    pub const fn new(
128        payload: &'a PaymentPayload<serde_json::Value, serde_json::Value>,
129        requirements: &'a PaymentRequirements,
130        response: &'a SettleResponse,
131    ) -> Self {
132        Self {
133            payload,
134            requirements,
135            response,
136            resource_url: None,
137            advertised: None,
138        }
139    }
140
141    /// Sets the 402/request resource URL.
142    #[must_use]
143    pub const fn with_resource_url(mut self, resource_url: &'a str) -> Self {
144        self.resource_url = Some(resource_url);
145        self
146    }
147
148    /// Sets advertised 402 extensions.
149    #[must_use]
150    pub const fn with_advertised(mut self, advertised: &'a Extensions) -> Self {
151        self.advertised = Some(advertised);
152        self
153    }
154}
155
156/// A single x402 protocol extension.
157///
158/// Prefer static `impl Extension`. Use [`DynExtension`] only for a
159/// heterogeneous collection.
160pub trait Extension: Send + Sync {
161    /// Stable extension identifier (`"bazaar"`, `"payment-identifier"`, …).
162    fn id(&self) -> &'static str;
163
164    /// Called when assembling a 402 response.
165    fn advertise(&self, _ctx: &AdvertiseContext<'_>) -> Option<ExtensionEntry> {
166        None
167    }
168
169    /// Info fields regenerated per 402 and excluded from echo matching.
170    fn dynamic_info_fields(&self) -> &'static [&'static str] {
171        &[]
172    }
173
174    /// Async 402 enrich. Default is a no-op; offer-receipt signs offers here.
175    ///
176    /// A `Some` result replaces any existing declaration for this extension.
177    fn enrich_payment_required<'a>(
178        &'a self,
179        _ctx: &'a AdvertiseContext<'a>,
180    ) -> impl Future<Output = Option<ExtensionEntry>> + Send + 'a {
181        std::future::ready(None)
182    }
183
184    /// Called after facilitator `verify`. Return value goes on
185    /// `VerifyResponse.extensions`, not `extension_responses`.
186    fn on_verify<'a>(
187        &'a self,
188        _ctx: &'a VerifyContext<'a>,
189    ) -> impl Future<Output = Option<ExtensionEntry>> + Send + 'a {
190        std::future::ready(None)
191    }
192
193    /// Called after facilitator `settle`. Return value goes on
194    /// `SettleResponse.extensions`, not `extension_responses`.
195    fn on_settle<'a>(
196        &'a self,
197        _ctx: &'a SettleContext<'a>,
198    ) -> impl Future<Output = Option<ExtensionEntry>> + Send + 'a {
199        std::future::ready(None)
200    }
201}
202
203/// Object-safe erasure of [`Extension`].
204pub trait DynExtension: Send + Sync {
205    /// Stable identifier.
206    fn id(&self) -> &'static str;
207
208    /// See [`Extension::advertise`].
209    fn advertise(&self, ctx: &AdvertiseContext<'_>) -> Option<ExtensionEntry>;
210
211    /// See [`Extension::dynamic_info_fields`].
212    fn dynamic_info_fields(&self) -> &'static [&'static str];
213
214    /// See [`Extension::enrich_payment_required`].
215    fn enrich_payment_required<'a>(
216        &'a self,
217        ctx: &'a AdvertiseContext<'a>,
218    ) -> BoxFuture<'a, Option<ExtensionEntry>>;
219
220    /// See [`Extension::on_verify`].
221    fn on_verify<'a>(&'a self, ctx: &'a VerifyContext<'a>)
222    -> BoxFuture<'a, Option<ExtensionEntry>>;
223
224    /// See [`Extension::on_settle`].
225    fn on_settle<'a>(&'a self, ctx: &'a SettleContext<'a>)
226    -> BoxFuture<'a, Option<ExtensionEntry>>;
227}
228
229impl<T: Extension + ?Sized> DynExtension for T {
230    fn id(&self) -> &'static str {
231        <Self as Extension>::id(self)
232    }
233
234    fn advertise(&self, ctx: &AdvertiseContext<'_>) -> Option<ExtensionEntry> {
235        <Self as Extension>::advertise(self, ctx)
236    }
237
238    fn dynamic_info_fields(&self) -> &'static [&'static str] {
239        <Self as Extension>::dynamic_info_fields(self)
240    }
241
242    fn enrich_payment_required<'a>(
243        &'a self,
244        ctx: &'a AdvertiseContext<'a>,
245    ) -> BoxFuture<'a, Option<ExtensionEntry>> {
246        Box::pin(<Self as Extension>::enrich_payment_required(self, ctx))
247    }
248
249    fn on_verify<'a>(
250        &'a self,
251        ctx: &'a VerifyContext<'a>,
252    ) -> BoxFuture<'a, Option<ExtensionEntry>> {
253        Box::pin(<Self as Extension>::on_verify(self, ctx))
254    }
255
256    fn on_settle<'a>(
257        &'a self,
258        ctx: &'a SettleContext<'a>,
259    ) -> BoxFuture<'a, Option<ExtensionEntry>> {
260        Box::pin(<Self as Extension>::on_settle(self, ctx))
261    }
262}
263
264/// Ordered collection of extensions.
265#[derive(Clone, Default)]
266pub struct ExtensionRegistry {
267    ordered: Vec<Arc<dyn DynExtension>>,
268    by_id: HashMap<CompactString, Arc<dyn DynExtension>>,
269}
270
271impl Debug for ExtensionRegistry {
272    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
273        let ids: Vec<&str> = self.ordered.iter().map(|e| e.id()).collect();
274        f.debug_tuple("ExtensionRegistry").field(&ids).finish()
275    }
276}
277
278impl ExtensionRegistry {
279    /// Empty registry.
280    #[must_use]
281    pub fn new() -> Self {
282        Self::default()
283    }
284
285    /// Registers an extension. Same id overwrites the previous entry.
286    pub fn register<E: Extension + 'static>(&mut self, extension: E) {
287        let id = extension.id();
288        let arc: Arc<dyn DynExtension> = Arc::new(extension);
289        self.ordered.retain(|existing| existing.id() != id);
290        self.ordered.push(Arc::clone(&arc));
291        let _ = self.by_id.insert(CompactString::from(id), arc);
292    }
293
294    /// Looks up an extension by stable id.
295    #[must_use]
296    pub fn get(&self, id: &str) -> Option<&dyn DynExtension> {
297        self.by_id.get(id).map(AsRef::as_ref)
298    }
299
300    /// Registered extensions in registration order.
301    pub fn iter(&self) -> impl Iterator<Item = &dyn DynExtension> {
302        self.ordered.iter().map(AsRef::as_ref)
303    }
304
305    /// Whether no extensions are registered.
306    #[must_use]
307    pub fn is_empty(&self) -> bool {
308        self.ordered.is_empty()
309    }
310
311    /// Number of registered extensions.
312    #[must_use]
313    pub fn len(&self) -> usize {
314        self.ordered.len()
315    }
316
317    /// Builds a wire [`Extensions`] block for seller advertising.
318    #[must_use]
319    pub fn advertise(&self, ctx: &AdvertiseContext<'_>) -> Extensions {
320        let mut out = Extensions::new();
321        for ext in self.iter() {
322            if let Some(entry) = ext.advertise(ctx) {
323                out.insert(ext.id(), entry);
324            }
325        }
326        out
327    }
328
329    /// Collects each extension's `on_verify` return value.
330    pub async fn collect_verify(&self, ctx: &VerifyContext<'_>) -> Extensions {
331        let mut out = Extensions::new();
332        for ext in self.iter() {
333            if let Some(entry) = ext.on_verify(ctx).await {
334                out.insert(ext.id(), entry);
335            }
336        }
337        out
338    }
339
340    /// Collects each extension's `on_settle` return value.
341    pub async fn collect_settle(&self, ctx: &SettleContext<'_>) -> Extensions {
342        let mut out = Extensions::new();
343        for ext in self.iter() {
344            if let Some(entry) = ext.on_settle(ctx).await {
345                out.insert(ext.id(), entry);
346            }
347        }
348        out
349    }
350}
351
352#[cfg(test)]
353#[allow(clippy::unwrap_used, reason = "unit tests panic on assertion failure")]
354mod tests {
355    use serde_json::json;
356
357    use super::*;
358
359    struct StubExt(&'static str, serde_json::Value);
360
361    impl Extension for StubExt {
362        fn id(&self) -> &'static str {
363            self.0
364        }
365
366        fn advertise(&self, _: &AdvertiseContext<'_>) -> Option<ExtensionEntry> {
367            Some(ExtensionEntry::info(self.1.clone()))
368        }
369    }
370
371    #[test]
372    fn register_and_lookup() {
373        let mut registry = ExtensionRegistry::new();
374        registry.register(StubExt("bazaar", json!({"registered": true})));
375        registry.register(StubExt("other", json!({"x": 1})));
376        assert_eq!(registry.len(), 2);
377        assert!(registry.get("bazaar").is_some());
378        assert!(registry.get("missing").is_none());
379    }
380
381    #[test]
382    fn duplicate_registration_overwrites() {
383        let mut registry = ExtensionRegistry::new();
384        registry.register(StubExt("x", json!(1)));
385        registry.register(StubExt("x", json!(2)));
386        assert_eq!(registry.len(), 1);
387        let ctx = AdvertiseContext::new(None);
388        let ext = registry.advertise(&ctx);
389        let val = ext.get("x").unwrap().as_info().unwrap();
390        assert_eq!(val, &json!(2));
391    }
392
393    #[test]
394    fn advertise_emits_every_entry() {
395        let mut registry = ExtensionRegistry::new();
396        registry.register(StubExt("a", json!("A")));
397        registry.register(StubExt("b", json!("B")));
398        let ctx = AdvertiseContext::new(None);
399        let ext = registry.advertise(&ctx);
400        assert_eq!(ext.len(), 2);
401    }
402}