ap-relay-protocol 0.12.0

Shared wire protocol types for the ap-relay WebSocket server
Documentation
//! Rendezvous codes for temporary peer discovery.
//!
//! The rendezvous system provides short, human-readable codes that clients can share
//! to discover each other's identities without exchanging long-lived public keys.
//!
//! # Overview
//! 1. Client requests a rendezvous code from the server
//! 2. Server generates a unique code (e.g., "ABC-DEF-GHI") and maps it to the client's identity
//! 3. Peer uses the code to look up the client's identity
//! 4. Server returns the identity and deletes the code (single-use)

use rand::distributions::{Alphanumeric, DistString};
use serde::{Deserialize, Serialize};

/// A temporary rendezvous code for peer discovery.
///
/// Rendezvous codes are short, human-readable identifiers (format: "ABC-DEF-GHI") that
/// temporarily map to a client's identity on the relay server.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RendezvousCode {
    code: String,
}

impl Default for RendezvousCode {
    fn default() -> Self {
        Self::new()
    }
}

impl RendezvousCode {
    /// Generate a new random rendezvous code.
    ///
    /// Creates a 9-character code from the alphanumeric alphabet (A-Z, 0-9),
    /// formatted with hyphen separators (e.g., "ABC-DEF-GHI").
    ///
    /// # Entropy
    ///
    /// With an alphabet of 36 characters and 9 positions:
    /// - Total possibilities: 36^9 = 101,559,956,668,416
    /// - Given the 5-minute lifetime, brute-force is impractical
    ///
    /// # Examples
    ///
    /// ```
    /// use ap_relay_protocol::RendezvousCode;
    ///
    /// let code1 = RendezvousCode::new();
    /// let code2 = RendezvousCode::new();
    ///
    /// // Each call generates a different random code
    /// assert_ne!(code1.as_str(), code2.as_str());
    ///
    /// // Format is always ABC-DEF-GHI style
    /// assert_eq!(code1.as_str().len(), 11); // 9 chars + 2 hyphens
    /// assert_eq!(code1.as_str().chars().nth(3), Some('-'));
    /// assert_eq!(code1.as_str().chars().nth(7), Some('-'));
    /// ```
    pub fn new() -> Self {
        let mut rng = rand::thread_rng();

        // The code has an alphabet of size 36. With 9 characters, that's
        // 36^9 = 101,559,956,668,416 possible codes. The codes are short-lived, and the connections to the relay are rate-limited,
        // which is why this is considered sufficient.
        let code = Alphanumeric.sample_string(&mut rng, 9);
        let code = code.to_ascii_uppercase();
        // SAFETY: Alphanumeric + to_ascii_uppercase produces only ASCII characters,
        // so indexing at character boundaries is safe.
        #[allow(clippy::string_slice)]
        let code = format!("{}-{}-{}", &code[..3], &code[3..6], &code[6..]);

        RendezvousCode { code }
    }

    /// Create a rendezvous code from an existing string.
    ///
    /// Useful for:
    /// - Testing with known codes
    /// - Parsing user input
    /// - Deserializing from storage
    ///
    /// No validation is performed - the caller is responsible for ensuring
    /// the code is in the correct format.
    ///
    /// # Examples
    ///
    /// ```
    /// use ap_relay_protocol::RendezvousCode;
    ///
    /// let code = RendezvousCode::from_string("ABC-DEF-GHI".to_string());
    /// assert_eq!(code.as_str(), "ABC-DEF-GHI");
    /// ```
    pub fn from_string(code: String) -> Self {
        RendezvousCode { code }
    }

    /// Get the code string.
    ///
    /// Returns the formatted code (e.g., "ABC-DEF-GHI") that can be displayed to
    /// users or sent to the server.
    ///
    /// # Examples
    ///
    /// ```
    /// use ap_relay_protocol::RendezvousCode;
    ///
    /// let code = RendezvousCode::new();
    /// println!("Your code: {}", code.as_str());
    /// ```
    pub fn as_str(&self) -> &str {
        &self.code
    }
}

impl std::fmt::Display for RendezvousCode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.code)
    }
}