docs(peer): document authenticated sharing architecture

Update user and developer documentation for the protocol-8 system: persistent
SPKI-derived identities, exact catalog ContentId authority, pinned responder
pulls, structured runtime ownership, direct-author Call to Play, and the global
local-network sharing switch.

Remove active descriptions of repository-wide certificates, pushed deltas,
relayed histories, and metadata consensus. Keep operational and UI boundaries
aligned with the implementation, including the fail-closed production catalog
gate.

Test Plan:
- `just fmt` (passed)
- `git diff --cached --check` (passed)
This commit is contained in:
2026-08-10 14:05:00 +02:00
parent 4a1b08db98
commit e0eafa6e33
4 changed files with 465 additions and 260 deletions
+254 -167
View File
@@ -1,8 +1,9 @@
# lanspread-peer proposed protocol and architecture
# lanspread-peer architecture
This document proposes a tighter, more fault-tolerant protocol while keeping the
current idea: mDNS discovery, QUIC transport, on-demand metadata, and chunked
file transfers.
The peer uses mDNS discovery, authenticated QUIC transport, responder-owned
metadata snapshots, and catalog-authorized file transfers. Wire protocol 8 and
ALPN `lanspread/8` are the only supported wire mode; there is no legacy decode,
fallback, or compatibility shim.
## Goals (unchanged)
@@ -11,136 +12,181 @@ file transfers.
- UI drives operations through `PeerCommand`, peers remain headless.
- Peers can appear/disappear at any time without data loss.
## Installation identity and QUIC authentication
- One installation-local Ed25519 key signs one self-issued TLS certificate.
`PeerId` is BLAKE3 over the certificate's complete canonical DER
SubjectPublicKeyInfo, encoded as 52 lowercase unpadded RFC 4648 base32
characters. It is not a UUID, display name, IP address, or wire assertion.
- The desktop app stores a strict version-1 record at `peer-identity-v1.json` in
its Tauri app-data directory. The record binds the Ed25519 certificate,
private key, SPKI-derived `PeerId`, and exact SAN. Reads and publication do
not follow symlinks or reparse points, creation is no-clobber, corrupt bytes
are quarantined before replacement, and private material is kept at mode 0600
where the platform supports Unix modes.
- Persistence failure produces a typed `Ephemeral` outcome and a fresh identity
for that runtime; it never falls back to another peer-ID scheme. The peer CLI
can instead select an existing record with `--identity-file`; that path is
loaded strictly and is never repaired, replaced, or generated implicitly.
- Identity state assumes one normal application owner for its app-data directory
and deliberately has no cross-process identity lease. Retained no-follow
handles contain path traversal and external-target mutation, and publication
does not overwrite an existing winner. A concurrent same-user writer can still
cause availability or continuity loss during quarantine; it is outside the
supported ownership model.
- QUIC uses the rustls provider with TLS 1.3 only. Every outbound operation
carries a `PeerEndpoint { peer_id, addr }`; the expected ID is encoded in an
exact SNI name, checked against the responder certificate SPKI, and the real
TLS 1.3 CertificateVerify signature is delegated to rustls. ALPN is
`lanspread/8`. Client resumption and 0-RTT are disabled; the server issues no
tickets and sends no early or half-RTT data.
## Peer lifecycle and message flow
### 1) Startup and advertise
- Start QUIC server.
- Advertise via mDNS with TXT records:
- `peer_id` (stable ID, not tied to IP)
- `peer_id` (the canonical SPKI-derived ID)
- `proto_ver`
- `library_rev` (monotonic local library revision)
- optional `hostname`
### 2) Discovery and handshake
When a peer is discovered:
1. Connect and send
`Hello { peer_id, proto_ver, listen_addr, library_rev, library_digest, features }`.
`listen_addr` is mandatory; the QUIC source port is only a temporary
transport port and must not be recorded as the peer's listener.
2. Receive
`HelloAck { peer_id, proto_ver, listen_addr, library_rev, library_digest, features }`.
3. If the remote `peer_id` is already known but the address changed, update it.
4. If protocol versions are incompatible, drop the peer (and keep mDNS
watching).
5. If library digests match, do nothing else.
6. If digests differ:
- If we have a known `library_rev` for that peer, request `LibraryDelta`.
- Otherwise request `LibrarySnapshot`.
1. Parse `peer_id`, address, and `proto_ver` into a candidate `PeerEndpoint`.
Discovery is not authentication and does not add the peer to `PeerGameDB` or
emit UI membership events. The mDNS ingress queue and active candidate
negotiations are each capped at 64.
2. Reserve a candidate negotiation lease before queueing or awaiting work, then
establish a TLS-pinned connection to that exact endpoint. Missing or non-v8
records are rejected, and neither an ephemeral QUIC source port nor a payload
can replace the candidate listener address.
3. Send the empty `Hello` pull request. The pinned responder returns a
`HelloSnapshot` containing a `PeerStateSnapshot` with `runtime_session_id`,
`library`, and `call_to_play`: only that responder's local library and local
Call-to-Play author slice.
4. Decode the strict bounded wire shape, then validate the library and
Call-to-Play domains independently. Commit the endpoint and each valid domain
only if the candidate lease is still current for both peer ID and address.
This domain isolation lets an invalid remote Call-to-Play slice be cleared or
preserved according to its session without discarding a valid library.
5. Assign a fresh endpoint generation to every successful authentication.
Dropped, failed, or superseded work releases its candidate claims, and a late
result cannot mutate a newer generation.
### 3) Steady state
- Any message updates `last_seen`.
- Pings run only when idle (or on a longer interval), not every 5 seconds.
- Library updates are pushed as deltas, debounced and coalesced.
- Call to Play actions are broadcast as immutable, uniquely identified events.
- Successful liveness probes update `last_seen` only when both the typed
endpoint and its authenticated generation are still current. Stale probes
cannot refresh or remove a reauthenticated endpoint.
- `Ping` receives a `Pong` carrying `PeerRevisions` with the runtime-session ID
and both domain revisions. A session or revision mismatch schedules a full
pinned pull, so idle liveness heals dropped change hints.
- Local library or Call-to-Play changes send cheap revision hints. Hints are
untrusted and lossy: unknown claims are ignored, and a known claim can only
coalesce a pull from the claimed peer's already authenticated endpoint.
- The state-sync scheduler caps its ingress and tracked peers at 64, coalesces
work for five seconds, and runs at most eight pinned pulls and eight hint
sends concurrently. Child work is drained lexically on shutdown.
- The server admits at most 64 unauthenticated handshakes, 64 established
connection scopes, and 32 control streams per connection. Each control stream
accepts one frame followed by request EOF and sends at most one response; the
frame cap is 8 MiB and control I/O has ten-second deadlines.
### Call to Play replication
Call to Play is transient peer-session state rather than database state. The
peer keeps a bounded event history, deduplicated by event ID. Every event and
chat message remains in the snapshot for the full lifetime of an active call, so
a peer joining mid-call receives the complete context. A creator's `Start` or
`Cancel` makes the call terminal and read-only, but its complete roster and chat
history remain in snapshots for 15 minutes so late joiners can see the outcome.
After that display window the history compacts to the Start or Cancel tombstone
for the rest of the peer session. Active and recently terminal calls are never
partially trimmed. If genuinely active history reaches the bound, local
publishes return an error to the caller instead of appearing to succeed;
terminal histories and tombstones do not consume that active-history capacity. A
call whose deadline elapses remains available for five minutes so the creator
can start or extend it, then its unresolved history is evicted as a unit. A
local action is applied to that history, sent to the UI, and broadcast to every
currently known peer. An incoming live event is applied once and sent to the UI
without being rebroadcast, which prevents forwarding loops.
Call to Play is transient, responder-owned state. Each peer serves exactly one
local author snapshot:
Live Call to Play delivery is acknowledged by the receiver. Applied, duplicate,
and obsolete events need no follow-up. An unknown envelope peer, missing call
root, transport failure, or malformed acknowledgement makes the sender perform
one normal `Hello` / `HelloAck` exchange with that peer. The handshake carries
the full retained history in both directions, so a transient request failure
heals without waiting for mDNS rediscovery or a later reconnect. A rejected
event is logged without retry. Local publication remains successful while this
healing happens asynchronously, so an offline peer cannot block an action.
```text
CallToPlayAuthorSnapshot { revision, display_name, events }
CallToPlayAuthorEvent { id, call_id, at, action }
```
Actors are keyed by the peer's stable ID and carry a separate display name. The
origin peer overwrites the actor ID on local actions. A live-event envelope must
name a peer already in the receiver's roster, and every enclosed actor ID must
match that envelope. This prevents accidental identity mixing and protects
creator controls from other normal clients. It is not authentication against a
hostile LAN peer: all peers use the shared application TLS identity, and stable
peer IDs are self-asserted under the project's trusted-LAN model.
The outer `PeerStateSnapshot` owns the runtime-session ID. A `CallId` encodes
the creator's typed `PeerId` plus a 128-bit nonce. The UI supplies only a local
intent and current display name; the peer core generates call/event nonces and
timestamps. Identity and display name are not repeated inside events. Creator
actions (`Create`, `Start`, `Cancel`, and `AddTime`) must come from the
`CallId.creator`; participant authors may contribute only participant actions.
`Hello` and `HelloAck` include each side's event history. This lets peers that
join after a call was created reconstruct the same nominations, responses,
RSVPs, chat, and terminal actions. The launcher reducer sorts the event stream
deterministically and derives deadlines and check-in phases from timestamps.
Those phases compare creator-supplied wall-clock timestamps with each viewer's
local wall clock, so LAN machines are assumed to be synchronized closely enough
for human-scale minute countdowns; clock skew shifts the displayed boundary by
the same amount. There is deliberately no compatibility path for older protocol
versions.
The receiver assigns author identity from the pinned `PeerEndpoint`, validates
and normalizes the whole author slice off-lock, then commits it against that
endpoint's current generation and runtime session. For the same session, only a
higher revision replaces the slice; an equal identical snapshot is a no-op, an
equal different snapshot is a conflict, and a lower revision is stale. A new
authenticated session clears the previous slice before accepting valid new
state. Invalid same-session state preserves the last valid slice, while invalid
new-session state leaves that author absent.
Participant events are retained but hidden until the creator's direct author
slice supplies the call root. Removing a participant removes only that author's
rows; removing the creator hides the entire call. Because every snapshot is
served only by its author and attributed from the pinned responder, there is no
union merge, relay, live-event push, acknowledgement, or author claim on the
wire. The UI receives a deterministic complete replacement view.
Unresolved call history is removed as a whole after the deadline plus five
minutes; terminal history is removed as a whole after `Start` or `Cancel` plus
15 minutes. No tombstone survives pruning. Pruning a local slice advances its
revision before a snapshot or `Pong` is served, allowing peers to discover the
removal. The store caps total authors at 64, events per author at 4,096, and one
encoded author snapshot at 4 MiB, with reserved capacity for terminal actions.
### 4) Shutdown
- Optional `Goodbye { peer_id }` lets others remove the peer quickly.
- If a peer vanishes without goodbye, stale timeout + ping removal handle it.
- Goodbye is a hint, never required for correctness.
- Cancellation stops admission, drains all lexically owned connection, stream,
state-sync, operation, and mDNS children, closes the shared endpoints, and
joins the supervisor.
- There is no `Goodbye` control message. Pinned liveness failure and stale
generation-conditional removal are authoritative for departure.
## Library sync protocol
### Summary and snapshot
The responder's full library slice is
`LibrarySnapshot { revision, games: Vec<GameAvailability> }`, where each sorted,
unique availability row contains only `game_id` and the catalog-derived exact
`ContentId`. A snapshot contains at most 4,096 games. It does not carry titles,
versions, paths, sizes, file lists, hashes chosen by the peer, installed state,
or another peer's availability.
- `LibrarySummary { peer_id, summary: { library_rev, library_digest, game_count } }`
- `LibrarySnapshot { peer_id, snapshot: { library_rev, games: Vec<GameSummary> } }`
The empty `Hello` request always returns the responder's current complete slice.
For the same runtime session, a higher revision atomically replaces the cached
slice; lower or equal stale work cannot roll it back. A new authenticated
runtime session resets the old domain before its candidate snapshot is
considered. `LibraryChanged(ChangeHint)` carries only an untrusted claimed
identity, runtime session, and revision. It never mutates a library directly and
has no delta payload. A pinned `Pong` revision mismatch schedules the same full
pull.
### Delta updates
## Local library publication
- `LibraryDelta { peer_id, delta: { from_rev, to_rev, added, updated, removed } }`
- `removed` is a list of game IDs.
- Deltas are idempotent; ignore if `to_rev` <= known rev.
### GameSummary (concept)
- `id`, `name`, `eti_version`, `size`, `downloaded`, `installed`
- `manifest_hash` (hash of file list + sizes)
- `availability` (e.g., `ready`, `downloading`, `local_only`)
## When peers broadcast their game list
- Only on changes, not on a timer.
- Only real local availability changes advance the revision and schedule a hint;
there is no periodic list broadcast.
- Filesystem events are gated per game ID instead of time-debounced:
- an active operation lock drops events for that game;
- a rescan already running for the ID sets a rescan-pending flag;
- the running rescan loops once more when that flag was set.
- Local library scans emit `LocalLibraryChanged` only for real library changes,
except that accepted game-directory changes can force a UI snapshot for the
new path without sending a peer delta.
new path without changing peer availability.
- Active operation mutations emit `ActiveOperationsChanged` from the mutation
path instead of riding on local library scans.
- Send `LibraryDelta` to known peers; send `LibrarySummary` on new connections.
- The remote UI projection is a wholesale replacement derived from all current
authenticated per-peer slices; two peers offering the same game remain
distinct exact-content sources.
## Local game scanning: fast and low cost
### Strategy
1. Maintain a persistent on-disk index (per game):
- `manifest_hash`, total size, file list (optional), and a fingerprint
(root-level `version.ini` mtime, root-level `.eti` mtime/size, and `local/`
directory presence).
2. Use filesystem watchers to update only changed games.
1. Maintain a persistent revisioned on-disk index of local summaries and cheap
per-game fingerprints (root-level `version.ini` contents/mtime, root-level
`.eti` name/size/mtime, `local/` presence, and recovery state).
2. Poll a bounded non-recursive metadata snapshot once per second and update
only changed games.
3. Keep a 300-second fallback scan to recover from missed events.
### Fast-path scanning
@@ -150,7 +196,7 @@ versions.
- root-level `.eti` file names, sizes, and mtimes
- root-level `version.ini` mtime
- presence of `local/` as a directory
- If fingerprint unchanged, reuse cached size and manifest hash.
- If the fingerprint is unchanged, reuse the cached local summary.
- Only run a recursive scan for new or changed games.
## Local State and Recovery
@@ -179,11 +225,14 @@ Reserved per-game paths:
flight.
- `games/<game_id>/install_intent.json` in the configured state directory is the
atomic per-game intent log.
- `games/<game_id>/download_ownership.json` in that state directory records the
last committed and any pending downloader-owned regular-file set. The record
is bound to one canonical configured games directory.
- `games/<game_id>/download_ownership/v1-<root_digest>/record.json` in that
state directory records the last committed and any pending downloader-owned
regular-file set and its exact catalog content ID. The namespace is derived
from one canonical configured games directory, and the record retains the full
lossless root identity. Same-ID state for different configured roots therefore
remains independent.
- `.lanspread_owned` inside `.local.*` directories proves Lanspread ownership
when the current intent is `None`.
when no active intent independently proves transaction ownership.
Downloaded-file removal is not an uninstall transaction. For a catalog ID that
is a single direct child of the configured game directory, it requires a valid
@@ -192,12 +241,18 @@ ownership record and regular root-level `version.ini`, and refuses `local/`,
generation, deletes only committed downloader-owned files and the sentinel, and
keeps the game root plus every unknown file or directory.
Recovery reads app-state `install_intent.json` and combines the recorded intent
with the observed `local/`, `.local.installing/`, and `.local.backup/` state.
Intent states `Installing`, `Updating`, and `Uninstalling` prove ownership of
the corresponding reserved directories even if the marker was not flushed before
a crash. With intent `None`, markerless `.local.*` directories are left
untouched.
The state layout is game-first: root namespaces live under each game ID rather
than under a global root tree. A former singleton ownership record is migrated
once into its derived namespace with copy-first durable publication. Singleton
state is not accepted as a parallel runtime format, and conflicting or split
migration evidence fails closed.
Recovery reads app-state `install_intent.json` and combines the recorded active
intent with the observed `local/`, `.local.installing/`, and `.local.backup/`
state. Intent states `Installing`, `Updating`, and `Uninstalling` prove
ownership of the corresponding reserved directories even if the marker was not
flushed before a crash. Settled state is represented by absence of the intent;
markerless `.local.*` directories are then left untouched.
Legacy `.lanspread/`, `.lanspread.json`, `.lanspread.json.tmp`,
`.softlan_game_installed`, and `local/.softlan_first_start_done` files are
@@ -210,21 +265,49 @@ Most scans become O(number of game dirs), with full recursion only when needed.
## File manifests and downloads
- Keep `GetGame`/manifest requests, but keyed by `manifest_hash` so repeated
calls can be skipped when unchanged.
- The complete remote description is converted into a
`ValidatedDownloadManifest` before any destination mutation. It contains
canonical game-root-relative paths and rejects aliases, reserved state, shape
conflicts, and bounded-size violations as one unit.
- Tauri and the peer CLI inject one immutable `CatalogBundle`: `game.db` defines
catalog identity/version, the mandatory compact
`manifests/catalog-content-index-v1.jsonl` maps every exact catalog row to its
expected `ContentId` and Stream Install capability, and
`manifests/<game_id>.json` defines canonical paths, kinds, sizes, 128 MiB
chunks, and BLAKE3 values. The index has exact database coverage and is loaded
once; manifest bodies remain on demand and must recompute both indexed fields.
Disk body loads and full validation recheck the durable publication marker
before and after their work. Runtime peers cannot add catalog entries or
replace that byte authority.
- Authenticated remote-availability joins use the compact index only. Unknown or
mismatched `(game_id, content_id)` pairs are rejected without filesystem I/O
or manifest parsing; actual serving, download planning, and Stream Install
still load and validate the full body.
- The local catalog artifact is converted into a `ValidatedDownloadManifest`
before any destination mutation. It contains canonical game-root-relative
paths and rejects aliases, reserved state, shape conflicts, and bounded-size
violations as one unit. Remote peers never define the storage plan.
- Source selection requires the exact catalog `(game_id, content_id)` pair from
an authenticated peer's current library slice. Every ordinary chunk request
repeats the exact `ContentId`, `CanonicalCatalogPath`, offset, and length.
There is no version-only or address-only eligibility adapter.
- A new manifest target that already exists without prior committed ownership is
rejected before the sentinel, ownership journal, or payload is mutated.
- Download mutation holds a capability handle for the direct catalog game root.
Directory components and final files are reopened relative to that handle
without following links or Windows reparse points; chunk writes and checks use
the same opened file handle.
- Downloads remain chunked QUIC streams with the existing integrity checks.
- Downloads hash every chunk, including the buffered `version.ini`, as it is
received and require the catalog BLAKE3 value, exact length, and exact end of
stream. One absolute ten-minute application deadline covers the complete
open/request/receive/check lifecycle of each ordinary chunk.
- An integrity failure quarantines `(PeerId, content_id)` in shared memory for
the rest of that peer runtime, independent of address changes. Transport
failures remain retryable without quarantine. Each failed chunk may try every
distinct eligible peer identity once; there is no separate numeric retry cap.
- A no-transfer local shortcut is allowed only when settled ownership records
the exact expected committed content ID. A matching `version.ini` alone, or a
legacy/pre-content-ID record, is not catalog-content proof.
- A game is transferable only when its ID is in the catalog, no operation is
active for that ID, and the root-level `version.ini` sentinel exists.
- The sender admits only an exact catalog path and, for ranged requests, an
exact catalog chunk range before opening the retained no-follow file handle.
- `local/` paths are never served, even if a stale or malicious manifest request
asks for them.
- Cancelling or recovering a download removes only paths named by its durable
@@ -236,65 +319,69 @@ Most scans become O(number of game dirs), with full recursion only when needed.
- Low-disk streamed installs request archive-derived file bytes from one peer
and write them directly into the install transaction staging directory.
- The receiver verifies every streamed file against the sender archive's file
size and RAR CRC32 before the transaction may commit. This catches truncated
streams, transport corruption, and provider bugs.
- This is not malicious-peer protection: the peer controls both the archive
metadata and the streamed bytes. A trusted-content model needs catalog-owned
hashes, either for the root archives or for extracted files, and receiver-side
SHA-256 verification against those catalog values before commit.
- A sender admits Stream Install only for a manifest with verified extracted
output and only when its direct regular root `.eti` set exactly equals the
catalog archive set. Missing or extra archives stop before the extraction
provider receives authority.
- The Stream Install request names the exact catalog `ContentId`. All
path-bearing frames, including archive names, use `CanonicalCatalogPath` and
the fallible checked decoder distinguishes malformed framing from an explicit
sender `Error` frame.
- The receiver independently requires the exact catalog archive set and the
complete extracted path/kind/size set. It hashes every regular file with
BLAKE3 while writing and verifies every expected entry before commit. Regular
files may occur only once across the entire archive set; repeated directory
entries are allowed because they carry no bytes. RAR CRC32 remains only an
early corruption signal, not an authority boundary.
- Integrity failures quarantine the source for that catalog content and retry a
fresh transaction from another eligible peer. Transport failures retry without
quarantine; local I/O and cancellation stop. Every failed attempt rolls back
before the next source begins.
- After complete catalog verification, account/language/persona settings are
rewritten in staging before promotion. The one-shot launch-settings marker is
written only after successful promotion; if it cannot be written, first play
safely retries the rewrite.
- A game without a verified extracted-file manifest neither offers nor accepts
Stream Install. There is no CRC32-only fallback.
## Catalog publication and packaging
- `lanspread-catalog-publisher` generates production manifests and their
complete compact identity index beside the canonical packages, independently
rebuilds/verifies each selected artifact, and uses a durable corpus marker so
an interrupted body/index publication fails closed. Incremental generation
requires an existing exact indexed corpus and atomically republishes the full
index after the selected bodies. After it owns the marker, it freshly
validates every unselected body/index pair before deriving that mixed index;
no pre-marker snapshot can roll another completed publication backward.
- `lanspread-fixture-catalog` is a separate test-only generator. It derives
reduced peer-CLI `game.db` files and manifests from explicitly selected
fixtures; those outputs are development and acceptance-test authority only.
- Tauri fixture builds require both the exact committed development resource map
and `LANSPREAD_USE_FIXTURE_CATALOG=1`. Every other build mode defaults to the
production resource map, and the custom production profile cannot be
downgraded to fixtures.
- Production packaging requires the complete production `game.db`/manifest
corpus to pass `check --all`. The canonical production packages and their 186
generated manifests are not present in this checkout, so that gate remains
intentionally blocked; fixture success is not production completion.
## Fault tolerance rules
- Every peer is keyed by `peer_id`, not by IP address.
- Peer addresses are listener addresses from mDNS or `Hello`/`HelloAck`, never
ephemeral QUIC source ports.
- `library_rev` is monotonic and guards against out-of-order updates.
- Any mismatch or missing delta falls back to `LibrarySnapshot`.
- Loss of goodbye is harmless; stale timeout is authoritative.
## Roadmap from current design to this one
1. Protocol updates in `lanspread-proto`:
- Define `Hello`, `HelloAck`, `LibrarySummary`, `LibrarySnapshot`,
`LibraryDelta`, and optional `Goodbye` messages.
- Thread `peer_id`, `library_rev`, and `manifest_hash` through all library
and manifest-bearing types.
- Make `Hello` and `HelloAck` carry the sender's `listen_addr`,
`library_rev`, and `library_digest` so both sides can record stable
listener addresses and immediately select `LibraryDelta` vs
`LibrarySnapshot`.
2. Peer identity:
- Persist a stable `peer_id` (UUID) in the peer config and inject it into
`PeerInfo` and `PeerGameDB` at startup.
- Track `peer_id -> SocketAddr` in the discovery table and update the address
on any incoming handshake or mDNS refresh.
3. Discovery handshake:
- Publish `peer_id` and `library_rev` in mDNS TXT records to avoid immediate
TCP/QUIC roundtrips when nothing changed.
- Add a lightweight handshake in `run_peer_discovery` that exchanges
`Hello`/`HelloAck` before any library sync.
- Ignore peers that do not advertise the current protocol version.
4. Library revisioning:
- Store a monotonic `library_rev` locally and increment only after a
successful index refresh completes.
- Apply `LibraryDelta` when `library_rev` matches; reject stale or future
revisions and request `LibrarySnapshot` instead.
- Cache the last accepted `manifest_hash` per peer to short-circuit manifest
requests when unchanged.
5. Local index + scan optimizations:
- Use the cached `local_library/index.json` file in the configured state
directory to store per-root fingerprints and computed manifests.
- Use filesystem watchers with a debounce window to collect changes and
incrementally update the cache.
- Schedule a low-frequency full scan to reconcile missed watcher events.
6. Announce updates:
- Broadcast `LibraryDelta` updates keyed by `library_rev`.
- Send `LibrarySummary` on new connections to seed the delta flow.
7. File manifest caching:
- Store per-game `manifest_hash` and only fetch details when changed.
8. Liveness:
- Reduce ping frequency; update `last_seen` on any message.
- Add optional `Goodbye` on shutdown paths.
9. Tests:
- Delta apply/merge, rev ordering, manifest hashing, and scan cache behavior.
- Every authenticated peer is keyed by `PeerId`, not by IP address.
- An authenticated `PeerEndpoint` retains the candidate or explicitly supplied
listener address. Payload fields and ephemeral QUIC source ports cannot
rewrite it, and no network caller can dial an address without the expected
responder ID.
- Every successful authentication assigns a fresh endpoint generation. Refresh,
liveness update, and stale removal are conditional on the exact endpoint and
generation they observed.
- Runtime-session IDs and per-domain revisions prevent delayed snapshots from
rolling state back across restart or endpoint replacement. Change hints never
bypass the pinned pull and generation checks.
- A generation-conditional authenticated departure removes that peer's library
and Call-to-Play slices and publishes complete replacement views. Ordinary
short-lived QUIC connection closure is not roster departure.
- Protocol 8 is strict and current-only. Unknown fields, noncanonical typed IDs
or paths, duplicate/unsorted library rows, oversized domains, extra control
frames, and v7 messages are rejected instead of adapted.