Skip to main content

r402_core/extensions/
mod.rs

1//! Extension framework for x402 protocol extensions.
2//!
3//! Per the x402 v2 spec, an **extension** is a named piece of behaviour that
4//! attaches additional fields to the wire types (`PaymentRequired.extensions`,
5//! `PaymentPayload.extensions`, `VerifyResponse.extensions`, ...). Extensions
6//! are orthogonal to schemes: any scheme may opt into any extension, and
7//! extensions compose.
8//!
9//! # Architecture
10//!
11//! - [`Extension`] — the trait extensions implement. Uses AFIT (async fn in
12//!   traits) for zero-cost static dispatch when the concrete type is known.
13//! - [`DynExtension`] — object-safe erasure of [`Extension`] for heterogeneous
14//!   registries.
15//! - [`ExtensionRegistry`] — orders + indexes extensions for a facilitator /
16//!   paygate.
17//! - [`AdvertiseContext`] / [`VerifyContext`] / [`SettleContext`] — contexts
18//!   passed to each hook, carrying immutable borrowed views of the wire types.
19//!
20//! # Built-in Extensions
21//!
22//! Feature-gated:
23//!
24//! - `ext-bazaar` — [`bazaar::BazaarExtension`] for resource discovery
25//! - `ext-payment-id` — [`payment_id::PaymentIdentifierExtension`] for
26//!   client-supplied idempotency keys
27//!
28//! # Implementing Your Own
29//!
30//! ```no_run
31//! use std::future::Future;
32//! use r402_core::extensions::{Extension, AdvertiseContext, VerifyContext};
33//! use r402_core::wire::ExtensionEntry;
34//!
35//! struct MyExt;
36//!
37//! impl Extension for MyExt {
38//!     fn id(&self) -> &'static str { "my-ext" }
39//!     fn advertise(&self, _ctx: &AdvertiseContext<'_>) -> Option<ExtensionEntry> {
40//!         Some(ExtensionEntry::info(serde_json::json!({"version": 1})))
41//!     }
42//! }
43//! ```
44
45use std::collections::HashMap;
46use std::fmt::{self, Debug, Formatter};
47use std::future::Future;
48use std::pin::Pin;
49use std::sync::Arc;
50
51use compact_str::CompactString;
52
53use crate::wire::{
54    ExtensionEntry, Extensions, PaymentPayload, PaymentRequirements, SettleResponse, VerifyResponse,
55};
56
57#[cfg(feature = "ext-bazaar")]
58#[cfg_attr(docsrs, doc(cfg(feature = "ext-bazaar")))]
59pub mod bazaar;
60
61#[cfg(feature = "ext-payment-id")]
62#[cfg_attr(docsrs, doc(cfg(feature = "ext-payment-id")))]
63pub mod payment_id;
64
65/// Boxed future type alias used by [`DynExtension`].
66pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
67
68/// Context passed to [`Extension::advertise`].
69#[derive(Debug)]
70#[non_exhaustive]
71pub struct AdvertiseContext<'a> {
72    /// The requirement entry being advertised (if any). `None` when the
73    /// extension is being advertised as a top-level `PaymentRequired.extensions`
74    /// entry rather than per-requirement.
75    pub requirement: Option<&'a PaymentRequirements>,
76}
77
78/// Context passed to [`Extension::on_verify`].
79#[non_exhaustive]
80pub struct VerifyContext<'a> {
81    /// The fully-decoded payment payload (as generic JSON, since per-scheme
82    /// types are unknown at this level). Use `serde_json::from_value` if the
83    /// extension needs scheme-specific fields.
84    pub payload: &'a PaymentPayload<serde_json::Value, serde_json::Value>,
85    /// The matched requirements.
86    pub requirements: &'a PaymentRequirements,
87    /// The in-flight verify response (mutable so extensions may observe the
88    /// preliminary outcome before returning their own payload).
89    pub response: &'a VerifyResponse,
90}
91
92impl Debug for VerifyContext<'_> {
93    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
94        f.debug_struct("VerifyContext")
95            .field("requirements", &self.requirements)
96            .finish_non_exhaustive()
97    }
98}
99
100/// Context passed to [`Extension::on_settle`].
101#[non_exhaustive]
102pub struct SettleContext<'a> {
103    /// The fully-decoded payment payload.
104    pub payload: &'a PaymentPayload<serde_json::Value, serde_json::Value>,
105    /// The matched requirements.
106    pub requirements: &'a PaymentRequirements,
107    /// The in-flight settle response.
108    pub response: &'a SettleResponse,
109}
110
111impl Debug for SettleContext<'_> {
112    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113        f.debug_struct("SettleContext")
114            .field("requirements", &self.requirements)
115            .finish_non_exhaustive()
116    }
117}
118
119/// A single x402 protocol extension.
120///
121/// Each extension has a stable string ID that is used as the key in the
122/// `extensions` map on the wire. Extensions opt into lifecycle hooks by
123/// overriding default-noop methods.
124///
125/// **Static dispatch is preferred** — depend on concrete `impl Extension`
126/// types whenever possible for zero overhead. Use [`DynExtension`] only
127/// when a heterogeneous collection is unavoidable.
128pub trait Extension: Send + Sync {
129    /// Stable extension identifier (e.g. `"bazaar"`, `"payment-identifier"`).
130    fn id(&self) -> &'static str;
131
132    /// Called when the seller assembles a 402 response. Return an entry to
133    /// attach to `PaymentRequired.extensions` (or the per-requirement
134    /// extensions, if [`AdvertiseContext::requirement`] is `Some`).
135    fn advertise(&self, _ctx: &AdvertiseContext<'_>) -> Option<ExtensionEntry> {
136        None
137    }
138
139    /// Called by the facilitator after `verify`. Return an entry to attach
140    /// to the outgoing `VerifyResponse.extensions` map.
141    fn on_verify<'a>(
142        &'a self,
143        _ctx: &'a VerifyContext<'a>,
144    ) -> impl Future<Output = Option<ExtensionEntry>> + Send + 'a {
145        async { None }
146    }
147
148    /// Called by the facilitator after `settle`. Return an entry to attach
149    /// to the outgoing `SettleResponse.extensions` map.
150    fn on_settle<'a>(
151        &'a self,
152        _ctx: &'a SettleContext<'a>,
153    ) -> impl Future<Output = Option<ExtensionEntry>> + Send + 'a {
154        async { None }
155    }
156}
157
158/// Dyn-compatible erasure of [`Extension`] used by [`ExtensionRegistry`].
159///
160/// Users rarely implement this directly; the blanket `impl<T: Extension>`
161/// below turns any `Extension` into a `DynExtension`.
162pub trait DynExtension: Send + Sync {
163    /// Stable identifier.
164    fn id(&self) -> &'static str;
165
166    /// See [`Extension::advertise`].
167    fn advertise(&self, ctx: &AdvertiseContext<'_>) -> Option<ExtensionEntry>;
168
169    /// See [`Extension::on_verify`].
170    fn on_verify<'a>(&'a self, ctx: &'a VerifyContext<'a>)
171    -> BoxFuture<'a, Option<ExtensionEntry>>;
172
173    /// See [`Extension::on_settle`].
174    fn on_settle<'a>(&'a self, ctx: &'a SettleContext<'a>)
175    -> BoxFuture<'a, Option<ExtensionEntry>>;
176}
177
178impl<T: Extension + ?Sized> DynExtension for T {
179    fn id(&self) -> &'static str {
180        <Self as Extension>::id(self)
181    }
182
183    fn advertise(&self, ctx: &AdvertiseContext<'_>) -> Option<ExtensionEntry> {
184        <Self as Extension>::advertise(self, ctx)
185    }
186
187    fn on_verify<'a>(
188        &'a self,
189        ctx: &'a VerifyContext<'a>,
190    ) -> BoxFuture<'a, Option<ExtensionEntry>> {
191        Box::pin(<Self as Extension>::on_verify(self, ctx))
192    }
193
194    fn on_settle<'a>(
195        &'a self,
196        ctx: &'a SettleContext<'a>,
197    ) -> BoxFuture<'a, Option<ExtensionEntry>> {
198        Box::pin(<Self as Extension>::on_settle(self, ctx))
199    }
200}
201
202/// Ordered collection of extensions attached to a facilitator or paygate.
203///
204/// Stored as `Vec<Arc<dyn DynExtension>>` so extensions can be shared across
205/// clones. Lookups by ID go through a parallel `HashMap`.
206#[derive(Clone, Default)]
207pub struct ExtensionRegistry {
208    ordered: Vec<Arc<dyn DynExtension>>,
209    by_id: HashMap<CompactString, Arc<dyn DynExtension>>,
210}
211
212impl Debug for ExtensionRegistry {
213    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
214        let ids: Vec<&str> = self.ordered.iter().map(|e| e.id()).collect();
215        f.debug_tuple("ExtensionRegistry").field(&ids).finish()
216    }
217}
218
219impl ExtensionRegistry {
220    /// Constructs an empty registry.
221    #[must_use]
222    pub fn new() -> Self {
223        Self::default()
224    }
225
226    /// Registers an extension. Later registrations of the same id overwrite
227    /// earlier ones (idempotent insert).
228    pub fn register<E: Extension + 'static>(&mut self, extension: E) {
229        let id = extension.id();
230        let arc: Arc<dyn DynExtension> = Arc::new(extension);
231        // de-duplicate by id
232        self.ordered.retain(|existing| existing.id() != id);
233        self.ordered.push(Arc::clone(&arc));
234        let _ = self.by_id.insert(CompactString::from(id), arc);
235    }
236
237    /// Looks up an extension by its stable id.
238    #[must_use]
239    pub fn get(&self, id: &str) -> Option<&dyn DynExtension> {
240        self.by_id.get(id).map(AsRef::as_ref)
241    }
242
243    /// Iterates over all registered extensions in registration order.
244    pub fn iter(&self) -> impl Iterator<Item = &dyn DynExtension> {
245        self.ordered.iter().map(AsRef::as_ref)
246    }
247
248    /// Returns `true` when no extensions are registered.
249    #[must_use]
250    pub fn is_empty(&self) -> bool {
251        self.ordered.is_empty()
252    }
253
254    /// Number of registered extensions.
255    #[must_use]
256    pub fn len(&self) -> usize {
257        self.ordered.len()
258    }
259
260    /// Builds a wire-level [`Extensions`] block for seller advertising.
261    #[must_use]
262    pub fn advertise(&self, ctx: &AdvertiseContext<'_>) -> Extensions {
263        let mut out = Extensions::new();
264        for ext in self.iter() {
265            if let Some(entry) = ext.advertise(ctx) {
266                out.insert(ext.id(), entry);
267            }
268        }
269        out
270    }
271
272    /// Builds a wire-level [`Extensions`] block by collecting each extension's
273    /// `on_verify` return value.
274    pub async fn collect_verify(&self, ctx: &VerifyContext<'_>) -> Extensions {
275        let mut out = Extensions::new();
276        for ext in self.iter() {
277            if let Some(entry) = ext.on_verify(ctx).await {
278                out.insert(ext.id(), entry);
279            }
280        }
281        out
282    }
283
284    /// Builds a wire-level [`Extensions`] block by collecting each extension's
285    /// `on_settle` return value.
286    pub async fn collect_settle(&self, ctx: &SettleContext<'_>) -> Extensions {
287        let mut out = Extensions::new();
288        for ext in self.iter() {
289            if let Some(entry) = ext.on_settle(ctx).await {
290                out.insert(ext.id(), entry);
291            }
292        }
293        out
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use serde_json::json;
300
301    use super::*;
302
303    struct StubExt(&'static str, serde_json::Value);
304
305    impl Extension for StubExt {
306        fn id(&self) -> &'static str {
307            self.0
308        }
309        fn advertise(&self, _: &AdvertiseContext<'_>) -> Option<ExtensionEntry> {
310            Some(ExtensionEntry::info(self.1.clone()))
311        }
312    }
313
314    #[test]
315    fn register_and_lookup() {
316        let mut registry = ExtensionRegistry::new();
317        registry.register(StubExt("bazaar", json!({"registered": true})));
318        registry.register(StubExt("other", json!({"x": 1})));
319        assert_eq!(registry.len(), 2);
320        assert!(registry.get("bazaar").is_some());
321        assert!(registry.get("missing").is_none());
322    }
323
324    #[test]
325    fn duplicate_registration_overwrites() {
326        let mut registry = ExtensionRegistry::new();
327        registry.register(StubExt("x", json!(1)));
328        registry.register(StubExt("x", json!(2)));
329        assert_eq!(registry.len(), 1);
330        let ctx = AdvertiseContext { requirement: None };
331        let ext = registry.advertise(&ctx);
332        let val = ext.get("x").unwrap().as_info().unwrap();
333        assert_eq!(val, &json!(2));
334    }
335
336    #[test]
337    fn advertise_emits_every_entry() {
338        let mut registry = ExtensionRegistry::new();
339        registry.register(StubExt("a", json!("A")));
340        registry.register(StubExt("b", json!("B")));
341        let ctx = AdvertiseContext { requirement: None };
342        let ext = registry.advertise(&ctx);
343        assert_eq!(ext.len(), 2);
344    }
345}