use serde::{Deserialize, Serialize};
pub const CLOSE_APP_MISMATCH: u16 = 4001;
pub const CLOSE_HELLO_PROTOCOL: u16 = 4002;
pub const CLOSE_IDENTITY_MISMATCH: u16 = 4003;
pub const HELLO_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HelloEnvelope {
pub kind: HelloKind,
pub version: u32,
pub identity: PeerIdentity,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HelloKind {
Hello,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PeerIdentity {
pub app_id: String,
pub device_id: String,
pub device_name: String,
pub os: String,
#[serde(default)]
pub tailscale_id: String,
}
impl HelloEnvelope {
pub const CURRENT_VERSION: u32 = 2;
pub const MIN_SUPPORTED_VERSION: u32 = 2;
pub fn new(identity: PeerIdentity) -> Self {
Self {
kind: HelloKind::Hello,
version: Self::CURRENT_VERSION,
identity,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hello_envelope_round_trips_json() {
let hello = HelloEnvelope::new(PeerIdentity {
app_id: "playground".into(),
device_id: "01J4K9M2Z8AB3RNYQPW6H5TC0X".into(),
device_name: "Alice's MacBook".into(),
os: "darwin".into(),
tailscale_id: "node-abc".into(),
});
let json = serde_json::to_string(&hello).unwrap();
let parsed: HelloEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.version, HelloEnvelope::CURRENT_VERSION);
assert_eq!(parsed.identity.app_id, "playground");
assert_eq!(parsed.identity.device_id, "01J4K9M2Z8AB3RNYQPW6H5TC0X");
assert_eq!(parsed.identity.device_name, "Alice's MacBook");
assert_eq!(parsed.identity.os, "darwin");
assert_eq!(parsed.identity.tailscale_id, "node-abc");
}
#[test]
fn hello_envelope_json_layout_matches_rfc_shape() {
let hello = HelloEnvelope::new(PeerIdentity {
app_id: "chat".into(),
device_id: "01HZZZZZZZZZZZZZZZZZZZZZZZ".into(),
device_name: "laptop".into(),
os: "linux".into(),
tailscale_id: "tsnode-1".into(),
});
let value: serde_json::Value = serde_json::to_value(&hello).unwrap();
assert_eq!(value["kind"], "hello");
assert_eq!(value["version"], 2);
assert_eq!(value["identity"]["app_id"], "chat");
assert_eq!(value["identity"]["device_id"], "01HZZZZZZZZZZZZZZZZZZZZZZZ");
assert_eq!(value["identity"]["device_name"], "laptop");
assert_eq!(value["identity"]["os"], "linux");
}
#[test]
fn hello_envelope_rejects_unknown_kind() {
let raw = r#"{"kind":"not_hello","version":2,"identity":{"app_id":"a","device_id":"x","device_name":"y","os":"darwin","tailscale_id":"z"}}"#;
let parsed: Result<HelloEnvelope, _> = serde_json::from_str(raw);
assert!(
parsed.is_err(),
"unknown kind should fail to deserialize as HelloEnvelope"
);
}
}