Skip to main content

parley_core/
ids.rs

1//! Identifier newtypes.
2//!
3//! All identifiers are kept as strongly-typed newtypes to prevent accidental
4//! cross-use (a `ChannelId` cannot be passed where a `MessageId` is expected).
5//! Wire encoding for binary IDs is base64url without padding (RFC 4648 §5).
6
7use base64::engine::general_purpose::URL_SAFE_NO_PAD;
8use base64::Engine as _;
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use std::fmt;
11use std::str::FromStr;
12
13use crate::error::CoreError;
14
15// ---------------------------------------------------------------------------
16// NetworkId
17// ---------------------------------------------------------------------------
18
19/// Network identifier, e.g. `"parley-mainnet"`. Format: `[a-z0-9-]{1,64}`,
20/// no leading or trailing hyphen. See spec §4.
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct NetworkId(String);
24
25impl NetworkId {
26    pub fn new(s: impl Into<String>) -> Result<Self, CoreError> {
27        let s: String = s.into();
28        validate_network_id(&s)?;
29        Ok(Self(s))
30    }
31
32    pub fn as_str(&self) -> &str {
33        &self.0
34    }
35}
36
37impl fmt::Display for NetworkId {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.write_str(&self.0)
40    }
41}
42
43impl FromStr for NetworkId {
44    type Err = CoreError;
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        Self::new(s)
47    }
48}
49
50fn validate_network_id(s: &str) -> Result<(), CoreError> {
51    let invalid = |reason: &str| CoreError::InvalidNetworkId(format!("{s:?}: {reason}"));
52    if s.is_empty() {
53        return Err(invalid("empty"));
54    }
55    if s.len() > 64 {
56        return Err(invalid("longer than 64 bytes"));
57    }
58    if s.starts_with('-') || s.ends_with('-') {
59        return Err(invalid("leading or trailing hyphen"));
60    }
61    if !s
62        .bytes()
63        .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
64    {
65        return Err(invalid("contains characters outside [a-z0-9-]"));
66    }
67    Ok(())
68}
69
70// ---------------------------------------------------------------------------
71// AgentPubkey
72// ---------------------------------------------------------------------------
73
74/// Ed25519 public key (32 bytes). Wire format is base64url-no-pad (43 chars).
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub struct AgentPubkey([u8; 32]);
77
78impl AgentPubkey {
79    /// Sentinel "author" stored for sealed-sender messages: the relay never
80    /// learns which member authored them, so it records this constant instead
81    /// of a real pubkey. The true sender is inside the MLS envelope. All-`0xFF`
82    /// is not a valid Ed25519 point used as an identity here.
83    pub const SEALED_SENDER: AgentPubkey = AgentPubkey([0xFF; 32]);
84
85    pub fn from_bytes(bytes: [u8; 32]) -> Self {
86        Self(bytes)
87    }
88
89    pub fn as_bytes(&self) -> &[u8; 32] {
90        &self.0
91    }
92}
93
94impl fmt::Display for AgentPubkey {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
97    }
98}
99
100impl FromStr for AgentPubkey {
101    type Err = CoreError;
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        let bytes = URL_SAFE_NO_PAD.decode(s)?;
104        let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
105            CoreError::InvalidAgentPubkey(format!("expected 32 bytes, got {}", v.len()))
106        })?;
107        Ok(Self(arr))
108    }
109}
110
111impl Serialize for AgentPubkey {
112    fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
113        ser.serialize_str(&self.to_string())
114    }
115}
116
117impl<'de> Deserialize<'de> for AgentPubkey {
118    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
119        let s = String::deserialize(de)?;
120        s.parse().map_err(serde::de::Error::custom)
121    }
122}
123
124// ---------------------------------------------------------------------------
125// ChannelId / MessageId — 16-byte opaque identifiers
126// ---------------------------------------------------------------------------
127
128macro_rules! opaque_id {
129    ($name:ident, $err_variant:ident, $ctx:literal) => {
130        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131        pub struct $name([u8; 16]);
132
133        impl $name {
134            pub fn from_bytes(bytes: [u8; 16]) -> Self {
135                Self(bytes)
136            }
137
138            pub fn as_bytes(&self) -> &[u8; 16] {
139                &self.0
140            }
141
142            /// Generate a fresh random identifier.
143            pub fn generate() -> Self {
144                use rand::RngCore as _;
145                let mut bytes = [0u8; 16];
146                rand::thread_rng().fill_bytes(&mut bytes);
147                Self(bytes)
148            }
149        }
150
151        impl fmt::Display for $name {
152            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153                f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
154            }
155        }
156
157        impl FromStr for $name {
158            type Err = CoreError;
159            fn from_str(s: &str) -> Result<Self, Self::Err> {
160                let bytes = URL_SAFE_NO_PAD.decode(s)?;
161                let arr: [u8; 16] = bytes.try_into().map_err(|v: Vec<u8>| {
162                    CoreError::$err_variant(format!(
163                        concat!($ctx, ": expected 16 bytes, got {}"),
164                        v.len()
165                    ))
166                })?;
167                Ok(Self(arr))
168            }
169        }
170
171        impl Serialize for $name {
172            fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
173                ser.serialize_str(&self.to_string())
174            }
175        }
176
177        impl<'de> Deserialize<'de> for $name {
178            fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
179                let s = String::deserialize(de)?;
180                s.parse().map_err(serde::de::Error::custom)
181            }
182        }
183    };
184}
185
186opaque_id!(ChannelId, InvalidChannelId, "channel_id");
187opaque_id!(MessageId, InvalidMessageId, "message_id");
188opaque_id!(BlobId, InvalidBlobId, "blob_id");
189
190impl ChannelId {
191    /// Deterministic public-feed channel id for an author: the first 16
192    /// bytes of `SHA-256("parley-feed-v1" || author)`. Lets any consumer
193    /// address an agent's feed from the pubkey alone, no lookup table.
194    #[must_use]
195    pub fn feed_for(author: &AgentPubkey) -> Self {
196        use sha2::{Digest as _, Sha256};
197        let mut hasher = Sha256::new();
198        hasher.update(b"parley-feed-v1");
199        hasher.update(author.as_bytes());
200        let digest = hasher.finalize();
201        let mut id = [0u8; 16];
202        id.copy_from_slice(&digest[..16]);
203        Self(id)
204    }
205}
206
207// ---------------------------------------------------------------------------
208// Seq
209// ---------------------------------------------------------------------------
210
211/// Monotonic per-channel message sequence number. Starts at 1, dense (no gaps).
212#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
213#[serde(transparent)]
214pub struct Seq(pub u64);
215
216impl fmt::Display for Seq {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        self.0.fmt(f)
219    }
220}
221
222// ---------------------------------------------------------------------------
223// Nonce — 16 random bytes, base64url-no-pad on the wire
224// ---------------------------------------------------------------------------
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
227pub struct Nonce([u8; 16]);
228
229impl Nonce {
230    pub fn from_bytes(bytes: [u8; 16]) -> Self {
231        Self(bytes)
232    }
233
234    pub fn as_bytes(&self) -> &[u8; 16] {
235        &self.0
236    }
237
238    pub fn generate() -> Self {
239        use rand::RngCore as _;
240        let mut bytes = [0u8; 16];
241        rand::thread_rng().fill_bytes(&mut bytes);
242        Self(bytes)
243    }
244}
245
246impl fmt::Display for Nonce {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        f.write_str(&URL_SAFE_NO_PAD.encode(self.0))
249    }
250}
251
252impl FromStr for Nonce {
253    type Err = CoreError;
254    fn from_str(s: &str) -> Result<Self, Self::Err> {
255        let bytes = URL_SAFE_NO_PAD.decode(s)?;
256        let arr: [u8; 16] = bytes.try_into().map_err(|v: Vec<u8>| {
257            CoreError::InvalidNonce(format!("expected 16 bytes, got {}", v.len()))
258        })?;
259        Ok(Self(arr))
260    }
261}
262
263impl Serialize for Nonce {
264    fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
265        ser.serialize_str(&self.to_string())
266    }
267}
268
269impl<'de> Deserialize<'de> for Nonce {
270    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
271        let s = String::deserialize(de)?;
272        s.parse().map_err(serde::de::Error::custom)
273    }
274}
275
276#[cfg(test)]
277#[allow(clippy::unwrap_used)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn network_id_validates() {
283        assert!(NetworkId::new("parley-mainnet").is_ok());
284        assert!(NetworkId::new("a").is_ok());
285        assert!(NetworkId::new("").is_err());
286        assert!(NetworkId::new("-leading").is_err());
287        assert!(NetworkId::new("trailing-").is_err());
288        assert!(NetworkId::new("UPPER").is_err());
289        assert!(NetworkId::new("under_score").is_err());
290        assert!(NetworkId::new("x".repeat(65)).is_err());
291    }
292
293    #[test]
294    fn channel_id_roundtrip() {
295        let id = ChannelId::generate();
296        let s = id.to_string();
297        assert_eq!(s.len(), 22);
298        let parsed: ChannelId = s.parse().unwrap();
299        assert_eq!(id, parsed);
300    }
301
302    #[test]
303    fn agent_pubkey_roundtrip() {
304        let pk = AgentPubkey::from_bytes([7u8; 32]);
305        let s = pk.to_string();
306        assert_eq!(s.len(), 43);
307        let parsed: AgentPubkey = s.parse().unwrap();
308        assert_eq!(pk, parsed);
309    }
310}