# lanspread-peer `lanspread-peer` is the networking runtime that lets Lanspread nodes find each other on the local network, exchange library metadata, and transfer game files. It is designed to run headless – other crates (most notably `lanspread-tauri-deno-ts`) embed it and drive it through a channel-based API. ## Runtime Overview - `start_peer(game_dir, tx_events, peer_game_db, unpacker, catalog)` boots the asynchronous runtime in the background and returns a `PeerRuntimeHandle` whose sender controls the peer. The injected `Unpacker` keeps archive extraction out of the peer crate's platform layer, and the immutable `CatalogBundle` gates which local game roots are announced or served. - `PeerCommand` is the UI-facing control surface for complete library views, exact-content downloads and installs, game-directory changes, direct typed endpoint connections, and locally authored Call-to-Play intents. - `PeerEvent` enumerates everything the peer runtime reports back to the UI: wholesale remote-library and Call-to-Play views, download/install/uninstall lifecycle updates, runtime failures, and authenticated peer membership changes. - `PeerGameDB` collects metadata only for TLS-authenticated peer endpoints. It records the endpoint generation, runtime session, per-domain revisions, and exact `GameAvailability { game_id, content_id }` rows supplied by that pinned responder. It does not accept remote file manifests or descriptions. Internally the peer runtime owns five coordinated services that run for the lifetime of the process: 1. **Server component** (`run_server_component`) – listens for QUIC connections, advertises via mDNS, and serves bounded `Ping`, `Hello`, change-hint, exact-content chunk, and Stream Install requests. 2. **Discovery loop** (`run_peer_discovery`) – uses the `lanspread-mdns` helper to observe candidate endpoints. A dedicated joinable OS thread owns the blocking browser. Its nonblocking ingress queue and the async set of active candidate negotiations are each bounded at 64; repeated observations that overflow ingress are coalesced by dropping the hint. 3. **Ping service** (`run_ping_service`) – periodically issues QUIC ping requests to keep peer liveness up to date and prunes stale entries from `PeerGameDB`. Every probe captures the authenticated endpoint generation; success, failure, and stale removal are conditional on that exact endpoint and generation, so delayed work cannot affect a reauthenticated peer. 4. **Local game monitor** (`run_local_game_monitor`) – polls a bounded non-recursive metadata snapshot of the configured game directory and its direct game roots once per second, gates per-ID rescans while operations are active, emits local-library changes separately from active-operation snapshots, and runs a 300-second fallback scan for broader reconciliation. 5. **State-sync scheduler** (`run_state_sync`) – coalesces lossy revision hints, compares authenticated `Pong` revisions, and performs bounded `Hello` pulls from exact pinned responders. Its queue and peer table are each capped at 64 entries, with at most eight pulls and eight hint sends running concurrently. `scan_local_library` maintains a lightweight on-disk index and produces both a `GameDB` and protocol summaries. A game is downloaded only when its root-level `version.ini` sentinel exists; `local/` being a directory is the install signal. ## Networking and File Transfer - Transport is handled by [`s2n-quic`](https://github.com/aws/s2n-quic) with its rustls provider. Every outbound operation requires a typed `PeerEndpoint { peer_id, addr }` and TLS 1.3 pins the responder's complete DER SPKI to that expected ID before delegating the real CertificateVerify signature to rustls. ALPN is `lanspread/8`; resumption, tickets, 0-RTT, and half-RTT data are disabled. - Protocol messages are strict, fallible JSON structures defined in `lanspread-proto::{Request, Response}`. Protocol 8 is the only accepted wire shape. A control stream carries exactly one request frame followed by EOF and at most one response; frames are capped at 8 MiB and control I/O is bounded by ten-second deadlines. - File transfers stream raw bytes over dedicated bidirectional QUIC streams. `peer::send_game_file_chunk` services catalog-authorized ranges. The bounded server admits at most 64 unauthenticated handshakes, 64 established connection scopes, and 32 control-stream tasks per connection. Each installation owns one Ed25519 key and one self-issued certificate. `PeerId` is lowercase unpadded RFC 4648 base32 of BLAKE3 over the certificate's complete canonical DER SPKI. The desktop app loads or creates the strict version-1 `peer-identity-v1.json` record in Tauri's app-data directory. Record loading checks the exact version, algorithm, canonical base64, size bounds, certificate/key match, derived SPKI ID, and exact SAN. Opens and publication do not follow symlinks or reparse points, creation does not clobber an existing winner, corrupt bytes are preserved in quarantine before replacement, and Unix private material is mode 0600. Read, permission, quarantine, or write failure returns a typed `Ephemeral` persistence outcome and uses a fresh identity for that runtime. The peer CLI's `--identity-file` selects an existing record strictly and never repairs, quarantines, replaces, or generates that explicit path. Identity persistence supports one normal app-data owner and has no cross-process lease; a concurrent same-user writer can cause availability or continuity loss, but retained no-follow handles contain traversal and publication never overwrites a concurrent winner. ### Responder-owned state and Call to Play `Hello` is an empty pull request. A successful pinned responder returns only its own `PeerStateSnapshot`: one runtime-session ID, a revisioned local library slice, and a revisioned local Call-to-Play author slice. The receiver validates the library and Call-to-Play domains independently and commits them only while the authenticated endpoint generation is still current. A peer never relays another peer's library or Call-to-Play state. Local changes send cheap `LibraryChanged` or `CallToPlayChanged` revision hints. Hints are untrusted and lossy: an unknown claimed peer is ignored, and a known hint can only coalesce a full pull from that peer's pinned endpoint. `Pong` carries the responder's runtime session and both revisions, so idle liveness also heals a dropped hint. Call-to-Play is stored as bounded per-author slices. The core, not the UI, creates event/call nonces and timestamps. Wire events contain a typed `CallId`, event ID, timestamp, and action; author identity comes from the pinned responder and the display name belongs to the author snapshot. Whole snapshots replace a newer revision from the same runtime session. Participant events may be retained but remain hidden until the call creator's direct author slice is present; no event push, acknowledgement, union merge, relay, or tombstone path exists. ### Download Pipeline When the UI asks to download a game: 1. The UI submits only the game ID. The peer core loads that game's immutable `CatalogContentManifest` and constructs one root-confined `ValidatedDownloadManifest` before any filesystem mutation. Remote file descriptions do not choose local paths, sizes, chunks, or hashes. 2. Source selection requires a peer to advertise the exact catalog `(game_id, content_id)` pair. Every chunk request repeats that `ContentId`, a typed canonical catalog path, and the exact catalog range; a version string alone never makes a peer eligible. 3. `download_game_files` recovers any earlier attempt, parks an old `version.ini` as `.version.ini.discarded`, and durably journals the exact old and proposed downloader-owned file sets and their catalog content IDs before preparing non-sentinel files. It then emits `PeerEvent::DownloadGameFilesBegin` and builds a per-peer plan (`build_peer_plans`) that round-robins catalog chunks across the eligible peers. 4. Each plan is executed in its own task (`download_from_peer`). Chunk requests use per-chunk QUIC streams and write into pre-created files. Every chunk, including `version.ini`, must have the catalog length and BLAKE3 digest. One absolute ten-minute deadline covers opening, requesting, receiving, and the post-receive checks for each ordinary chunk. 5. `DownloadProgressTracker` samples byte counters, transfer speed, and the number of unique peers that are actively streaming chunks. The Tauri UI sees those values together through the regular download-progress event. 6. Integrity failures quarantine the runtime-local `(PeerId, content_id)` pair; changing the peer's address does not clear it. Transport failures remain retryable and do not create durable trust state. A failed chunk is tried at most once against every distinct eligible peer identity, with no separate numeric retry cap. 7. `version.ini` chunks are buffered in memory. After transfer, paths owned by the previous successful download but absent from the new manifest are removed. Payload files and their directories are synced before the new sentinel is committed last via `.version.ini.tmp` followed by an atomic rename. A sentinel rename whose directory sync fails leaves ownership pending for recovery instead of being reported as a durable success. 8. Failure, cancellation, and startup recovery use the journal to remove only exact downloader-owned files. Unknown user files, `local/`, and install transaction state are preserved. A regular `version.ini` beside a pending journal proves that the final rename landed; otherwise recovery aborts the incomplete payload. 9. After a successful sentinel commit, `PeerEvent::DownloadGameFilesFinished` is emitted and the peer auto-runs the install transaction. The no-transfer local shortcut requires both a catalog-matching local sentinel and settled ownership whose committed content ID exactly matches the requested catalog manifest. A version string or pre-content-ID ownership record alone is never treated as verified local content. ### Streamed Install Pipeline Low-disk installs use `PeerCommand::StreamInstallGame` instead of the normal archive download pipeline. The peer core loads the catalog's extracted-file manifest, rejects games that do not have one, selects sources advertising the exact catalog `ContentId`, and emits the regular download/install lifecycle events while streaming archive-expanded bytes directly into an isolated `StreamedInstallTransaction`. Every Stream Install control-frame path, including archive names, is a `CanonicalCatalogPath`; malformed framing is distinct from an explicit sender error. The sender admits Stream Install only when its catalog authorizes the feature and the direct regular root `.eti` set exactly matches the catalog archive set; missing, extra, or unsupported archives are rejected before provider work begins. The receiver independently requires the exact archive set and the complete catalog path/kind/size set, hashes every regular extracted file with BLAKE3 while writing, and rejects any missing, unknown, repeated, or mismatched file. Repeated directory entries are harmless and may appear across archives, but a regular extracted path may appear only once globally. The sender-side `StreamInstallProvider` writes control and chunk frames through a cancellable `StreamInstallFrameSink`. If the QUIC writer fails because the receiver cancelled or disconnected, the sink wakes any producer blocked on the bounded frame channel and lets the transfer guard drop normally. Each failed peer attempt rolls back its staging directory before trying the next validated peer. A transaction that created a previously missing game root removes that root again when rollback leaves it empty. Once staging has been fully verified, the account, language, and persona settings are rewritten in that staging tree before it is promoted to `local/`. The one-shot settings marker is written only after successful promotion; if that marker write fails, first play safely retries the rewrite. Post-promote intent cleanup failures are logged for startup recovery rather than reported as a failed install. `PeerCommand::CancelDownload` cancels the tracked download token for an active transfer. The transfer task remains responsible for clearing `active_operations`, discarding partial payload files, and refreshing the settled local snapshot, so the UI continues to treat active-operation snapshots as the single source of truth for whether a download is still running. ### Install Transactions Install, update, uninstall, and install-side startup recovery live under `src/install/`. Install-side operation intent is stored atomically under the configured peer state directory, at `games//install_intent.json`. Game roots still use Lanspread-owned `.local.installing/` and `.local.backup/` directories marked by `.lanspread_owned`. Startup recovery combines the recorded intent with the observed filesystem state and only deletes reserved directories when intent or marker ownership proves they belong to Lanspread. Download provenance is stored separately under `games//download_ownership/v1-/record.json` in the peer state directory. Each namespace is derived from the canonical configured games directory, while the record retains the full lossless root identity for collision and binding checks. Switching roots therefore preserves independent recovery and removal authority for the same game ID in each tree. Downloaded- file removal is deliberately separate from uninstall: it refuses installed or in-flight roots, journals an empty pending generation, and deletes only the regular sentinel plus paths proven by the last committed ownership set. Unknown files, directories, and the game root remain untouched. Ownership schema 2 binds committed and pending file generations to their exact catalog content IDs. A former singleton ownership record is migrated once into its derived root namespace using copy-first durable publication; it is not kept as a second live format. Split or conflicting migration evidence fails closed. Legacy launcher-owned files in game directories are migrated by a dedicated pre-start phase. Normal install, recovery, scan, and transfer paths use only the configured state directory for launcher-owned metadata. ## Integration with `lanspread-tauri-deno-ts` The Tauri application embeds this crate in `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs`: - `LanSpreadState` holds onto the peer control channel, the latest aggregated `GameDB`, per-game operation state, the immutable catalog bundle, and the user-selected game directory. - The Tauri commands (`request_games`, `install_game`, `update_game`, `remove_downloaded_game`, and `update_game_directory`) translate UI actions into `PeerCommand`s. Tauri loads and validates the packaged `game.db` plus companion manifests once during setup; `update_game_directory` validates the filesystem path before storing it, starts the peer runtime on demand with that same bundle, and mirrors installed/uninstalled state into the separate UI-facing database. - A background task consumes `PeerEvent`s and fans them out to the front-end via Tauri publish/subscribe events (`games-list-updated`, `game-download-*`, `game-install-*`, `game-uninstall-*`, `peer-*`). The Tauri crate now only provides the unrar sidecar through the injected `Unpacker`; rollback and cleanup live in the peer transaction code. ## Security & Operational Notes - Outbound TLS authenticates the exact responder endpoint. Inbound hints carry no authority, state snapshots are accepted only from a pinned pull, and file requests are admitted against the sender's local catalog and exact `ContentId`. There is no address-only dial or repository-shared certificate. - mDNS is a link-local source of bounded candidates, not roster authority. A candidate reaches peer/UI/library state only after the pinned outbound handshake commits its still-current negotiation lease. - Long-running blocking mDNS calls are isolated on an owned worker thread whose shutdown is explicitly joined. - File writes are catalog-verified per chunk, and root-level `version.ini` is buffered and written only after the rest of the download has succeeded. ## Known Limitations - `PeerGameDB` currently models the latest metadata that other peers advertise. If the UI needs to surface titles that only exist locally, additional merging with the locally scanned `GameDB` will be required. - The download planner uses a simple round-robin and does not yet take per-peer throughput into account when distributing initial work. Refer to the source (particularly `src/lib.rs`) for the exact message shapes and state machines.