feat: DM epoch fix, federation relay, and mDNS mesh discovery
- schema: createChannel returns wasNew :Bool to elect the MLS initiator unambiguously; prevents duplicate group creation on concurrent /dm calls - core: group helpers for epoch tracking and key-package lifecycle - server: federation subsystem — mTLS QUIC server-to-server relay with Cap'n Proto RPC; enqueue/batchEnqueue relay unknown recipients to their home domain via FederationClient - server: mDNS _quicproquo._udp.local. service announcement on startup - server: storage + sql_store — identity_exists, peek/ack, federation home-server lookup helpers - client: /mesh peers REPL command (mDNS discovery, feature = "mesh") - client: MeshDiscovery — background mDNS browse with ServiceDaemon - client: was_new=false path in cmd_dm waits for peer Welcome instead of creating a duplicate initiator group - p2p: fix ALPN from quicnprotochat/p2p/1 → quicproquo/p2p/1 - workspace: re-include quicproquo-p2p in members
This commit is contained in:
@@ -127,9 +127,12 @@ pub trait Store: Send + Sync {
|
||||
/// Resolve a peer's P2P endpoint address.
|
||||
fn resolve_endpoint(&self, identity_key: &[u8]) -> Result<Option<Vec<u8>>, StorageError>;
|
||||
|
||||
/// Create a 1:1 channel between two members. Returns 16-byte channel_id (UUID).
|
||||
/// Members are stored in sorted order for deterministic lookup.
|
||||
fn create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result<Vec<u8>, StorageError>;
|
||||
/// Create a 1:1 channel between two members.
|
||||
/// Returns `(channel_id, was_new)` where `was_new` is true iff the channel was created by
|
||||
/// this call (false = it already existed). Members are stored in sorted order for deterministic
|
||||
/// lookup — both `create_channel(a, b)` and `create_channel(b, a)` return the same channel_id.
|
||||
/// The caller who receives `was_new = true` is the MLS group initiator and must send the Welcome.
|
||||
fn create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result<(Vec<u8>, bool), StorageError>;
|
||||
|
||||
/// Get the two members of a channel by channel_id (16 bytes). Returns (member_a, member_b) in sorted order.
|
||||
fn get_channel_members(&self, channel_id: &[u8]) -> Result<Option<(Vec<u8>, Vec<u8>)>, StorageError>;
|
||||
@@ -137,6 +140,7 @@ pub trait Store: Send + Sync {
|
||||
// ── Federation ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Store the home server domain for an identity key.
|
||||
#[allow(dead_code)] // federation not yet wired up
|
||||
fn store_identity_home_server(
|
||||
&self,
|
||||
identity_key: &[u8],
|
||||
@@ -157,6 +161,7 @@ pub trait Store: Send + Sync {
|
||||
) -> Result<(), StorageError>;
|
||||
|
||||
/// List all active federation peers.
|
||||
#[allow(dead_code)] // federation not yet wired up
|
||||
fn list_federation_peers(&self) -> Result<Vec<(String, bool)>, StorageError>;
|
||||
}
|
||||
|
||||
@@ -647,7 +652,7 @@ impl Store for FileBackedStore {
|
||||
Ok(map.get(identity_key).cloned())
|
||||
}
|
||||
|
||||
fn create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result<Vec<u8>, StorageError> {
|
||||
fn create_channel(&self, member_a: &[u8], member_b: &[u8]) -> Result<(Vec<u8>, bool), StorageError> {
|
||||
let (a, b) = if member_a < member_b {
|
||||
(member_a.to_vec(), member_b.to_vec())
|
||||
} else {
|
||||
@@ -655,14 +660,14 @@ impl Store for FileBackedStore {
|
||||
};
|
||||
let mut map = lock(&self.channels)?;
|
||||
if let Some((channel_id, _)) = map.iter().find(|(_, (ma, mb))| ma == &a && mb == &b) {
|
||||
return Ok(channel_id.clone());
|
||||
return Ok((channel_id.clone(), false));
|
||||
}
|
||||
let mut channel_id = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut channel_id);
|
||||
let channel_id = channel_id.to_vec();
|
||||
map.insert(channel_id.clone(), (a, b));
|
||||
self.flush_channels(&self.channels_path, &*map)?;
|
||||
Ok(channel_id)
|
||||
Ok((channel_id, true))
|
||||
}
|
||||
|
||||
fn get_channel_members(&self, channel_id: &[u8]) -> Result<Option<(Vec<u8>, Vec<u8>)>, StorageError> {
|
||||
@@ -812,12 +817,40 @@ mod tests {
|
||||
let a = vec![1u8; 32];
|
||||
let b = vec![2u8; 32];
|
||||
assert_eq!(store.get_channel_members(&[0u8; 16]).unwrap(), None);
|
||||
let id1 = store.create_channel(&a, &b).unwrap();
|
||||
let (id1, was_new1) = store.create_channel(&a, &b).unwrap();
|
||||
assert_eq!(id1.len(), 16);
|
||||
assert!(was_new1, "first call must return was_new=true");
|
||||
let members = store.get_channel_members(&id1).unwrap().unwrap();
|
||||
assert_eq!(members.0, a);
|
||||
assert_eq!(members.1, b);
|
||||
let id2 = store.create_channel(&b, &a).unwrap();
|
||||
let (id2, was_new2) = store.create_channel(&b, &a).unwrap();
|
||||
assert_eq!(id1, id2, "reversed key order must return same channel_id");
|
||||
assert!(!was_new2, "second call (reversed) must return was_new=false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_idempotent_same_direction() {
|
||||
let (_dir, store) = temp_store();
|
||||
let a = vec![3u8; 32];
|
||||
let b = vec![4u8; 32];
|
||||
let (id1, was_new1) = store.create_channel(&a, &b).unwrap();
|
||||
let (id2, was_new2) = store.create_channel(&a, &b).unwrap();
|
||||
assert_eq!(id1, id2);
|
||||
assert!(was_new1);
|
||||
assert!(!was_new2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_channel_different_pairs_get_different_ids() {
|
||||
let (_dir, store) = temp_store();
|
||||
let a = vec![5u8; 32];
|
||||
let b = vec![6u8; 32];
|
||||
let c = vec![7u8; 32];
|
||||
let (id_ab, _) = store.create_channel(&a, &b).unwrap();
|
||||
let (id_ac, _) = store.create_channel(&a, &c).unwrap();
|
||||
let (id_bc, _) = store.create_channel(&b, &c).unwrap();
|
||||
assert_ne!(id_ab, id_ac);
|
||||
assert_ne!(id_ab, id_bc);
|
||||
assert_ne!(id_ac, id_bc);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user