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

+115 -10
View File
@@ -1,13 +1,19 @@
use std::path::{Path, PathBuf};
const PEER_ID_FILE: &str = "peer_id";
const PEER_IDENTITY_FILE: &str = "peer-identity-v1.json";
const LOCAL_LIBRARY_DIR: &str = "local_library";
const LOCAL_LIBRARY_INDEX_FILE: &str = "index.json";
const GAMES_DIR: &str = "games";
const SETUP_DONE_FILE: &str = "setup_done";
const LAUNCH_SETTINGS_APPLIED_FILE: &str = "launch_settings_applied";
const DOWNLOAD_OWNERSHIP_FILE: &str = "download_ownership.json";
const DOWNLOAD_OWNERSHIP_TMP_FILE: &str = "download_ownership.json.tmp";
pub(crate) const DOWNLOAD_OWNERSHIP_DIR: &str = "download_ownership";
pub(crate) const DOWNLOAD_OWNERSHIP_RECORD_FILE: &str = "record.json";
pub(crate) const DOWNLOAD_OWNERSHIP_TMP_FILE: &str = "record.json.tmp";
pub(crate) const DOWNLOAD_OWNERSHIP_RECOVERY_REQUIRED_FILE: &str = "recovery-required";
pub(crate) const LEGACY_DOWNLOAD_OWNERSHIP_FILE: &str = "download_ownership.json";
pub(crate) const LEGACY_DOWNLOAD_OWNERSHIP_TMP_FILE: &str = "download_ownership.json.tmp";
pub(crate) const LEGACY_DOWNLOAD_OWNERSHIP_RECOVERY_REQUIRED_FILE: &str =
"download_ownership.recovery-required";
pub(crate) fn resolve_state_dir(explicit: Option<&Path>) -> PathBuf {
if let Some(dir) = explicit {
@@ -25,8 +31,8 @@ pub(crate) fn resolve_state_dir(explicit: Option<&Path>) -> PathBuf {
std::env::temp_dir().join("lanspread")
}
pub(crate) fn peer_id_path(state_dir: &Path) -> PathBuf {
state_dir.join(PEER_ID_FILE)
pub(crate) fn peer_identity_path(state_dir: &Path) -> PathBuf {
state_dir.join(PEER_IDENTITY_FILE)
}
pub(crate) fn local_library_index_path(state_dir: &Path) -> PathBuf {
@@ -36,7 +42,11 @@ pub(crate) fn local_library_index_path(state_dir: &Path) -> PathBuf {
}
pub(crate) fn game_state_dir(state_dir: &Path, game_id: &str) -> PathBuf {
state_dir.join(GAMES_DIR).join(game_id)
games_state_dir(state_dir).join(game_id)
}
pub(crate) fn games_state_dir(state_dir: &Path) -> PathBuf {
state_dir.join(GAMES_DIR)
}
#[must_use]
@@ -49,10 +59,105 @@ pub fn launch_settings_applied_path(state_dir: &Path, game_id: &str) -> PathBuf
game_state_dir(state_dir, game_id).join(LAUNCH_SETTINGS_APPLIED_FILE)
}
pub(crate) fn download_ownership_path(state_dir: &Path, game_id: &str) -> PathBuf {
game_state_dir(state_dir, game_id).join(DOWNLOAD_OWNERSHIP_FILE)
pub(crate) fn download_ownership_namespaces_dir(state_dir: &Path, game_id: &str) -> PathBuf {
game_state_dir(state_dir, game_id).join(DOWNLOAD_OWNERSHIP_DIR)
}
pub(crate) fn download_ownership_tmp_path(state_dir: &Path, game_id: &str) -> PathBuf {
game_state_dir(state_dir, game_id).join(DOWNLOAD_OWNERSHIP_TMP_FILE)
pub(crate) fn download_ownership_namespace_component(games_folder_key: &str) -> String {
let key = games_folder_key.as_bytes();
let mut hasher = blake3::Hasher::new();
hasher.update(b"lanspread-download-ownership-root\0");
hasher.update(&u64::try_from(key.len()).unwrap_or(u64::MAX).to_le_bytes());
hasher.update(key);
format!("v1-{}", hasher.finalize().to_hex())
}
pub(crate) fn download_ownership_namespace_dir(
state_dir: &Path,
game_id: &str,
games_folder_key: &str,
) -> PathBuf {
download_ownership_namespaces_dir(state_dir, game_id)
.join(download_ownership_namespace_component(games_folder_key))
}
pub(crate) fn download_ownership_path(
state_dir: &Path,
game_id: &str,
games_folder_key: &str,
) -> PathBuf {
download_ownership_namespace_dir(state_dir, game_id, games_folder_key)
.join(DOWNLOAD_OWNERSHIP_RECORD_FILE)
}
pub(crate) fn download_ownership_tmp_path(
state_dir: &Path,
game_id: &str,
games_folder_key: &str,
) -> PathBuf {
download_ownership_namespace_dir(state_dir, game_id, games_folder_key)
.join(DOWNLOAD_OWNERSHIP_TMP_FILE)
}
pub(crate) fn download_ownership_recovery_required_path(
state_dir: &Path,
game_id: &str,
games_folder_key: &str,
) -> PathBuf {
download_ownership_namespace_dir(state_dir, game_id, games_folder_key)
.join(DOWNLOAD_OWNERSHIP_RECOVERY_REQUIRED_FILE)
}
pub(crate) fn legacy_download_ownership_path(state_dir: &Path, game_id: &str) -> PathBuf {
game_state_dir(state_dir, game_id).join(LEGACY_DOWNLOAD_OWNERSHIP_FILE)
}
pub(crate) fn legacy_download_ownership_tmp_path(state_dir: &Path, game_id: &str) -> PathBuf {
game_state_dir(state_dir, game_id).join(LEGACY_DOWNLOAD_OWNERSHIP_TMP_FILE)
}
pub(crate) fn legacy_download_ownership_recovery_required_path(
state_dir: &Path,
game_id: &str,
) -> PathBuf {
game_state_dir(state_dir, game_id).join(LEGACY_DOWNLOAD_OWNERSHIP_RECOVERY_REQUIRED_FILE)
}
/// Stable, lossless platform-native identity for a canonical games directory.
///
/// Callers must canonicalize and validate the directory before deriving its
/// key. Persistent state uses this value to prevent records from one configured
/// games directory from authorizing mutations in another.
#[cfg(unix)]
pub(crate) fn games_folder_key(path: &Path) -> String {
use std::os::unix::ffi::OsStrExt as _;
format!("unix:{}", hex_encode(path.as_os_str().as_bytes()))
}
#[cfg(windows)]
pub(crate) fn games_folder_key(path: &Path) -> String {
use std::{fmt::Write as _, os::windows::ffi::OsStrExt as _};
let mut encoded = String::from("windows:");
for unit in path.as_os_str().encode_wide() {
let _ = write!(encoded, "{unit:04x}");
}
encoded
}
#[cfg(not(any(unix, windows)))]
pub(crate) fn games_folder_key(path: &Path) -> String {
format!("native:{}", hex_encode(path.as_os_str().as_encoded_bytes()))
}
#[cfg(not(windows))]
fn hex_encode(bytes: &[u8]) -> String {
use std::fmt::Write as _;
let mut encoded = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let _ = write!(encoded, "{byte:02x}");
}
encoded
}