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

@@ -457,7 +457,7 @@ impl Store for SqlStore {
.map_err(|e| StorageError::Db(e.to_string()))
}
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 {
@@ -473,7 +473,7 @@ impl Store for SqlStore {
.optional()
.map_err(|e| StorageError::Db(e.to_string()))?;
if let Some(id) = existing {
return Ok(id);
return Ok((id, false));
}
let mut channel_id = [0u8; 16];
rand::thread_rng().fill_bytes(&mut channel_id);
@@ -482,7 +482,7 @@ impl Store for SqlStore {
params![channel_id.as_slice(), a, b],
)
.map_err(|e| StorageError::Db(e.to_string()))?;
Ok(channel_id.to_vec())
Ok((channel_id.to_vec(), true))
}
fn get_channel_members(&self, channel_id: &[u8]) -> Result<Option<(Vec<u8>, Vec<u8>)>, StorageError> {
@@ -721,4 +721,107 @@ mod tests {
let b_msgs = store.fetch(&rk, b"ch-b").unwrap();
assert_eq!(b_msgs, vec![(0u64, b"b1".to_vec())]);
}
#[test]
fn create_channel_was_new_first_call() {
let store = open_in_memory();
let a = [10u8; 32];
let b = [11u8; 32];
let (id, was_new) = store.create_channel(&a, &b).unwrap();
assert_eq!(id.len(), 16, "channel_id must be 16 bytes");
assert!(was_new, "first create_channel must return was_new=true");
}
#[test]
fn create_channel_idempotent_same_direction() {
let store = open_in_memory();
let a = [12u8; 32];
let b = [13u8; 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, "repeated call must return same channel_id");
assert!(was_new1);
assert!(!was_new2, "second call must return was_new=false");
}
#[test]
fn create_channel_idempotent_reversed_direction() {
let store = open_in_memory();
let a = [14u8; 32];
let b = [15u8; 32];
let (id1, was_new1) = store.create_channel(&a, &b).unwrap();
let (id2, was_new2) = store.create_channel(&b, &a).unwrap();
assert_eq!(id1, id2, "reversed-key call must return same channel_id");
assert!(was_new1);
assert!(!was_new2, "reversed-key second call must return was_new=false");
}
#[test]
fn create_channel_different_pairs_isolated() {
let store = open_in_memory();
let a = [16u8; 32];
let b = [17u8; 32];
let c = [18u8; 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);
}
#[test]
fn create_channel_get_members_roundtrip() {
let store = open_in_memory();
let a = [20u8; 32];
let b = [21u8; 32];
let (id, _) = store.create_channel(&a, &b).unwrap();
let members = store.get_channel_members(&id).unwrap();
assert!(members.is_some(), "get_channel_members must return Some after create");
let (ma, mb) = members.unwrap();
// members stored in canonical (lex) order
let (expected_a, expected_b) = if a < b {
(a.to_vec(), b.to_vec())
} else {
(b.to_vec(), a.to_vec())
};
assert_eq!(ma, expected_a);
assert_eq!(mb, expected_b);
}
#[test]
fn get_channel_members_unknown_id_returns_none() {
let store = open_in_memory();
assert!(store.get_channel_members(&[0u8; 16]).unwrap().is_none());
}
#[test]
fn resolve_identity_key_after_store() {
let store = open_in_memory();
let ik = [30u8; 32];
store.store_user_record("carol", b"record".to_vec()).unwrap();
store.store_user_identity_key("carol", ik.to_vec()).unwrap();
let resolved = store.resolve_identity_key(&ik).unwrap();
assert_eq!(resolved, Some("carol".to_string()));
}
#[test]
fn resolve_identity_key_unknown_returns_none() {
let store = open_in_memory();
let unknown = [31u8; 32];
assert!(store.resolve_identity_key(&unknown).unwrap().is_none());
}
#[test]
fn resolve_identity_key_two_users_distinct() {
let store = open_in_memory();
let ik_a = [32u8; 32];
let ik_b = [33u8; 32];
store.store_user_record("user_a", b"ra".to_vec()).unwrap();
store.store_user_record("user_b", b"rb".to_vec()).unwrap();
store.store_user_identity_key("user_a", ik_a.to_vec()).unwrap();
store.store_user_identity_key("user_b", ik_b.to_vec()).unwrap();
assert_eq!(store.resolve_identity_key(&ik_a).unwrap(), Some("user_a".to_string()));
assert_eq!(store.resolve_identity_key(&ik_b).unwrap(), Some("user_b".to_string()));
}
}