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:
2026-03-03 14:41:56 +01:00
parent e24497bf90
commit c8398d6cb7
27 changed files with 3375 additions and 303 deletions

View File

@@ -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);
}
}