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:
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user