Skip to main content

iroh_services/
preset.rs

1//! An [`iroh::endpoint`] preset tailored for use with iroh-services.
2//!
3//! [`IrohServicesPreset`] starts from the n0 stock preset (production crypto
4//! provider + n0 DNS-based address lookup) and overlays the bits that
5//! iroh-services callers usually want to configure together: the relay map
6//! the endpoint should use, an optional explicit [`SecretKey`], and an
7//! optional [`ApiSecret`] that downstream code can retrieve to wire up a
8//! [`crate::Client`].
9//!
10//! # Example
11//! ```no_run
12//! use iroh::Endpoint;
13//!
14//! async fn run() -> anyhow::Result<()> {
15//!     let preset = iroh_services::preset()
16//!         .relays(["https://us-east1.project_username.iroh.link"])?
17//!         .api_secret_from_env()?
18//!         .build()?;
19//!     let endpoint = Endpoint::bind(preset.clone()).await?;
20//!     // the preset hands its api secret to the client, so you don't pass it twice
21//!     let _client = preset.client_builder(&endpoint).build().await?;
22//!     Ok(())
23//! }
24//! ```
25use std::{str::FromStr, time::Duration};
26
27use anyhow::{Context, Result, anyhow};
28use iroh::{Endpoint, RelayMap, RelayMode, RelayUrl, SecretKey, endpoint::presets::Preset};
29
30use crate::{
31    ClientBuilder,
32    api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret},
33    caps::{Cap, Caps, DEFAULT_CAP_EXPIRY},
34};
35
36/// An iroh endpoint preset configured for iroh-services. Build one with
37/// [`preset`] or [`IrohServicesPreset::builder`], then pass it to
38/// [`iroh::Endpoint::builder`].
39#[derive(Debug, Clone)]
40pub struct IrohServicesPreset {
41    secret_key: SecretKey,
42    relays: RelayMap,
43    // not used by the preset, only for creating a client builder
44    api_secret: ApiSecret,
45}
46
47impl IrohServicesPreset {
48    /// Start a new builder seeded with iroh-services defaults. Equivalent to
49    /// the free-standing [`preset`] function.
50    pub fn builder() -> PresetBuilder {
51        preset()
52    }
53
54    /// Returns the [`ApiSecret`] used to create this preset.
55    /// Useful for handing the same secret to a [`crate::Client`] without
56    /// plumbing it through twice.
57    pub fn api_secret(&self) -> &ApiSecret {
58        &self.api_secret
59    }
60
61    /// Returns a [`ClientBuilder`] pre-configured with this preset's API secret.
62    pub fn client_builder(&self, endpoint: &Endpoint) -> ClientBuilder {
63        // unwrap is ok here because the api_secret has been factored
64        // to the point that it can no longer fail.
65        ClientBuilder::new(endpoint)
66            .api_secret(self.api_secret.clone())
67            .unwrap()
68    }
69}
70
71impl Preset for IrohServicesPreset {
72    fn apply(self, builder: iroh::endpoint::Builder) -> iroh::endpoint::Builder {
73        // Inherit n0 defaults (crypto provider + DNS address lookup), then
74        // overlay our relay map and (optionally) an explicit secret key
75        let mut builder = iroh::endpoint::presets::N0.apply(builder);
76        builder = builder.relay_mode(RelayMode::Custom(self.relays));
77        builder = builder.secret_key(self.secret_key);
78        builder
79    }
80}
81
82/// Fluent builder for [`IrohServicesPreset`]. Construct one through
83/// [`preset`] or [`IrohServicesPreset::builder`].
84#[derive(Debug, Clone)]
85pub struct PresetBuilder {
86    cap_expiry: Duration,
87    secret_key: Option<SecretKey>,
88    relays: RelayMap,
89    api_secret: Option<ApiSecret>,
90}
91
92/// Start a new [`IrohServicesPreset`] builder seeded with iroh-services
93/// defaults: the n0 production relay map and no explicit secret key (the
94/// endpoint will generate one at bind time).
95///
96/// The built preset applies [`iroh::endpoint::presets::N0`] and then overlays
97/// its relay map, so leaving [`PresetBuilder::relays`] unset gives you plain
98/// `N0` plus a project access token for the public relays:
99///
100/// ```no_run
101/// # async fn run() -> anyhow::Result<()> {
102/// let preset = iroh_services::preset().api_secret_from_env()?.build()?;
103/// let endpoint = iroh::Endpoint::bind(preset.clone()).await?;
104/// // reuses the preset's api secret, no need to pass it twice
105/// let client = preset.client_builder(&endpoint).build().await?;
106/// # Ok(())
107/// # }
108/// ```
109pub fn preset() -> PresetBuilder {
110    PresetBuilder {
111        cap_expiry: DEFAULT_CAP_EXPIRY,
112        secret_key: None,
113        relays: iroh::endpoint::default_relay_mode().relay_map(),
114        api_secret: None,
115    }
116}
117
118impl PresetBuilder {
119    /// Set the endpoint's long-lived [`SecretKey`]. If left unset the
120    /// endpoint will generate a fresh random key at bind time.
121    pub fn secret_key(mut self, secret_key: SecretKey) -> Self {
122        self.secret_key = Some(secret_key);
123        self
124    }
125
126    /// Set relay URLs. This method accepts any iterator of &str, allowing the
127    /// common pattern:
128    /// ```no_run
129    /// fn build() -> anyhow::Result<()> {
130    ///     let _preset = iroh_services::preset()
131    ///         .relays([
132    ///             "https://us-east1.project_username.iroh.link",
133    ///             "https://eu-west1.project_username.iroh.link",
134    ///             "https://eu-central1.project_username.iroh.link",
135    ///         ])?
136    ///         .api_secret_from_env()?
137    ///         .build()?;
138    ///     Ok(())
139    /// }
140    /// ```
141    pub fn relays<I, S>(mut self, relays: I) -> Result<Self>
142    where
143        I: IntoIterator<Item = S>,
144        S: AsRef<str>,
145    {
146        let parsed = relays
147            .into_iter()
148            .map(|s| {
149                let s = s.as_ref();
150                s.parse::<RelayUrl>()
151                    .with_context(|| format!("invalid relay url {s:?}"))
152            })
153            .collect::<anyhow::Result<Vec<_>>>()?;
154
155        self.relays = RelayMap::from_iter(parsed);
156        Ok(self)
157    }
158
159    /// Pick relays via a [`RelayMode`] (e.g. `RelayMode::Staging` or a
160    /// pre-built `RelayMode::Custom(RelayMap)`).
161    pub fn relay_mode(mut self, mode: RelayMode) -> Self {
162        self.relays = mode.relay_map();
163        self
164    }
165
166    /// Pass in a [`RelayMap`] directly, bypassing URL parsing.
167    pub fn relay_map(mut self, map: RelayMap) -> Self {
168        self.relays = map;
169        self
170    }
171
172    /// Check IROH_SERVICES_API_SECRET environment variable for a valid API secret
173    pub fn api_secret_from_env(self) -> Result<Self> {
174        let ticket = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)?;
175        Ok(self.api_secret(ticket))
176    }
177
178    /// set client API secret from an encoded string
179    pub fn api_secret_from_str(self, secret_key: &str) -> Result<Self> {
180        let key = ApiSecret::from_str(secret_key).context("invalid iroh services api secret")?;
181        Ok(self.api_secret(key))
182    }
183
184    /// Stash an [`ApiSecret`] on the preset so callers can retrieve it later
185    /// via [`IrohServicesPreset::api_secret`] when constructing a client.
186    pub fn api_secret(mut self, api_secret: ApiSecret) -> Self {
187        self.api_secret = Some(api_secret);
188        self
189    }
190
191    /// Finalize the configuration into an [`IrohServicesPreset`].
192    pub fn build(self) -> Result<IrohServicesPreset> {
193        let secret_key = self.secret_key.unwrap_or_else(SecretKey::generate);
194
195        let Some(api_secret) = self.api_secret else {
196            return Err(anyhow!(
197                "api secret is required to use iroh_services relay preset"
198            ));
199        };
200
201        // build our token to interact with relays. This is only scoped to relay use.
202        let rcan = crate::caps::create_api_token_from_secret_key(
203            api_secret.secret.clone(),
204            secret_key.public(),
205            self.cap_expiry,
206            Caps::new([Cap::Relay(crate::caps::RelayCap::Use)]),
207        )?;
208
209        let mut token = data_encoding::BASE32_NOPAD.encode(&rcan.encode());
210        token.make_ascii_lowercase();
211
212        let relays = self.relays.with_auth_token(token);
213
214        Ok(IrohServicesPreset {
215            secret_key,
216            relays,
217            api_secret,
218        })
219    }
220}