use crate::{agents::ForAgent, Db, Storelike};
use iroh::{protocol::Router, Endpoint, NodeId};
use std::sync::OnceLock;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
fn io_err(e: impl std::fmt::Display) -> crate::errors::AtomicError {
format!("Iroh I/O error: {e}").into()
}
const ATOMIC_ALPN: &[u8] = b"atomic/1";
pub fn normalize_node_id(id: &str) -> String {
let mut s = id.trim().to_string();
if let Some(rest) = s.strip_prefix("did:ad:node:") {
s = rest.split(':').next().unwrap_or(rest).to_string();
} else if let Some(rest) = s.strip_prefix("iroh:") {
s = rest.to_string();
}
s.to_lowercase()
}
static NODE_ID: OnceLock<String> = OnceLock::new();
pub fn effective_device_name(store: &Db) -> String {
let from_db = get_device_name(store);
let raw = if !from_db.trim().is_empty() {
from_db
} else {
hostname::get()
.ok()
.and_then(|os| os.into_string().ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "Unknown".to_string())
};
let max = super::protocol::HELLO_MAX_CHARS;
if raw.chars().count() > max {
raw.chars().take(max).collect()
} else {
raw
}
}
pub fn get_node_id() -> Option<&'static str> {
NODE_ID.get().map(|s| s.as_str())
}
const IROH_SECRET_KEY: &[u8] = b"_iroh_secret_key";
const DEVICE_NAME_KEY: &[u8] = b"_device_name";
pub fn get_device_name(store: &Db) -> String {
store
.kv
.get(crate::db::trees::Tree::PluginMeta, DEVICE_NAME_KEY)
.ok()
.flatten()
.and_then(|b| String::from_utf8(b).ok())
.unwrap_or_default()
}
pub fn set_device_name(store: &Db, name: &str) {
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
DEVICE_NAME_KEY,
name.as_bytes(),
);
}
fn load_or_create_secret_key(store: &Db) -> iroh::SecretKey {
if let Ok(Some(bytes)) = store
.kv
.get(crate::db::trees::Tree::PluginMeta, IROH_SECRET_KEY)
{
if bytes.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
return iroh::SecretKey::from_bytes(&arr);
}
}
let key = iroh::SecretKey::generate(rand::rngs::OsRng);
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
IROH_SECRET_KEY,
&key.to_bytes(),
);
let _ = store.flush();
key
}
pub async fn start(store: Db) -> anyhow::Result<(NodeId, Router)> {
let secret_key = load_or_create_secret_key(&store);
let endpoint: Endpoint = Endpoint::builder()
.secret_key(secret_key)
.discovery_n0()
.discovery_local_network()
.bind()
.await?;
let node_id = endpoint.node_id();
NODE_ID.set(node_id.to_string()).ok();
ENDPOINT.set(endpoint.clone()).ok();
let relay = endpoint.home_relay();
tracing::info!("Iroh NodeID: {node_id}, waiting for relay...");
let relay_url =
tokio::time::timeout(std::time::Duration::from_secs(10), wait_for_relay(relay)).await;
match relay_url {
Ok(Some(url)) => tracing::info!("Iroh relay connected: {url}"),
Ok(None) => tracing::warn!("Iroh relay: none (direct connections only)"),
Err(_) => tracing::warn!("Iroh relay: timed out after 10s (connections may fail)"),
}
let bg_store = store.clone();
let router = Router::builder(endpoint)
.accept(ATOMIC_ALPN, AtomicHandler { store })
.spawn();
ROUTER.set(router.clone()).ok();
start_live_sync(bg_store.clone());
let auto_store = bg_store;
tokio::spawn(async move {
let my_id = normalize_node_id(get_node_id().unwrap_or_default());
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
loop {
let drive = match auto_store.get_active_drive() {
Some(d) => d,
None => {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
continue;
}
};
let peers = get_known_peers(&auto_store);
if peers.is_empty() {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
continue;
}
let mut all_connected = true;
for peer in &peers {
if normalize_node_id(&peer.node_id) == my_id {
continue;
}
let peer_key = normalize_node_id(&peer.node_id);
if live_peer_ids().contains(&peer_key) {
continue;
}
if normalize_node_id(&my_id) > peer_key {
if live_peer_ids().contains(&peer_key) {
continue;
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
if live_peer_ids().contains(&peer_key) {
continue;
}
}
all_connected = false;
tracing::info!(
"[auto_connect] connecting to {}",
&peer.node_id[..peer.node_id.len().min(12)]
);
match sync_drive_with_peer_if_needed(&peer.node_id, &drive, &auto_store).await {
Ok(count) => {
tracing::info!(
"[auto_connect] synced {count} resources, live connection established"
);
}
Err(e) => {
tracing::debug!("[auto_connect] failed: {e}");
}
}
}
if all_connected {
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
} else {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}
});
Ok((node_id, router))
}
async fn wait_for_relay(
mut watcher: iroh::watchable::Watcher<Option<iroh::RelayUrl>>,
) -> Option<iroh::RelayUrl> {
if let Ok(Some(url)) = watcher.get() {
return Some(url);
}
loop {
if watcher.updated().await.is_err() {
return None;
}
if let Ok(Some(url)) = watcher.get() {
return Some(url);
}
}
}
#[derive(Debug, Clone)]
struct AtomicHandler {
store: Db,
}
impl iroh::protocol::ProtocolHandler for AtomicHandler {
fn accept(
&self,
connection: iroh::endpoint::Connection,
) -> futures::future::BoxFuture<'static, anyhow::Result<()>> {
let store = self.store.clone();
Box::pin(async move {
let remote = connection.remote_node_id()?;
let remote_str = normalize_node_id(&remote.to_string());
tracing::info!("[accept] incoming connection from {remote_str}");
let (send, recv) = match connection.accept_bi().await {
Ok(pair) => pair,
Err(e) => {
tracing::info!("[accept] connection closed from {remote}: {e}");
return Ok(());
}
};
let store_clone = store.clone();
let remote_id = remote_str.clone();
match handle_stream(send, recv, store_clone, remote_id).await {
Ok(imported) => {
push_sync_event(&remote_str, imported);
}
Err(e) => {
tracing::warn!("[accept] stream error: {e}");
}
}
Ok(())
})
}
}
static ENDPOINT: OnceLock<Endpoint> = OnceLock::new();
static ROUTER: OnceLock<Router> = OnceLock::new();
use std::collections::HashMap;
use std::sync::Mutex;
static LIVE_PEERS: Mutex<Option<HashMap<String, tokio::sync::mpsc::Sender<Vec<u8>>>>> =
Mutex::new(None);
static LIVE_CONNECTIONS: Mutex<Option<Vec<iroh::endpoint::Connection>>> = Mutex::new(None);
static LIVE_PEER_NAMES: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
pub fn set_live_peer_name(peer_id: &str, name: &str) {
if name.is_empty() {
return;
}
if let Ok(mut guard) = LIVE_PEER_NAMES.lock() {
guard
.get_or_insert_with(HashMap::new)
.insert(normalize_node_id(peer_id), name.to_string());
}
}
pub fn live_peer_name(peer_id: &str) -> Option<String> {
LIVE_PEER_NAMES
.lock()
.ok()
.and_then(|guard| guard.as_ref()?.get(&normalize_node_id(peer_id)).cloned())
}
pub fn live_peer_count() -> usize {
LIVE_PEERS
.lock()
.ok()
.and_then(|map| map.as_ref().map(|m| m.len()))
.unwrap_or(0)
}
pub fn live_peer_ids() -> Vec<String> {
LIVE_PEERS
.lock()
.ok()
.and_then(|map| map.as_ref().map(|m| m.keys().cloned().collect()))
.unwrap_or_default()
}
pub fn remove_live_peer(peer_id: &str) {
remove_live_peer_inner(peer_id, true);
}
fn remove_live_peer_quiet(peer_id: &str) {
remove_live_peer_inner(peer_id, false);
}
fn remove_live_peer_inner(peer_id: &str, notify: bool) {
let key = normalize_node_id(peer_id);
let mut removed = false;
if let Ok(mut guard) = LIVE_PEERS.lock() {
if let Some(map) = guard.as_mut() {
removed = map.remove(&key).is_some();
}
}
if removed {
tracing::info!("[live] removed peer {}", &key[..key.len().min(12)]);
if notify {
push_event(&key, 0, "disconnected");
}
}
}
static LAST_EVENT_MS: OnceLock<std::sync::Mutex<std::collections::HashMap<(String, String), u64>>> =
OnceLock::new();
const EVENT_DEBOUNCE_MS: u64 = 15_000;
fn last_event_ms() -> &'static std::sync::Mutex<std::collections::HashMap<(String, String), u64>> {
LAST_EVENT_MS.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
pub fn is_importing() -> bool {
super::ws_apply::is_importing()
}
fn encode_live_update_wire_msg(subject_key: &str, loro_bytes: &[u8]) -> Vec<u8> {
let frame = super::protocol::encode_update(0, 0, subject_key, None, loro_bytes);
let len = frame.len() as u32;
let mut msg = Vec::with_capacity(4 + frame.len());
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(&frame);
msg
}
fn own_agent_update_frame(store: &Db) -> Option<Vec<u8>> {
let agent = store.get_default_agent().ok()?;
let key = crate::Subject::from_raw(
&agent.subject.to_string(),
store.get_base_domain().as_deref(),
)
.pure_id();
let snapshot = store
.kv
.get(crate::db::trees::Tree::LoroSnapshots, key.as_bytes())
.ok()
.flatten()
.filter(|b| !b.is_empty())?;
Some(encode_live_update_wire_msg(&key, &snapshot))
}
fn is_our_agent_subject(store: &Db, subject: &str) -> bool {
if !subject.starts_with("did:ad:agent:") {
return false;
}
let Ok(agent) = store.get_default_agent() else {
return false;
};
let ours = crate::Subject::from_raw(
&agent.subject.to_string(),
store.get_base_domain().as_deref(),
)
.pure_id();
crate::Subject::from_raw(subject, store.get_base_domain().as_deref()).pure_id() == ours
}
fn send_live_update_wire_msg(msg: Vec<u8>) {
let mut dead_peers = Vec::new();
let peers = LIVE_PEERS.lock().unwrap();
if let Some(map) = peers.as_ref() {
for (peer_id, tx) in map {
match tx.try_send(msg.clone()) {
Ok(_) => {}
Err(tokio::sync::mpsc::error::TrySendError::Full(m)) => {
let peer = peer_id.clone();
let tx_retry = tx.clone();
tokio::spawn(async move {
if tx_retry.send(m).await.is_err() {
tracing::warn!(
"[live_sync] retry send failed for {}",
&peer[..peer.len().min(12)]
);
}
});
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
dead_peers.push(peer_id.clone());
}
}
}
}
drop(peers);
for peer_id in dead_peers {
remove_live_peer(&peer_id);
}
}
pub fn broadcast_live_update(subject_key: &str, loro_bytes: &[u8]) {
if loro_bytes.is_empty() || super::ws_apply::is_importing() {
return;
}
let msg = encode_live_update_wire_msg(subject_key, loro_bytes);
send_live_update_wire_msg(msg);
}
fn start_live_sync(store: Db) {
{
let mut map = LIVE_PEERS.lock().unwrap();
if map.is_none() {
*map = Some(HashMap::new());
}
}
{
let mut conns = LIVE_CONNECTIONS.lock().unwrap();
if conns.is_none() {
*conns = Some(Vec::new());
}
}
tokio::spawn(async move {
let mut rx = store.subscribe_events();
tracing::info!("[live_sync] push loop started");
loop {
let event = match rx.recv().await {
Ok(e) => e,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(_) => {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
rx = store.subscribe_events();
continue;
}
};
if super::ws_apply::is_importing() {
continue;
}
let subject_key = match &event {
crate::DbEvent::Changed { subject, .. }
| crate::DbEvent::Destroyed { subject, .. } => subject.pure_id(),
_ => continue,
};
let loro_bytes: Option<Vec<u8>> = match &event {
crate::DbEvent::Changed {
delta: Some(delta), ..
} if !delta.is_empty() => Some(delta.clone()),
crate::DbEvent::Changed { .. } => store
.kv
.get(
crate::db::trees::Tree::LoroSnapshots,
subject_key.as_bytes(),
)
.ok()
.flatten()
.filter(|b| !b.is_empty()),
crate::DbEvent::Destroyed { .. } => {
let frame = super::protocol::encode_destroy(0, &subject_key);
let len = frame.len() as u32;
let mut msg = Vec::with_capacity(4 + frame.len());
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(&frame);
send_live_update_wire_msg(msg);
continue;
}
_ => None,
};
if let Some(bytes) = loro_bytes {
let msg = encode_live_update_wire_msg(&subject_key, &bytes);
send_live_update_wire_msg(msg);
}
}
});
}
#[cfg(test)]
pub(crate) async fn admitted_for_drive_for_test(
store: &Db,
agent: &ForAgent,
drive_subject: &str,
trust_owned: bool,
cache: &mut std::collections::HashMap<String, bool>,
) -> bool {
admitted_for_drive(store, agent, drive_subject, trust_owned, cache).await
}
async fn admitted_for_drive(
store: &Db,
agent: &ForAgent,
drive_subject: &str,
trust_owned: bool,
cache: &mut std::collections::HashMap<String, bool>,
) -> bool {
if let Some(&verdict) = cache.get(drive_subject) {
return verdict;
}
if !store.sync_policy().admit_drive_write(drive_subject) {
cache.insert(drive_subject.to_string(), false);
return false;
}
let drive_subj = crate::Subject::from_raw(drive_subject, store.get_base_domain().as_deref());
let verdict = match store.get_resource(&drive_subj).await {
Ok(drive_resource) => {
super::engine::may_accept_drive_write(store, &drive_resource, agent, trust_owned).await
}
Err(_) => true,
};
cache.insert(drive_subject.to_string(), verdict);
verdict
}
async fn apply_peer_remove(
store: &Db,
agent: &ForAgent,
subject: &str,
trust_owned: bool,
drive_cache: &mut std::collections::HashMap<String, bool>,
) {
match super::ws_apply::resolve_destroy_drive(store, subject).await {
Some(drive_subject) => {
if admitted_for_drive(store, agent, &drive_subject, trust_owned, drive_cache).await {
let _ = super::ws_apply::apply_destroy_checked(store, subject).await;
} else {
tracing::warn!(
"[sync] rejected SYNC_DIFF remove for {} from peer: not admitted for drive {}",
&subject[..subject.len().min(30)],
&drive_subject[..drive_subject.len().min(30)]
);
}
}
None => {
let _ = super::ws_apply::apply_destroy_checked(store, subject).await;
}
}
}
fn invalidate_drive_cache_on_identity_change(
agent: &ForAgent,
previous: &ForAgent,
drive_cache: &mut std::collections::HashMap<String, bool>,
) {
if agent != previous {
drive_cache.clear();
}
}
fn register_live_peer(
peer_id: String,
mut send: iroh::endpoint::SendStream,
mut recv: iroh::endpoint::RecvStream,
store: Db,
agent: ForAgent,
initiated_by_us: bool,
) {
let key = normalize_node_id(&peer_id);
let (tx, mut rx) = tokio::sync::mpsc::channel::<Vec<u8>>(64);
let tx_for_read = tx.clone();
let is_new_peer = {
let mut map = LIVE_PEERS.lock().unwrap();
if let Some(m) = map.as_mut() {
let replacing = m.contains_key(&key);
if replacing {
tracing::info!(
"[live] replacing existing connection to {}",
&key[..key.len().min(12)]
);
}
m.insert(key.clone(), tx);
!replacing
} else {
false
}
};
let peer_short = key[..key.len().min(12)].to_string();
tracing::info!("[live] registered peer {peer_short} (new={is_new_peer})");
push_event(&key, 0, "connected");
if let Some(frame) = own_agent_update_frame(&store) {
let _ = tx_for_read.try_send(frame);
}
let write_peer_id = key.clone();
tokio::spawn(async move {
tracing::info!(
"[live] write loop started for {}",
&write_peer_id[..write_peer_id.len().min(12)]
);
while let Some(msg) = rx.recv().await {
match send.write_all(&msg).await {
Ok(_) => {
tracing::trace!(
"[live] wrote {} bytes to {}",
msg.len(),
&write_peer_id[..write_peer_id.len().min(12)]
);
}
Err(e) => {
tracing::warn!(
"[live] write failed to {}: {e}",
&write_peer_id[..write_peer_id.len().min(12)]
);
break;
}
}
}
tracing::info!(
"[live] write loop ended for {}",
&write_peer_id[..write_peer_id.len().min(12)]
);
remove_live_peer(&write_peer_id);
});
let read_peer_id = key.clone();
tokio::spawn(async move {
tracing::info!(
"[live] read loop started for {} as {agent:?}",
&read_peer_id[..read_peer_id.len().min(12)]
);
let mut agent = agent;
let mut drive_cache: std::collections::HashMap<String, bool> =
std::collections::HashMap::new();
loop {
let len = match recv.read_u32().await {
Ok(n) => {
tracing::trace!(
"[live] received frame {} bytes from {}",
n,
&read_peer_id[..read_peer_id.len().min(12)]
);
n as usize
}
Err(e) => {
tracing::info!(
"[live] read error from {}: {e}",
&read_peer_id[..read_peer_id.len().min(12)]
);
break;
}
};
let frame_cap = if matches!(agent, ForAgent::Public) {
super::protocol::IROH_PREAUTH_FRAME_MAX_BYTES
} else {
super::protocol::IROH_FRAME_MAX_BYTES
};
if len == 0 || len > frame_cap {
break;
}
let mut buf = vec![0u8; len];
if recv.read_exact(&mut buf).await.is_err() {
break;
}
if buf.is_empty() {
continue;
}
if buf[0] == super::protocol::tag::DESTROY {
if buf.len() > 3 {
let subject = std::str::from_utf8(&buf[3..])
.unwrap_or_default()
.to_string();
match super::ws_apply::resolve_destroy_drive(&store, &subject).await {
Some(drive_subject) => {
if admitted_for_drive(
&store,
&agent,
&drive_subject,
initiated_by_us,
&mut drive_cache,
)
.await
{
let _ =
super::ws_apply::apply_destroy_checked(&store, &subject).await;
} else {
tracing::warn!(
"[live] rejected DESTROY for {} from {}: not admitted for drive {}",
&subject[..subject.len().min(20)],
&read_peer_id[..read_peer_id.len().min(12)],
&drive_subject[..drive_subject.len().min(20)]
);
}
}
None => {
let _ = super::ws_apply::apply_destroy_checked(&store, &subject).await;
}
}
}
continue;
}
if buf[0] == super::protocol::tag::UPDATE {
if let Some(decoded) = super::protocol::decode_update(&buf[1..]) {
if !decoded.loro_bytes.is_empty() {
if let Some(resolved) = super::ws_apply::resolve_update(
&store,
&decoded.subject,
&decoded.loro_bytes,
)
.await
{
if admitted_for_drive(
&store,
&agent,
&resolved.drive_subject,
initiated_by_us,
&mut drive_cache,
)
.await
{
let own_agent = is_our_agent_subject(&store, &decoded.subject);
if own_agent {
super::ws_apply::set_importing(true);
}
let _ = super::ws_apply::persist_update(
&store,
&decoded.subject,
resolved,
)
.await;
if own_agent {
super::ws_apply::set_importing(false);
}
tracing::trace!(
"[live] imported update for {} from {}",
&decoded.subject[..decoded.subject.len().min(20)],
&read_peer_id[..read_peer_id.len().min(12)]
);
} else {
tracing::warn!(
"[live] rejected UPDATE for {} from {}: not admitted for drive {}",
&decoded.subject[..decoded.subject.len().min(20)],
&read_peer_id[..read_peer_id.len().min(12)],
&resolved.drive_subject[..resolved.drive_subject.len().min(20)]
);
}
}
}
}
continue;
}
let agent_before_frame = agent.clone();
let responses = super::engine::handle_frame(&buf, &store, &mut agent).await;
invalidate_drive_cache_on_identity_change(
&agent,
&agent_before_frame,
&mut drive_cache,
);
for response in responses {
let mut framed = Vec::with_capacity(4 + response.len());
framed.extend_from_slice(&(response.len() as u32).to_be_bytes());
framed.extend_from_slice(&response);
if tx_for_read.send(framed).await.is_err() {
tracing::warn!(
"[live] response channel closed for {}, dropping responses",
&read_peer_id[..read_peer_id.len().min(12)]
);
break;
}
}
}
remove_live_peer(&read_peer_id);
});
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SyncEvent {
pub remote_node_id: String,
pub resources_imported: usize,
pub timestamp: u64,
#[serde(default = "default_event_kind")]
pub kind: String,
}
#[allow(dead_code)] fn default_event_kind() -> String {
"sync".into()
}
static SYNC_EVENT_TX: OnceLock<tokio::sync::broadcast::Sender<SyncEvent>> = OnceLock::new();
fn get_event_tx() -> &'static tokio::sync::broadcast::Sender<SyncEvent> {
SYNC_EVENT_TX.get_or_init(|| tokio::sync::broadcast::channel(32).0)
}
fn push_sync_event(remote_node_id: &str, resources_imported: usize) {
push_event(remote_node_id, resources_imported, "sync");
}
fn push_event(remote_node_id: &str, resources_imported: usize, kind: &str) {
let now = crate::utils::now() as u64;
let key = (remote_node_id.to_string(), kind.to_string());
if kind != "connected" {
if let Ok(mut last) = last_event_ms().lock() {
if let Some(&prev) = last.get(&key) {
if now.saturating_sub(prev) < EVENT_DEBOUNCE_MS {
tracing::debug!(
"[live] debounced {kind} for {}",
&remote_node_id[..remote_node_id.len().min(12)]
);
return;
}
}
last.insert(key, now);
}
}
let event = SyncEvent {
remote_node_id: remote_node_id.to_string(),
resources_imported,
timestamp: now,
kind: kind.to_string(),
};
let _ = get_event_tx().send(event);
}
pub fn poll_sync_events() -> Vec<SyncEvent> {
let mut rx = get_event_tx().subscribe();
let mut events = Vec::new();
while let Ok(e) = rx.try_recv() {
events.push(e);
}
events
}
pub async fn wait_for_sync_event() -> SyncEvent {
let mut rx = get_event_tx().subscribe();
loop {
match rx.recv().await {
Ok(event) => return event,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(_) => {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
rx = get_event_tx().subscribe();
}
}
}
}
pub async fn wait_for_peer_count_change(current: usize) -> usize {
let mut rx = get_event_tx().subscribe();
loop {
let count = live_peer_count();
if count != current {
return count;
}
match tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv()).await {
Ok(Ok(_)) => {
let count = live_peer_count();
if count != current {
return count;
}
}
Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
Ok(Err(_)) => {
rx = get_event_tx().subscribe();
}
Err(_) => {} }
}
}
pub async fn sync_drive_with_peer(
remote_node_id: &str,
drive: &str,
store: &Db,
) -> crate::errors::AtomicResult<usize> {
sync_drive_with_peer_forced(remote_node_id, drive, store, true).await
}
pub async fn sync_drive_with_peer_outcome(
remote_node_id: &str,
drive: &str,
store: &Db,
) -> crate::errors::AtomicResult<PeerSyncOutcome> {
let endpoint = ENDPOINT
.get()
.ok_or("Iroh peer not started. Call start() first.")?;
sync_drive_with_peer_using_outcome(endpoint, remote_node_id, drive, store, true).await
}
pub async fn sync_drive_with_peer_if_needed(
remote_node_id: &str,
drive: &str,
store: &Db,
) -> crate::errors::AtomicResult<usize> {
sync_drive_with_peer_forced(remote_node_id, drive, store, false).await
}
async fn sync_drive_with_peer_forced(
remote_node_id: &str,
drive: &str,
store: &Db,
force: bool,
) -> crate::errors::AtomicResult<usize> {
let endpoint = ENDPOINT
.get()
.ok_or("Iroh peer not started. Call start() first.")?;
sync_drive_with_peer_using(endpoint, remote_node_id, drive, store, force).await
}
#[derive(Debug, Clone)]
pub struct PeerSyncOutcome {
pub count: usize,
pub pushed: usize,
pub in_sync: bool,
pub peer_name: Option<String>,
}
pub async fn sync_drive_with_peer_using(
endpoint: &Endpoint,
remote_node_id: &str,
drive: &str,
store: &Db,
force: bool,
) -> crate::errors::AtomicResult<usize> {
sync_drive_with_peer_using_outcome(endpoint, remote_node_id, drive, store, force)
.await
.map(|o| o.count)
}
pub async fn sync_drive_with_peer_using_outcome(
endpoint: &Endpoint,
remote_node_id: &str,
drive: &str,
store: &Db,
force: bool,
) -> crate::errors::AtomicResult<PeerSyncOutcome> {
let remote_key = normalize_node_id(remote_node_id);
let node_id: NodeId = remote_key
.parse()
.map_err(|e| format!("Invalid NodeID '{remote_node_id}': {e}"))?;
if !force && live_peer_ids().contains(&remote_key) {
tracing::debug!(
"[sync] already live with {}, skipping bulk reconnect",
&remote_key[..remote_key.len().min(12)]
);
return Ok(PeerSyncOutcome {
count: 0,
pushed: 0,
in_sync: true,
peer_name: None,
});
}
if force && live_peer_ids().contains(&remote_key) {
remove_live_peer_quiet(&remote_key);
}
let my_node_id = endpoint.node_id();
let my_relay = endpoint.home_relay();
tracing::info!(
"[sync] my NodeID: {}, relay: {:?}, connecting to: {}, drive: {}",
&my_node_id.to_string()[..16],
my_relay.get(),
&node_id.to_string()[..node_id.to_string().len().min(16)],
&drive[..drive.len().min(20)],
);
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(20);
let remote_short = &node_id.to_string()[..node_id.to_string().len().min(16)];
let dial = dial_target(store, node_id);
let conn =
match tokio::time::timeout(CONNECT_TIMEOUT, endpoint.connect(dial, ATOMIC_ALPN)).await {
Ok(Ok(c)) => c,
Ok(Err(e)) => {
tracing::error!("[sync] connect failed to {remote_short}: {e}");
return Err(format!("Iroh connect to {remote_short} failed: {e}").into());
}
Err(_) => {
tracing::error!("[sync] connect timed out to {remote_short}");
return Err(format!(
"Iroh connect to {remote_short} timed out after {}s. \
Is the other device online, on the network, and running the app?",
CONNECT_TIMEOUT.as_secs()
)
.into());
}
};
tracing::info!("[sync] connected! Opening bi stream...");
let (mut send, mut recv) = conn.open_bi().await.map_err(|e| {
tracing::error!("[sync] open_bi failed: {e}");
format!("Iroh open_bi failed: {e}")
})?;
tracing::info!("[sync] bi stream open, sending AUTH...");
let agent = store.get_default_agent()?;
let auth_frame = super::protocol::encode_auth(&agent, drive)?;
send.write_u32(auth_frame.len() as u32)
.await
.map_err(io_err)?;
send.write_all(&auth_frame).await.map_err(io_err)?;
let auth_len = match recv.read_u32().await {
Ok(n) => n as usize,
Err(e) => return Err(format!("Failed to read auth response: {e}").into()),
};
if auth_len > super::protocol::IROH_PREAUTH_FRAME_MAX_BYTES {
return Err(format!(
"Auth response frame too large: {auth_len} bytes (max {})",
super::protocol::IROH_PREAUTH_FRAME_MAX_BYTES
)
.into());
}
let mut auth_buf = vec![0u8; auth_len];
recv.read_exact(&mut auth_buf).await.map_err(io_err)?;
if auth_buf.is_empty() || auth_buf[0] != super::protocol::tag::AUTH_OK {
let msg = if auth_buf.len() > 5 {
std::str::from_utf8(&auth_buf[5..]).unwrap_or("unknown error")
} else {
"auth rejected"
};
return Err(format!("Authentication failed: {msg}").into());
}
tracing::info!("Authenticated with peer");
let hello_frame = super::protocol::encode_hello(&effective_device_name(store));
send.write_u32(hello_frame.len() as u32)
.await
.map_err(io_err)?;
send.write_all(&hello_frame).await.map_err(io_err)?;
let mut peer_display_name: Option<String> = None;
let mut remote_agent = ForAgent::Public;
let mut drive_cache: std::collections::HashMap<String, bool> = std::collections::HashMap::new();
let drive_subject = crate::Subject::from_raw(drive, store.get_base_domain().as_deref());
let drive_subjects = super::engine::collect_drive_subjects(store, &drive_subject).await;
let vvs = super::engine::build_drive_vvs(store, &drive_subjects);
let drive_hash = super::engine::compute_drive_hash(&vvs);
let mut peer_set = std::collections::BTreeSet::new();
for vv in vvs.values() {
for peer_id in vv.keys() {
peer_set.insert(peer_id.clone());
}
}
let peers: Vec<String> = peer_set.into_iter().collect();
let peer_index: std::collections::HashMap<&str, usize> = peers
.iter()
.enumerate()
.map(|(i, p)| (p.as_str(), i))
.collect();
let mut resources: std::collections::HashMap<String, Vec<i32>> =
std::collections::HashMap::new();
for (subject, vv) in &vvs {
let mut counters = vec![0i32; peers.len()];
for (pid, &counter) in vv {
if let Some(&idx) = peer_index.get(pid.as_str()) {
counters[idx] = counter;
}
}
resources.insert(subject.clone(), counters);
}
let sync_frame = super::protocol::encode_sync(drive, &drive_hash, &peers, &resources);
send.write_u32(sync_frame.len() as u32)
.await
.map_err(io_err)?;
send.write_all(&sync_frame).await.map_err(io_err)?;
let mut total_imported = 0;
let mut total_pushed = 0usize;
let mut acked_in_sync = false;
let mut pull_subjects: Vec<String> = Vec::new();
while let Ok(n) = recv.read_u32().await {
let len = n as usize;
if len == 0 || len > super::protocol::IROH_FRAME_MAX_BYTES {
break;
}
let mut buf = vec![0u8; len];
recv.read_exact(&mut buf).await.map_err(io_err)?;
if buf.is_empty() {
break;
}
let tag = buf[0];
let payload = &buf[1..];
match tag {
super::protocol::tag::HELLO => {
if peer_display_name.is_none() {
peer_display_name = super::protocol::decode_hello(payload);
if let Some(name) = &peer_display_name {
tracing::info!(
"[sync] peer {} introduced itself as \"{}\"",
&remote_key[..remote_key.len().min(12)],
name
);
if !name.is_empty() {
add_known_peer(store, &remote_key, name);
}
}
}
continue;
}
super::protocol::tag::SYNC_OK => {
tracing::info!("Peer says drive {drive} is in sync");
acked_in_sync = true;
break;
}
super::protocol::tag::SYNC_DIFF => {
if let Some(diff) = super::protocol::decode_sync_diff(payload) {
tracing::info!(
"SYNC_DIFF: server pushes {}, server pulls {}, remove {}",
diff.push.len(),
diff.pull.len(),
diff.remove.len()
);
for subject in &diff.remove {
apply_peer_remove(store, &remote_agent, subject, true, &mut drive_cache)
.await;
}
pull_subjects = diff.pull.clone();
if diff.push.is_empty() {
if !diff.pull.is_empty() {
let entries = super::engine::collect_readable_snapshots(
store,
&remote_agent,
&diff.pull,
)
.await;
if !entries.is_empty() {
let refs: Vec<(&str, &[u8])> = entries
.iter()
.map(|(s, b)| (s.as_str(), b.as_slice()))
.collect();
for chunk in super::protocol::encode_sync_push_chunks(drive, &refs)
{
send.write_u32(chunk.len() as u32).await.map_err(io_err)?;
send.write_all(&chunk).await.map_err(io_err)?;
}
total_pushed += entries.len();
tracing::info!("Pushed {} resources to peer", entries.len());
}
}
break;
}
}
}
super::protocol::tag::SYNC_PUSH => {
let mut last_chunk = false;
if let Some(push) = super::protocol::decode_sync_push(payload) {
last_chunk = push.last;
let (count, blob_requests) =
super::engine::import_sync_push(&push, store, &remote_agent, true).await;
total_imported += count;
for req_frame in blob_requests {
send.write_u32(req_frame.len() as u32)
.await
.map_err(io_err)?;
send.write_all(&req_frame).await.map_err(io_err)?;
}
}
if !last_chunk {
continue;
}
if !pull_subjects.is_empty() {
let entries = super::engine::collect_readable_snapshots(
store,
&remote_agent,
&pull_subjects,
)
.await;
if !entries.is_empty() {
let refs: Vec<(&str, &[u8])> = entries
.iter()
.map(|(s, b)| (s.as_str(), b.as_slice()))
.collect();
for chunk in super::protocol::encode_sync_push_chunks(drive, &refs) {
send.write_u32(chunk.len() as u32).await.map_err(io_err)?;
send.write_all(&chunk).await.map_err(io_err)?;
}
total_pushed += entries.len();
tracing::info!("Pushed {} resources back to peer", entries.len());
}
}
break;
}
super::protocol::tag::ERROR => {
let msg =
std::str::from_utf8(payload.get(4..).unwrap_or(&[])).unwrap_or("unknown error");
tracing::warn!("Peer returned error: {msg}");
break;
}
super::protocol::tag::AUTH => {
if let Ok(json) = std::str::from_utf8(payload) {
match serde_json::from_str::<crate::authentication::AuthValues>(json) {
Ok(auth) => {
match crate::authentication::get_agent_from_auth_values_and_check(
Some(auth),
store,
)
.await
{
Ok(a) => {
tracing::info!(
"[sync] peer {} authenticated back as {a:?}",
&remote_key[..remote_key.len().min(12)]
);
invalidate_drive_cache_on_identity_change(
&a,
&remote_agent,
&mut drive_cache,
);
remote_agent = a;
}
Err(e) => {
tracing::debug!("[sync] peer's auth-back rejected: {e}");
}
}
}
Err(e) => {
tracing::debug!("[sync] invalid auth-back JSON from peer: {e}");
}
}
}
}
_ => {
tracing::debug!("Unexpected frame tag from peer: 0x{tag:02x}");
}
}
}
tracing::info!(
"sync_drive_with_peer: imported {total_imported} resources from {remote_node_id}"
);
if total_imported == 0 && total_pushed == 0 && !acked_in_sync {
return Err(format!(
"Connected to that device, but nothing synced: it shared nothing \
readable with you and took nothing of yours ({remote_agent})."
)
.into());
}
mark_peer_synced(store, &remote_key);
if let Some(info) = endpoint.remote_info(node_id) {
let addr: iroh::NodeAddr = info.into();
remember_peer_addr(
store,
&remote_key,
addr.relay_url.map(|u| u.to_string()),
addr.direct_addresses
.iter()
.map(|a| a.to_string())
.collect(),
);
}
tracing::info!(
"[live] transitioning to live mode with {}",
&remote_node_id[..remote_node_id.len().min(12)]
);
{
let mut conns = LIVE_CONNECTIONS.lock().unwrap();
if let Some(v) = conns.as_mut() {
v.push(conn);
}
}
register_live_peer(
remote_key.clone(),
send,
recv,
store.clone(),
remote_agent,
true,
);
if store.get_active_drive().as_deref() != Some(drive) {
if let Err(e) = store.set_active_drive(drive) {
tracing::warn!("[sync] could not remember the drive to reconnect to: {e}");
}
}
Ok(PeerSyncOutcome {
count: total_imported,
pushed: total_pushed,
in_sync: acked_in_sync,
peer_name: peer_display_name,
})
}
const KNOWN_PEERS_KEY: &[u8] = b"_iroh_known_peers_v2";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KnownPeer {
pub node_id: String,
pub name: String,
#[serde(default)]
pub last_synced: Option<i64>,
#[serde(default)]
pub relay_url: Option<String>,
#[serde(default)]
pub direct_addrs: Vec<String>,
}
pub fn get_known_peers(store: &Db) -> Vec<KnownPeer> {
if let Ok(Some(bytes)) = store
.kv
.get(crate::db::trees::Tree::PluginMeta, KNOWN_PEERS_KEY)
{
serde_json::from_slice(&bytes).unwrap_or_default()
} else {
vec![]
}
}
pub fn add_known_peer(store: &Db, node_id: &str, name: &str) {
let key = normalize_node_id(node_id);
let mut peers = get_known_peers(store);
if let Some(existing) = peers
.iter_mut()
.find(|p| normalize_node_id(&p.node_id) == key)
{
if !name.is_empty() {
existing.name = name.to_string();
}
} else {
peers.push(KnownPeer {
node_id: key,
name: name.to_string(),
last_synced: None,
relay_url: None,
direct_addrs: Vec::new(),
});
}
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
KNOWN_PEERS_KEY,
&serde_json::to_vec(&peers).unwrap_or_default(),
);
let _ = store.flush();
}
pub fn remember_peer_addr(
store: &Db,
node_id: &str,
relay_url: Option<String>,
direct_addrs: Vec<String>,
) {
if relay_url.is_none() && direct_addrs.is_empty() {
return;
}
let key = normalize_node_id(node_id);
let mut peers = get_known_peers(store);
let entry = if let Some(existing) = peers
.iter_mut()
.find(|p| normalize_node_id(&p.node_id) == key)
{
existing
} else {
peers.push(KnownPeer {
node_id: key.clone(),
name: String::new(),
last_synced: None,
relay_url: None,
direct_addrs: Vec::new(),
});
peers.last_mut().unwrap()
};
if relay_url.is_some() {
entry.relay_url = relay_url;
}
if !direct_addrs.is_empty() {
entry.direct_addrs = direct_addrs;
}
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
KNOWN_PEERS_KEY,
&serde_json::to_vec(&peers).unwrap_or_default(),
);
let _ = store.flush();
}
fn dial_target(store: &Db, node_id: NodeId) -> iroh::NodeAddr {
let key = node_id.to_string();
for peer in get_known_peers(store) {
if normalize_node_id(&peer.node_id) != normalize_node_id(&key) {
continue;
}
let mut addr = iroh::NodeAddr::new(node_id);
if let Some(relay) = peer.relay_url.as_deref().and_then(|s| s.parse().ok()) {
addr = addr.with_relay_url(relay);
}
let socks: Vec<std::net::SocketAddr> = peer
.direct_addrs
.iter()
.filter_map(|s| s.parse().ok())
.collect();
if !socks.is_empty() {
addr = addr.with_direct_addresses(socks);
}
return addr;
}
iroh::NodeAddr::new(node_id)
}
pub fn mark_peer_synced(store: &Db, node_id: &str) {
let key = normalize_node_id(node_id);
let mut peers = get_known_peers(store);
let now = crate::utils::now();
if let Some(existing) = peers
.iter_mut()
.find(|p| normalize_node_id(&p.node_id) == key)
{
existing.last_synced = Some(now);
} else {
peers.push(KnownPeer {
node_id: key,
name: String::new(),
last_synced: Some(now),
relay_url: None,
direct_addrs: Vec::new(),
});
}
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
KNOWN_PEERS_KEY,
&serde_json::to_vec(&peers).unwrap_or_default(),
);
let _ = store.flush();
}
pub fn remove_known_peer(store: &Db, node_id: &str) {
let key = normalize_node_id(node_id);
let mut peers = get_known_peers(store);
peers.retain(|p| normalize_node_id(&p.node_id) != key);
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
KNOWN_PEERS_KEY,
&serde_json::to_vec(&peers).unwrap_or_default(),
);
let _ = store.flush();
}
async fn handle_stream(
mut send: iroh::endpoint::SendStream,
mut recv: iroh::endpoint::RecvStream,
store: Db,
remote_id: String,
) -> anyhow::Result<usize> {
let remote_key = normalize_node_id(&remote_id);
let mut agent = ForAgent::Public;
let mut total_imported = 0;
let mut sent_sync_ok = false;
let mut hello_sent = false;
let mut auth_sent = false;
let mut peer_display_name: Option<String> = None;
while let Ok(n) = recv.read_u32().await {
let len = n as usize;
let frame_cap = if matches!(agent, ForAgent::Public) {
super::protocol::IROH_PREAUTH_FRAME_MAX_BYTES
} else {
super::protocol::IROH_FRAME_MAX_BYTES
};
if len == 0 || len > frame_cap {
break;
}
let mut buf = vec![0u8; len];
recv.read_exact(&mut buf).await.map_err(io_err)?;
if !buf.is_empty() && buf[0] == super::protocol::tag::HELLO {
if peer_display_name.is_none() {
peer_display_name = super::protocol::decode_hello(&buf[1..]);
if let Some(name) = &peer_display_name {
tracing::info!(
"[accept] peer {} introduced itself as \"{}\"",
&remote_key[..remote_key.len().min(12)],
name
);
set_live_peer_name(&remote_key, name);
}
}
continue;
}
if !buf.is_empty() && buf[0] == super::protocol::tag::SYNC_PUSH {
if let Some(push) = super::protocol::decode_sync_push(&buf[1..]) {
total_imported += push.entries.len();
}
}
let responses = super::engine::handle_frame(&buf, &store, &mut agent).await;
let just_authed = !buf.is_empty()
&& buf[0] == super::protocol::tag::AUTH
&& responses
.iter()
.any(|r| !r.is_empty() && r[0] == super::protocol::tag::AUTH_OK);
if just_authed {
tracing::info!(
"[accept] {} authenticated as {agent} — serving what they may read",
&remote_key[..remote_key.len().min(12)]
);
}
let client_pushed = !buf.is_empty() && buf[0] == super::protocol::tag::SYNC_PUSH;
let sync_ok = responses
.iter()
.any(|r| !r.is_empty() && r[0] == super::protocol::tag::SYNC_OK);
if sync_ok {
sent_sync_ok = true;
}
let sync_diff_needs_no_pushback = responses.iter().any(|r| {
!r.is_empty()
&& r[0] == super::protocol::tag::SYNC_DIFF
&& super::protocol::decode_sync_diff(&r[1..])
.is_some_and(|diff| diff.pull.is_empty())
});
for response in responses {
if let Err(e) = send.write_u32(response.len() as u32).await {
tracing::warn!(
"[accept] failed to write response header to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
break;
}
if let Err(e) = send.write_all(&response).await {
tracing::warn!(
"[accept] failed to write response body to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
break;
}
}
if just_authed && !hello_sent {
hello_sent = true;
let hello = super::protocol::encode_hello(&effective_device_name(&store));
let header_ok = send.write_u32(hello.len() as u32).await;
if let Err(e) = header_ok {
tracing::warn!(
"[accept] failed to write HELLO header to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
} else if let Err(e) = send.write_all(&hello).await {
tracing::warn!(
"[accept] failed to write HELLO body to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
}
}
if just_authed && !auth_sent {
auth_sent = true;
if let Ok(our_agent) = store.get_default_agent() {
if let Ok(auth_frame) = super::protocol::encode_auth(&our_agent, &remote_key) {
if let Err(e) = send.write_u32(auth_frame.len() as u32).await {
tracing::warn!(
"[accept] failed to write auth-back header to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
} else if let Err(e) = send.write_all(&auth_frame).await {
tracing::warn!(
"[accept] failed to write auth-back body to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
}
}
}
}
if sync_ok || client_pushed || sync_diff_needs_no_pushback {
tracing::info!(
"[accept] sync complete, transitioning to live mode with {}",
&remote_key[..remote_key.len().min(12)]
);
register_live_peer(remote_key, send, recv, store, agent, false);
return Ok(total_imported);
}
}
if sent_sync_ok {
tracing::info!(
"[accept] SYNC_OK sent, entering live mode with {}",
&remote_key[..remote_key.len().min(12)]
);
register_live_peer(remote_key, send, recv, store, agent, false);
}
Ok(total_imported)
}
#[cfg(test)]
mod live_write_admission_tests {
use super::*;
use crate::Db;
use std::collections::HashMap;
#[tokio::test]
async fn relayed_write_to_our_own_drive_accepted_only_when_we_dialed() {
let db = Db::init_temp("relay_owned_drive").await.unwrap();
let (_alice, drive) = db.setup("Alice").await.unwrap();
let server = db.create_agent(Some("Server")).await.unwrap();
let server_agent = ForAgent::AgentSubject(server.subject.clone());
let mut cache = HashMap::new();
assert!(
!admitted_for_drive(&db, &server_agent, &drive, false, &mut cache).await,
"a peer that dialed us may not write our drive on its own identity"
);
let mut cache = HashMap::new();
assert!(
admitted_for_drive(&db, &server_agent, &drive, true, &mut cache).await,
"a server we dialed must be able to relay updates to our own drive"
);
}
#[tokio::test]
async fn owner_admitted_stranger_rejected() {
let db = Db::init_temp("live_admission_owner_vs_stranger")
.await
.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let mallory = db.create_agent(Some("Mallory")).await.unwrap();
let mut cache = HashMap::new();
assert!(
admitted_for_drive(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&drive,
false,
&mut cache
)
.await,
"the drive's own owner must be admitted"
);
let mut cache = HashMap::new();
assert!(
!admitted_for_drive(
&db,
&ForAgent::AgentSubject(mallory.subject.clone()),
&drive,
false,
&mut cache
)
.await,
"an unrelated agent with no rights to the drive must be rejected"
);
}
#[tokio::test]
async fn owner_rejected_when_drive_not_admitted() {
let db = Db::init_temp("live_admission_policy_gate").await.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let policy = std::sync::Arc::new(crate::sync::policy::AllowlistPolicy::new());
policy.set_grace(std::time::Duration::ZERO);
db.set_sync_policy(policy);
let mut cache = HashMap::new();
assert!(
!admitted_for_drive(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&drive,
false,
&mut cache
)
.await,
"the owner's ACL rights don't matter if the drive isn't admitted by policy"
);
}
#[tokio::test]
async fn verdict_is_cached_after_first_check() {
let db = Db::init_temp("live_admission_cache").await.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let mut cache = HashMap::new();
assert!(cache.get(&drive).is_none());
admitted_for_drive(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&drive,
false,
&mut cache,
)
.await;
assert_eq!(cache.get(&drive), Some(&true));
}
#[tokio::test]
async fn stale_public_verdict_cleared_after_late_auth_upgrades_identity() {
let db = Db::init_temp("live_admission_cache_identity_change")
.await
.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let mut cache = HashMap::new();
assert!(
!admitted_for_drive(&db, &ForAgent::Public, &drive, false, &mut cache).await,
"Public should not be admitted for Alice's drive"
);
assert_eq!(cache.get(&drive), Some(&false));
let previous = ForAgent::Public;
let upgraded = ForAgent::AgentSubject(alice.subject.clone());
invalidate_drive_cache_on_identity_change(&upgraded, &previous, &mut cache);
assert!(
cache.get(&drive).is_none(),
"the stale Public verdict must be gone after the identity change"
);
assert!(
admitted_for_drive(&db, &upgraded, &drive, false, &mut cache).await,
"Alice must be admitted for her own drive once the stale cache is cleared"
);
}
#[tokio::test]
async fn cache_survives_when_identity_is_unchanged() {
let db = Db::init_temp("live_admission_cache_identity_stable")
.await
.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let mut cache = HashMap::new();
admitted_for_drive(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&drive,
false,
&mut cache,
)
.await;
assert_eq!(cache.get(&drive), Some(&true));
let same = ForAgent::AgentSubject(alice.subject.clone());
invalidate_drive_cache_on_identity_change(&same, &same, &mut cache);
assert_eq!(
cache.get(&drive),
Some(&true),
"an unchanged identity must not clear the cache"
);
}
}
#[cfg(test)]
mod initiator_trust_tests {
use super::*;
use crate::Db;
use std::collections::HashMap;
async fn private_drive_with_child(db: &Db, owner: &crate::agents::Agent) -> (String, String) {
let mut builder = crate::commit::CommitBuilder::new("placeholder".into());
builder.set(
crate::urls::IS_A.into(),
crate::Value::ResourceArray(vec![crate::urls::DRIVE.into()]),
);
builder.set(
crate::urls::NAME.into(),
crate::Value::String("Private Drive".into()),
);
builder.set(
crate::urls::WRITE.into(),
crate::Value::ResourceArray(vec![owner.subject.to_string().into()]),
);
builder.set(
crate::urls::READ.into(),
crate::Value::ResourceArray(vec![owner.subject.to_string().into()]),
);
let commit = crate::commit::Commit::create_did(builder, owner, db)
.await
.unwrap();
let drive_did = commit.subject.to_string();
let opts = crate::commit::CommitOpts {
validate_signature: true,
validate_timestamp: false,
validate_previous_commit: false,
validate_rights: false,
update_index: true,
..crate::commit::CommitOpts::no_validations_no_index()
};
db.apply_commit(commit, &opts).await.unwrap();
db.set_active_drive(&drive_did).unwrap();
let child = db
.create_resource(
crate::urls::CLASS,
&drive_did,
"Secret Doc",
Some(vec![(
crate::urls::DESCRIPTION,
crate::Value::String("top secret".into()),
)]),
)
.await
.unwrap();
(drive_did, child)
}
#[tokio::test]
async fn pull_serving_refuses_unreadable_subjects_for_public_peer() {
let db = Db::init_temp("initiator_pull_public").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (_drive, child) = private_drive_with_child(&db, &alice).await;
let served = crate::sync::engine::collect_readable_snapshots(
&db,
&ForAgent::Public,
&[child.clone()],
)
.await;
assert!(
served.is_empty(),
"a Public peer must not be served a snapshot for a subject it can't read"
);
let served_owner = crate::sync::engine::collect_readable_snapshots(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&[child.clone()],
)
.await;
assert_eq!(
served_owner.len(),
1,
"the owner must still be served the snapshot it can read"
);
assert_eq!(served_owner[0].0, child);
}
#[tokio::test]
async fn pull_serving_refuses_unreadable_subjects_for_stranger() {
let db = Db::init_temp("initiator_pull_stranger").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (_drive, child) = private_drive_with_child(&db, &alice).await;
let mallory = db.create_agent(Some("Mallory")).await.unwrap();
let served = crate::sync::engine::collect_readable_snapshots(
&db,
&ForAgent::AgentSubject(mallory.subject.clone()),
&[child],
)
.await;
assert!(
served.is_empty(),
"a peer that authenticated as an unrelated agent must not receive unreadable snapshots"
);
}
#[tokio::test]
async fn remove_rejected_for_unadmitted_peer_known_subject() {
let db = Db::init_temp("initiator_remove_stranger").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (_drive, child) = private_drive_with_child(&db, &alice).await;
let mallory = db.create_agent(Some("Mallory")).await.unwrap();
let mut cache = HashMap::new();
apply_peer_remove(
&db,
&ForAgent::AgentSubject(mallory.subject.clone()),
&child,
false,
&mut cache,
)
.await;
assert!(
db.get_resource(&child.as_str().into()).await.is_ok(),
"an unadmitted peer's remove[] entry must NOT delete a subject we hold"
);
assert!(
!crate::sync::tombstones::is_tombstoned(&db, &child),
"an unadmitted peer's remove[] entry must NOT tombstone a subject we hold"
);
}
#[tokio::test]
async fn remove_applied_for_admitted_owner() {
let db = Db::init_temp("initiator_remove_owner").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (_drive, child) = private_drive_with_child(&db, &alice).await;
let mut cache = HashMap::new();
apply_peer_remove(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&child,
false,
&mut cache,
)
.await;
assert!(
db.get_resource(&child.as_str().into()).await.is_err(),
"the owner's remove[] entry must delete the subject"
);
assert!(
crate::sync::tombstones::is_tombstoned(&db, &child),
"the owner's remove[] entry must record a tombstone"
);
}
}