aboutsummaryrefslogtreecommitdiff
path: root/src/client/endpoint.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-06 02:27:06 +0300
committerGitHub <noreply@github.com>2026-09-06 02:27:06 +0300
commit9e9bc8a144667a6b7debfd4f0c7a31ff0e11493a (patch)
tree03f1259f91fd9edeacfd4b904f2e84d4acf25232 /src/client/endpoint.rs
parent35b0dff96579f099e0436290085024424ffb289c (diff)
feat: manage multiple ssh machines from one client (#3670)
Diffstat (limited to 'src/client/endpoint.rs')
-rw-r--r--src/client/endpoint.rs120
1 files changed, 120 insertions, 0 deletions
diff --git a/src/client/endpoint.rs b/src/client/endpoint.rs
new file mode 100644
index 00000000..77aaae3b
--- /dev/null
+++ b/src/client/endpoint.rs
@@ -0,0 +1,120 @@
+use std::fmt;
+
+use serde::{Deserialize, Serialize};
+use sha2::{Digest as _, Sha256};
+
+mod activation;
+mod catalog;
+mod control;
+mod health;
+mod message_policy;
+mod registry;
+mod supervisor;
+mod writer;
+
+pub(crate) use activation::*;
+pub(crate) use catalog::*;
+pub(crate) use control::*;
+pub(crate) use message_policy::*;
+pub(crate) use registry::*;
+pub(crate) use supervisor::*;
+pub(crate) use writer::NativeEndpointTransport;
+
+const PROFILE_ID_BYTES: usize = 16;
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+#[serde(transparent)]
+pub(crate) struct ProfileId(String);
+
+impl ProfileId {
+ pub(crate) fn parse(value: impl Into<String>) -> Result<Self, String> {
+ let value = value.into();
+ if value.len() != PROFILE_ID_BYTES * 2
+ || !value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err("endpoint profile id must be 32 lowercase hexadecimal characters".into());
+ }
+ Ok(Self(value))
+ }
+
+ pub(crate) fn generate() -> Self {
+ use std::sync::atomic::{AtomicU64, Ordering};
+ static NEXT_ID: AtomicU64 = AtomicU64::new(1);
+
+ let sequence = NEXT_ID.fetch_add(1, Ordering::Relaxed);
+ let now = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_nanos();
+ let digest = Sha256::digest(format!("{}:{now}:{sequence}", std::process::id()).as_bytes());
+ Self(
+ digest[..PROFILE_ID_BYTES]
+ .iter()
+ .map(|byte| format!("{byte:02x}"))
+ .collect(),
+ )
+ }
+
+ pub(crate) fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+impl fmt::Display for ProfileId {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(&self.0)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub(crate) enum ClientEndpointId {
+ Local,
+ Ssh(ProfileId),
+}
+
+impl ClientEndpointId {
+ pub(crate) fn is_local(&self) -> bool {
+ matches!(self, Self::Local)
+ }
+
+ pub(crate) fn storage_key(&self) -> String {
+ match self {
+ Self::Local => "local".into(),
+ Self::Ssh(profile_id) => format!("ssh:{profile_id}"),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) enum ClientEndpointStatus {
+ Connecting,
+ Online,
+ Reconnecting,
+ Attention,
+ Disabled,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn profile_ids_are_opaque_and_stable_when_parsed() {
+ let first = ProfileId::generate();
+ let second = ProfileId::generate();
+ assert_ne!(first, second);
+ assert_eq!(ProfileId::parse(first.to_string()).unwrap(), first);
+ assert_eq!(first.as_str().len(), 32);
+ }
+
+ #[test]
+ fn endpoint_storage_keys_do_not_contain_ssh_targets() {
+ let profile = ProfileId::parse("0123456789abcdef0123456789abcdef").unwrap();
+ assert_eq!(
+ ClientEndpointId::Ssh(profile).storage_key(),
+ "ssh:0123456789abcdef0123456789abcdef"
+ );
+ }
+}