feat(peer)!: cut over to authenticated catalog sharing

Replace address-only trust and pushed peer state with installation identities,
SPKI-pinned QUIC, candidate-only discovery, and bounded responder-owned
protocol-8 pulls. The runtime now owns each network generation and all admitted
work through shutdown.

Add exact bundled content identities, reproducible manifest publishing,
capability-confined downloads, streaming BLAKE3 verification, quarantine and
retry, and crash-recoverable download and install transactions. Ship generated
fixture catalogs and fail closed when production manifests are absent.

The Tauri backend exposes durable sharing policy, redacted identity state, and
attempt-keyed transfer snapshots. Frontend consumption follows in the next
commit. Repository-wide test certificates and protocol-7 paths are removed.

BREAKING CHANGE: peers must use protocol 8 and exact catalog content artifacts;
protocol-7 frames and shared-certificate identities are no longer accepted.

Test Plan:
- `just test` -- passed on the completed stack (708 workspace tests)
- `just clippy` -- passed on the completed stack
- `just build` -- passed with fixture catalogs on the completed stack
- `just catalog-check-production` -- failed closed because the external
  production manifest corpus is absent
- `git diff --cached --check` -- passed
This commit is contained in:
ddidderr committed 2026-08-10 13:59:18 +02:00
1 parent 36c4785775
commit 60fd7ba0c2
128 files changed
+51759 -10784

No files matched your search

+76 -24
View File
@@ -1,14 +1,19 @@
//! UI event helpers used by peer command and service code.
use std::{collections::HashMap, net::SocketAddr, sync::Arc};
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
use lanspread_db::db::GameCatalog;
use lanspread_db::content_manifest::ContentId;
use tokio::sync::{RwLock, mpsc::UnboundedSender};
use crate::{
ActiveOperation,
ActiveOperationKind,
PeerEvent,
RemoteGameAvailability,
RemoteLibraryView,
context::OperationKind,
peer_db::PeerGameDB,
};
@@ -66,38 +71,85 @@ fn active_operation_kind(operation: OperationKind) -> ActiveOperationKind {
pub async fn emit_peer_game_list(
peer_game_db: &Arc<RwLock<PeerGameDB>>,
catalog: &Arc<RwLock<GameCatalog>>,
tx_notify_ui: &UnboundedSender<PeerEvent>,
) {
let games = {
let catalog = catalog.read().await;
peer_game_db.read().await.get_catalog_games(&catalog)
};
send(tx_notify_ui, PeerEvent::ListGames(games));
let db = peer_game_db.read().await;
send_remote_library_view_locked(&db, tx_notify_ui);
}
fn send_remote_library_view_locked(
peer_game_db: &PeerGameDB,
tx_notify_ui: &UnboundedSender<PeerEvent>,
) {
send(
tx_notify_ui,
PeerEvent::RemoteLibraryView(remote_library_view(peer_game_db)),
);
}
pub(crate) fn remote_library_view(peer_game_db: &PeerGameDB) -> RemoteLibraryView {
let mut counts = BTreeMap::<(String, ContentId), u32>::new();
for game in peer_game_db
.peer_snapshots()
.into_iter()
.flat_map(|peer| peer.games)
{
let count = counts.entry((game.game_id, game.content_id)).or_default();
*count = count.saturating_add(1);
}
RemoteLibraryView {
games: counts
.into_iter()
.map(
|((game_id, content_id), peer_count)| RemoteGameAvailability {
game_id,
content_id,
peer_count,
},
)
.collect(),
}
}
pub async fn emit_peer_count(
peer_game_db: &Arc<RwLock<PeerGameDB>>,
tx_notify_ui: &UnboundedSender<PeerEvent>,
) {
let peer_count = { peer_game_db.read().await.get_peer_addresses().len() };
let db = peer_game_db.read().await;
let peer_count = db.peer_endpoints().len();
send(tx_notify_ui, PeerEvent::PeerCountUpdated(peer_count));
}
pub async fn emit_peer_discovered(
peer_game_db: &Arc<RwLock<PeerGameDB>>,
tx_notify_ui: &UnboundedSender<PeerEvent>,
peer_addr: SocketAddr,
) {
send(tx_notify_ui, PeerEvent::PeerDiscovered(peer_addr));
emit_peer_count(peer_game_db, tx_notify_ui).await;
}
#[cfg(test)]
mod tests {
use super::*;
pub async fn emit_peer_lost(
peer_game_db: &Arc<RwLock<PeerGameDB>>,
tx_notify_ui: &UnboundedSender<PeerEvent>,
peer_addr: SocketAddr,
) {
send(tx_notify_ui, PeerEvent::PeerLost(peer_addr));
emit_peer_count(peer_game_db, tx_notify_ui).await;
#[tokio::test]
async fn remote_library_view_is_enqueued_before_a_waiting_writer_can_commit() {
let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new()));
let guard = peer_game_db.read().await;
let writer_db = Arc::clone(&peer_game_db);
let writer = tokio::spawn(async move {
let _guard = writer_db.write().await;
});
tokio::task::yield_now().await;
assert!(!writer.is_finished(), "writer must wait for the read guard");
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
send_remote_library_view_locked(&guard, &tx);
assert!(matches!(
rx.try_recv(),
Ok(PeerEvent::RemoteLibraryView(RemoteLibraryView { games })) if games.is_empty()
));
assert!(
!writer.is_finished(),
"the view must be enqueued while the read guard still orders writers"
);
drop(guard);
tokio::time::timeout(std::time::Duration::from_secs(1), writer)
.await
.expect("writer should acquire after the view is enqueued")
.expect("writer task should finish");
}
}