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