Skip to main content

heyo_sdk/
networks.rs

1//! Per-account sandbox networks. Mirrors `sdk-ts/src/networks.ts`.
2//!
3//! A `Network` is a control-plane record local and deployed sandboxes can
4//! register into so they're addressable across machines from the same
5//! account. Each account lazily gets a `default` network on first read
6//! (preserved from the unmerged `feat/sdn-basics` prototype); additional
7//! named networks can be created alongside it.
8
9use reqwest::Method;
10use serde::{Deserialize, Serialize};
11
12use crate::client::{HeyoClient, HeyoClientOptions, RequestOptions};
13use crate::commands::encode_path;
14use crate::errors::HeyoError;
15
16#[derive(Debug, Clone, Deserialize, Serialize)]
17pub struct NetworkInfo {
18    pub id: String,
19    pub account_id: String,
20    pub name: String,
21    pub is_default: bool,
22    #[serde(default)]
23    pub description: Option<String>,
24    pub created_at: String,
25    pub updated_at: String,
26}
27
28#[derive(Debug, Clone, Deserialize, Serialize)]
29pub struct NetworkMember {
30    pub network_id: String,
31    pub sandbox_kind: String,
32    pub sandbox_ref: String,
33    #[serde(default)]
34    pub device_name: Option<String>,
35    pub registered_at: String,
36    #[serde(default)]
37    pub last_seen_at: Option<String>,
38}
39
40#[derive(Debug, Clone, Default, Serialize)]
41pub struct NetworkCreateOptions {
42    pub name: String,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub description: Option<String>,
45}
46
47#[derive(Debug, Clone, Default, Serialize)]
48pub struct NetworkUpdateOptions {
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub name: Option<String>,
51    /// `None` leaves the description untouched; `Some(None)` clears it;
52    /// `Some(Some("…"))` sets it.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub description: Option<Option<String>>,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum NetworkMemberKind {
60    Local,
61    Deployed,
62}
63
64impl NetworkMemberKind {
65    pub fn as_str(&self) -> &'static str {
66        match self {
67            NetworkMemberKind::Local => "local",
68            NetworkMemberKind::Deployed => "deployed",
69        }
70    }
71}
72
73#[derive(Debug, Clone, Serialize)]
74pub struct NetworkMemberRegistration {
75    pub sandbox_kind: NetworkMemberKind,
76    pub sandbox_ref: String,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub device_name: Option<String>,
79}
80
81/// A private service route registered in a network — a `name:port` address
82/// other members can reach. When `connection_url` is set the service has a live
83/// iroh proxy route; otherwise it's `agent_proxy_pending` until someone runs
84/// `heyvm network expose-service`.
85#[derive(Debug, Clone, Deserialize, Serialize)]
86pub struct NetworkService {
87    pub id: String,
88    pub network_id: String,
89    /// Service name (the `name` in `name:port`).
90    pub name: String,
91    /// Convenience `name:port` form.
92    pub address: String,
93    pub sandbox_kind: String,
94    pub sandbox_ref: String,
95    pub port: u16,
96    pub protocol: String,
97    pub status: String,
98    /// `heyo://` ticket when a live route exists; `None` while pending.
99    #[serde(default)]
100    pub connection_url: Option<String>,
101    #[serde(default)]
102    pub last_seen_at: Option<String>,
103    /// `"iroh_tcp_proxy"` when routable, `"agent_proxy_pending"` otherwise.
104    #[serde(default)]
105    pub transport: Option<String>,
106}
107
108/// Result of dialing a service: a live route to it. `connection_url` is a
109/// `heyo://` ticket consumable by [`P2pTunnel::connect`](crate::P2pTunnel::connect).
110#[derive(Debug, Clone, Deserialize)]
111pub struct ServiceRoute {
112    /// `name:port` address that was dialed.
113    pub address: String,
114    /// Transport — `"iroh_tcp_proxy"`.
115    pub transport: String,
116    /// `heyo://` ticket for the live route.
117    pub connection_url: String,
118}
119
120#[derive(Deserialize)]
121struct NetworksEnvelope {
122    #[serde(default)]
123    networks: Vec<NetworkInfo>,
124}
125
126#[derive(Deserialize)]
127struct MembersEnvelope {
128    #[serde(default)]
129    members: Vec<NetworkMember>,
130}
131
132#[derive(Clone)]
133pub struct Network {
134    info: NetworkInfo,
135    client: HeyoClient,
136}
137
138impl Network {
139    fn from_raw(client: HeyoClient, info: NetworkInfo) -> Self {
140        Self { info, client }
141    }
142
143    pub fn id(&self) -> &str {
144        &self.info.id
145    }
146
147    pub fn name(&self) -> &str {
148        &self.info.name
149    }
150
151    pub fn is_default(&self) -> bool {
152        self.info.is_default
153    }
154
155    pub fn info(&self) -> &NetworkInfo {
156        &self.info
157    }
158
159    pub fn client(&self) -> &HeyoClient {
160        &self.client
161    }
162
163    /// `POST /networks` — create a named network.
164    pub async fn create(
165        options: NetworkCreateOptions,
166        client_options: HeyoClientOptions,
167    ) -> Result<Self, HeyoError> {
168        let client = HeyoClient::new(client_options)?;
169        let raw: NetworkInfo = client
170            .request(
171                Method::POST,
172                "/networks",
173                Some(&options),
174                RequestOptions::default(),
175            )
176            .await?;
177        Ok(Network::from_raw(client, raw))
178    }
179
180    /// `GET /networks` — list networks for the caller's account (lazily
181    /// creates the default network so the result is never empty).
182    pub async fn list(client_options: HeyoClientOptions) -> Result<Vec<NetworkInfo>, HeyoError> {
183        let client = HeyoClient::new(client_options)?;
184        let env: NetworksEnvelope = client
185            .request(Method::GET, "/networks", None::<&()>, RequestOptions::default())
186            .await?;
187        Ok(env.networks)
188    }
189
190    /// `GET /networks/{id}`.
191    pub async fn get(id: &str, client_options: HeyoClientOptions) -> Result<Self, HeyoError> {
192        let client = HeyoClient::new(client_options)?;
193        let path = format!("/networks/{}", encode_path(id));
194        let raw: NetworkInfo = client
195            .request(Method::GET, &path, None::<&()>, RequestOptions::default())
196            .await?;
197        Ok(Network::from_raw(client, raw))
198    }
199
200    /// `GET /networks/me` — the caller's default network, lazily created.
201    pub async fn default_for_me(
202        client_options: HeyoClientOptions,
203    ) -> Result<Self, HeyoError> {
204        let client = HeyoClient::new(client_options)?;
205        let raw: NetworkInfo = client
206            .request(Method::GET, "/networks/me", None::<&()>, RequestOptions::default())
207            .await?;
208        Ok(Network::from_raw(client, raw))
209    }
210
211    /// `DELETE /networks/{id}` — refuses the default network (400).
212    pub async fn delete_by_id(
213        id: &str,
214        client_options: HeyoClientOptions,
215    ) -> Result<(), HeyoError> {
216        let client = HeyoClient::new(client_options)?;
217        let path = format!("/networks/{}", encode_path(id));
218        client
219            .request::<serde_json::Value>(
220                Method::DELETE,
221                &path,
222                None::<&()>,
223                RequestOptions::default(),
224            )
225            .await?;
226        Ok(())
227    }
228
229    pub async fn delete(self) -> Result<(), HeyoError> {
230        let path = format!("/networks/{}", encode_path(&self.info.id));
231        self.client
232            .request::<serde_json::Value>(
233                Method::DELETE,
234                &path,
235                None::<&()>,
236                RequestOptions::default(),
237            )
238            .await?;
239        Ok(())
240    }
241
242    /// `PATCH /networks/{id}` — rename or change description. The default
243    /// network refuses rename attempts (server returns 400).
244    pub async fn update(&mut self, options: NetworkUpdateOptions) -> Result<(), HeyoError> {
245        let path = format!("/networks/{}", encode_path(&self.info.id));
246        let raw: NetworkInfo = self
247            .client
248            .request(Method::PATCH, &path, Some(&options), RequestOptions::default())
249            .await?;
250        self.info = raw;
251        Ok(())
252    }
253
254    /// `GET /networks/me/services` — services registered in the caller's
255    /// default network (each a `name:port` route). Use [`dial_service`](Self::dial_service)
256    /// to resolve one to a live connection ticket.
257    pub async fn list_services(
258        client_options: HeyoClientOptions,
259    ) -> Result<Vec<NetworkService>, HeyoError> {
260        let client = HeyoClient::new(client_options)?;
261        client
262            .request(
263                Method::GET,
264                "/networks/me/services",
265                None::<&()>,
266                RequestOptions::default(),
267            )
268            .await
269    }
270
271    /// `GET /networks/me/services/{name}/{port}/dial` — resolve a service to a
272    /// live iroh route. The returned [`ServiceRoute::connection_url`] is a
273    /// `heyo://` ticket consumable by [`P2pTunnel::connect`](crate::P2pTunnel::connect).
274    /// Errors with a 409 when the service has no live proxy route yet (run
275    /// `heyvm network expose-service` for the target).
276    pub async fn dial_service(
277        name: &str,
278        port: u16,
279        client_options: HeyoClientOptions,
280    ) -> Result<ServiceRoute, HeyoError> {
281        let client = HeyoClient::new(client_options)?;
282        let path = format!(
283            "/networks/me/services/{}/{}/dial",
284            encode_path(name),
285            port
286        );
287        client
288            .request(Method::GET, &path, None::<&()>, RequestOptions::default())
289            .await
290    }
291
292    /// `GET /networks/{id}/members`.
293    pub async fn list_members(&self) -> Result<Vec<NetworkMember>, HeyoError> {
294        let path = format!("/networks/{}/members", encode_path(&self.info.id));
295        let env: MembersEnvelope = self
296            .client
297            .request(Method::GET, &path, None::<&()>, RequestOptions::default())
298            .await?;
299        Ok(env.members)
300    }
301
302    /// `POST /networks/{id}/members` — register a sandbox into the
303    /// network. Idempotent on (network_id, sandbox_kind, sandbox_ref);
304    /// re-registering updates `device_name`.
305    pub async fn add_member(
306        &self,
307        registration: NetworkMemberRegistration,
308    ) -> Result<NetworkMember, HeyoError> {
309        let path = format!("/networks/{}/members", encode_path(&self.info.id));
310        self.client
311            .request(
312                Method::POST,
313                &path,
314                Some(&registration),
315                RequestOptions::default(),
316            )
317            .await
318    }
319
320    /// `DELETE /networks/{id}/members/{kind}/{ref}`.
321    pub async fn remove_member(
322        &self,
323        sandbox_kind: NetworkMemberKind,
324        sandbox_ref: &str,
325    ) -> Result<(), HeyoError> {
326        let path = format!(
327            "/networks/{}/members/{}/{}",
328            encode_path(&self.info.id),
329            sandbox_kind.as_str(),
330            encode_path(sandbox_ref)
331        );
332        self.client
333            .request::<serde_json::Value>(
334                Method::DELETE,
335                &path,
336                None::<&()>,
337                RequestOptions::default(),
338            )
339            .await?;
340        Ok(())
341    }
342}