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:
128 files changed
+51759
-10784
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user