# Quick 260917-kgc: Desktop-Client — Update in der App (tauri-plugin-updater) — Research **Researched:** 2026-09-17 **Domain:** Tauri 2 Updater-Plugin (Rust-API), NSIS-Update-Modus, AppImage-Ersetzung, minisign-Signatur im Cross-Bau, Endpunkt in der NestJS-API **Confidence:** HIGH fuer Plugin-/Bundler-/NSIS-Verhalten (Quelltext der installierten bzw. per `cargo fetch` geholten Crates gelesen), MEDIUM fuer den Cross-Bau des neuen TLS-Stacks (nur in der Pipeline beweisbar), LOW fuer SmartScreen-Verhalten des vom Updater gestarteten Installers Quellenkuerzel: `$REG` = `~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f`. Gelesene Crate-Staende: `tauri-plugin-updater-2.11.0` (aktuellste 2.x, 2026-08-31; 3.0.0-alpha wird von `"2"` nicht gewaehlt), `tauri-2.11.3`, `tauri-utils-2.9.3`, `tauri-bundler-2.9.4` (die CLI 2.11.3 lockt `tauri-bundler 2.9.3`, laut `tauri-cli-2.11.3/Cargo.lock` Z. 6606-6607; 2.9.4 ist der Patch dazu, der NSIS-Teil ist identisch aufgebaut), `tauri-cli-2.11.3` (Tarball von crates.io, Scratchpad), `reqwest-0.13.5`, `ring-0.17.14`, `semver-1`. Repo unveraendert (nur diese Datei). ## Summary Das offizielle `tauri-plugin-updater` 2.11.0 deckt genau den gewuenschten Ablauf ab, komplett von Rust aus: `app.updater_builder().endpoints(vec![url])?.version_comparator(..).build()?.check().await` liefert `Option`; `update.download_and_install(on_chunk, on_finish).await` laedt die Datei komplett in den Speicher, prueft die minisign-Signatur gegen `plugins.updater.pubkey`, und startet unter Windows den NSIS-Installer mit `/P /UPDATE /R /ARGS …` und beendet den eigenen Prozess per `std::process::exit(0)` — der Installer startet die App danach selbst neu (`.onInstSuccess` → `RunAsUser`). Unter Linux ersetzt das Plugin die laufende AppImage-Datei an Ort und Stelle (Pfad aus `APPIMAGE`), danach muss der Client selbst `app.restart()` rufen. Das Tray-Menue/`prevent_close`-Muster ist seit Tauri-PR #12313 (`RESTART_EXIT_CODE`) kein Hindernis mehr; unser Run-Handler laesst `code: Some(..)` bereits durch. Zwei harte Vorgaben ergeben sich aus dem Quelltext: (1) `plugins.updater.pubkey` MUSS in `tauri.conf.json` stehen — sonst bricht sowohl der Bau (`createUpdaterArtifacts: true` → CLI: „plugins > updater doesn't exist") als auch der App-Start ab (Plugin-Config-Deserialisierung, `pubkey: String` ohne Default). `endpoints` darf dagegen fehlen (`#[serde(default)]`) und wird zur Laufzeit gesetzt. (2) Mit `createUpdaterArtifacts: true` und gesetztem `pubkey` verlangt die CLI beim `tauri build` zwingend `TAURI_SIGNING_PRIVATE_KEY` (Inhalt ODER Pfad) — ohne Schluessel bricht der Bau ab; der Ausweg fuer lokale Baue ist `tauri build --no-sign` (dann entsteht keine `.sig`). Die `.sig`-Dateien entstehen host-unabhaengig in der CLI (reines Rust/minisign), also auch im `cargo-xwin`-Cross-Bau. Der Versionsvergleich ist der eigentliche Fallstrick: `RemoteRelease.version` ist bereits ein `semver::Version` (kein Rohstring), `1.2.0-beta.38c1400` ist gueltig, aber `1.2.0-beta.0123456` NICHT (fuehrende Null in numerischem Prerelease-Identifier → Deserialisierung schlaegt fehl, Check liefert Err). Deshalb Commit-Stempel immer mit Praefix: `1.2.0-beta.g38c1400` (wie `git describe`). `version_comparator` ersetzt den Standardvergleich (`release.version > current`) vollstaendig. **Primary recommendation:** `tauri-plugin-updater = "2"` mit Standard-Features (rustls+ring, Plattform-Zertifikatspruefung) einbauen; neuer API-Endpunkt `GET /desktop/update?target=&arch=¤t=&base=` (dynamisches Format, absolute `url` aus validiertem `base`, 204 ohne signiertes Paket); `desktop-collect.sh` schreibt den `.sig`-Inhalt als Feld `signature` ins Manifest; Version im Manifest bleibt `X.Y.Z`, der Endpunkt bildet `X.Y.Z` (live) bzw. `X.Y.Z-beta.g` (beta); eigener `version_comparator` (Basisversion groesser ODER Beta-Stempel verschieden). Schluesselpaar per `tauri signer generate -w`, privater Schluessel + Passwort als Gitea-Secrets (per `PUT /api/v1/repos/{owner}/{repo}/actions/secrets/{name}`, in Gitea 1.26.2 vorhanden), oeffentlicher Schluessel in `tauri.conf.json`. ## Antworten auf die acht Fragen ### 1. Plugin-API (Rust) - `UpdaterExt` ist fuer jeden `Manager` implementiert (`App`, `AppHandle`, Fenster): `fn updater_builder(&self) -> UpdaterBuilder` und `fn updater(&self) -> Result` [VERIFIED: `$REG/tauri-plugin-updater-2.11.0/src/lib.rs:58-121`]. `updater_builder()` haengt automatisch an: Windows `current_exe_args` (fuer `/ARGS`), Linux `executable_path(APPIMAGE)` (Z. 108-112), `on_before_exit(|| app_handle.cleanup_before_exit())` (Z. 116-118). - `UpdaterBuilder`: `version_comparator(Fn(Version, RemoteRelease) -> bool + Send + Sync + 'static)` (Z. 211), `endpoints(Vec) -> Result` (Z. 224, validiert https), `header(k, v) -> Result`, `headers(HeaderMap)`, `timeout(Duration)`, `pubkey(..)`, `installer_arg(s)`, `restart_after_install(bool)` (Windows, Default `true`), `configure_client(Fn(reqwest::ClientBuilder) -> ClientBuilder)`, `build() -> Result`; `build()` gibt `Error::EmptyEndpoints`, wenn weder Laufzeit- noch Config-Endpunkte da sind (Z. 365-371) [VERIFIED: `updater.rs:211-388`]. - `Updater::check(&self).await -> Result>` (Z. 432). `Update` (pub-Felder): `body: Option`, `current_version: String`, `version: String`, `date: Option`, `target: String`, `download_url: Url`, `signature: String`, `raw_json: serde_json::Value`, `timeout`, `proxy`, `no_proxy`, `headers` (Z. 642-673). `Update: Clone + Resource` (also `Send + Sync`) — kann in `app.manage(Mutex>)` liegen [VERIFIED: `updater.rs:642-676`]. - `update.download(on_chunk: FnMut(usize, Option), on_finish: FnOnce()) -> Result>` (ganze Datei im Speicher, danach `verify_signature`, Z. 680-742); `update.install(bytes)`; `update.download_and_install(on_chunk, on_finish).await` (Z. 761-768). Doc-Kommentar: „Windows: This function exits the app after launching the updater installer successfully — macOS / Linux: You need to relaunch the app" (Z. 754-760) [VERIFIED]. - `RemoteRelease { version: semver::Version, notes: Option, pub_date: Option, data: RemoteReleaseInner }` mit `RemoteReleaseInner::Dynamic(ReleaseManifestPlatform { url: Url, signature: String })` oder `Static { platforms: HashMap }` (Z. 70-96) [VERIFIED]. - `plugins.updater.endpoints` in `tauri.conf.json` ist optional (`#[serde(default)] pub endpoints: Vec`, `config.rs:136`); Laufzeit-`endpoints()` ersetzt die Config-Liste (`build()`: `self.endpoints.unwrap_or_else(|| config.endpoints)`, `updater.rs:366-368`) [VERIFIED]. - `plugins.updater.pubkey` ist Pflicht: `pub pubkey: String` ohne Default (`config.rs:137`). Fehlt `plugins.updater` ganz, uebergibt Tauri `JsonValue::Null` (`$REG/tauri-2.11.3/src/plugin.rs:1007`, `unwrap_or_default()`) → `serde_json::from_value` scheitert → „Error deserializing 'plugins.updater' within your Tauri configuration" (`plugin.rs:800-805`) → unser `.build(...).expect(..)` in `lib.rs:287-288` panict beim Start. Zusaetzlich verlangt die CLI beim Bau mit `createUpdaterArtifacts != false` den Block (`tauri-cli-2.11.3/src/interface/rust.rs:855-870`: „failed to get updater configuration: plugins > updater doesn't exist") [VERIFIED]. - Capabilities: Die Permissions (`updater:default` = `allow-check`, `allow-download`, `allow-install`, `allow-download-and-install`, `$REG/tauri-plugin-updater-2.11.0/permissions/default.toml`) gaten nur die vier `#[tauri::command]`-Handler fuer JS (`lib.rs:236-241`). Der Rust-Aufruf ueber `UpdaterExt` laeuft am ACL vorbei — `capabilities/default.json` bleibt unveraendert [VERIFIED]. ### 2. Antwortformat des Endpunkts - Dynamisches Format: JSON mit `version` (alias `name`), optional `notes`, optional `pub_date` (RFC 3339, sonst Deserialisierungsfehler), `url`, `signature`; fehlt `platforms`, wird `url`+`signature` verlangt („the `url` field was not set on the updater response") [VERIFIED: `updater.rs:1454-1497`]. HTTP 204 → `Ok(None)` (Z. 531-534). Andere Nicht-2xx-Status werden nur geloggt; ohne parsebare Antwort endet `check()` mit `Error::ReleaseNotFound` (Z. 573) [VERIFIED]. Doku: 204 „No Content", Felder `url`/`version`/`signature` Pflicht [CITED: https://v2.tauri.app/plugin/updater/]. - `url` ist typisiert `url::Url` → muss absolut sein (relative Pfade wie `/desktop/download/windows` scheitern beim Parsen) [VERIFIED: `updater.rs:72-76`]. Unser `/desktop/latest` liefert heute bewusst relative URLs (`desktop.service.ts`, `getLatest()`) — fuer den Updater braucht es einen eigenen Endpunkt mit absoluter URL (siehe Frage 6). - `version` muss gueltiges SemVer sein; ein fuehrendes `v` wird abgeschnitten (`parse_version`, Z. 1514-1521). `version_comparator` bekommt den **geparsten** `semver::Version` (kein Rohstring); Prerelease liegt in `release.version.pre`. Probe (Scratchpad, semver 1.x): `1.2.0-beta.38c1400` OK, `1.2.0-beta.1234567` OK, `1.2.0-beta.0123456` → `Err("invalid leading zero in pre-release identifier")`, `1.2.0-beta.g0123456` OK, `1.2.0-beta.g38c1400 < 1.2.0` = true [VERIFIED: Probe-Ausgabe, `scratchpad/semver-probe`]. Ein 7-stelliger Git-SHA kann rein numerisch mit fuehrender Null sein (≈0,4 % der Commits) → **immer `g`-Praefix**. - Client-Version: `current_version = app.package_info().version` (`updater.rs:197`), also `1.2.0` aus `tauri.conf.json` (`desktop-version.sh` schreibt reines X.Y.Z, D-07) [VERIFIED]. - Platzhalter in der Endpunkt-URL: `{{current_version}}`, `{{target}}`, `{{arch}}`, `{{bundle_type}}` — werden sowohl im Pfad (URL-kodiert `%7B%7B…%7D%7D`) als auch in Query-Parametern ersetzt (`updater.rs:473-486`). Werte: `target` = `linux` | `darwin` | `windows`; `arch` = `i686` | `x86_64` | `armv7` | `aarch64` | `riscv64`; `bundle_type` = `nsis` | `appimage` | `msi` | `deb` | `rpm` | `app` | `unknown` (`updater.rs:1395-1421`, `installer_for_bundle_type`). Windows x64 → `windows`/`x86_64`, AppImage x64 → `linux`/`x86_64` [VERIFIED]. - Der Request traegt `Accept: application/json` (Check) bzw. `application/octet-stream` (Download) und User-Agent `tauri-plugin-updater/2.11.0`; der Download liest `Content-Length` fuer die Fortschrittsanzeige (`updater.rs:434-437, 687-690, 722-727`) [VERIFIED]. Unser `StreamableFile` setzt `length: entry.size` → `Content-Length` vorhanden [VERIFIED: `apps/api/src/desktop/desktop.controller.ts`, `download()`]. ### 3. Artefakte und Signatur - `bundle.createUpdaterArtifacts: true` (Typ `Updater::Bool`; `"v1Compatible"` ist `Updater::String`, `$REG/tauri-utils-2.9.3/src/config.rs:1532-1571`). Im v2-Modus erzeugt der Bundler fuer NSIS/AppImage **kein** Zip/Tar mehr („Self contained updater, no need to zip", `$REG/tauri-bundler-2.9.4/src/bundle.rs:206-239`); der NSIS-Installer wird einmal mit `updater=false` gebaut (`bundle.rs:178`) — die normale `Tessera_X.Y.Z_x64-setup.exe` IST das Update-Artefakt, ebenso `Tessera_X.Y.Z_amd64.AppImage` [VERIFIED]. - Signatur passiert in der CLI nach dem Buendeln (`tauri-cli-2.11.3/src/bundle.rs:221, 226-314`, `sign_updaters`): fuer jedes Bundle vom Typ Nsis/Msi/AppImage/Deb/Rpm/Updater wird `.sig` daneben geschrieben (`helpers/updater_signature.rs:117-160`: Extension + `.sig`, Inhalt = Base64 der minisign-Signaturbox). Ergebnis: `target/x86_64-pc-windows-msvc/release/bundle/nsis/Tessera_1.2.0_x64-setup.exe.sig` und `target/release/bundle/appimage/Tessera_1.2.0_amd64.AppImage.sig` [VERIFIED]. Kein `cfg(windows)`/Host-Gating in `sign_updaters` → im `cargo-xwin`-Cross-Bau entsteht die `.sig` genauso [VERIFIED: Code-Lesung; Pipeline-Nachweis steht aus]. - Umgebung: `TAURI_SIGNING_PRIVATE_KEY` — Wert ist Inhalt ODER Pfad (Code prueft `Path::exists()`, `bundle.rs:277-289`); `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` optional — fehlt sie, gilt mit `--ci`/`CI`-Umgebung leeres Passwort, sonst interaktive Abfrage (`bundle.rs:272-275, 290-292`). Die vom Generator ausgegebene Variable `TAURI_SIGNING_PRIVATE_KEY_PATH` (`signer/generate.rs:60`) wird im Bau-Code NICHT gelesen — nur `TAURI_SIGNING_PRIVATE_KEY` [VERIFIED]. `pubkey` in `tauri.conf.json` darf Inhalt oder Dateipfad sein (`bundle.rs:261-269`); beim Signieren warnt die CLI, wenn `keynum` von privatem und oeffentlichem Schluessel nicht zusammenpassen (Z. 304-306) [VERIFIED]. - Generator: `pnpm --filter @tessera/desktop exec tauri signer generate -w [-p ] [--ci] [--force]` → schreibt `` (privat) und `.pub` (`signer/generate.rs:14-49`, `updater_signature.rs:61-72`) [VERIFIED]. Doku-Form: `npm run tauri signer generate -- -w ~/.tauri/myapp.key` [CITED: v2.tauri.app/plugin/updater/]. - Bau ohne Schluessel in der Umgebung, aber `createUpdaterArtifacts: true` + `pubkey` gesetzt → **Abbruch**: „A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable." (`bundle.rs:277-279`). Ausweg: `tauri build --no-sign` (`build.rs:81-88`, `bundle.rs:255-258`: „Updater signing is skipped due to --no-sign flag") → keine `.sig` [VERIFIED]. Konsequenz fuer das Konzept „ohne Schluessel → Feld fehlt → 204": funktioniert nur mit `--no-sign` (lokale Proben, CI-Fallback), nicht durch blosses Weglassen der Variable. ### 4. Windows-Installation durch den Updater - Ablauf `install_inner` (`updater.rs:835-877`): Bytes in `%TEMP%\Tessera--updater-\Tessera--installer.exe` schreiben (`make_temp_dir`/`write_to_temp`, Z. 956-1020; Ordner bleibt liegen, `.keep()`), `on_before_exit` → `cleanup_before_exit()` (Tray-Icons leeren, Fenster verstecken, `$REG/tauri-2.11.3/src/app.rs:1108-1120`), dann `ShellExecuteW(NULL, "open", , , SW_SHOW)`; Fehler (<=32) wird zurueckgegeben; sonst `std::process::exit(0)` [VERIFIED]. - Parameter (`updater_parameters`, Z. 879-907): `nsis_args(install_mode)` + `/UPDATE` + bei `restart_after_install` (Default `true`) `/R` (nicht bei `basicUi`) + `/ARGS ` + `installerArgs` aus Config. `installMode`: `passive` → `/P` (Default), `quiet` → `/S`, `basicUi` → keine Flags (`config.rs:41-56`; Config-Schluessel `plugins.updater.windows.installMode` / `installerArgs`, camelCase, Z. 80-92) [VERIFIED]. Doku: passive = kleines Fenster mit Fortschrittsbalken, quiet = keine Rueckmeldung [CITED: v2.tauri.app/plugin/updater/]. - NSIS-Template (`$REG/tauri-bundler-2.9.4/src/bundle/windows/nsis/installer.nsi`): `.onInit` liest `/P`, `/NS`, `/UPDATE` (Z. 477-491); im Update-Modus wird bei gleicher/hoeherer Version ohne Deinstallation direkt installiert (`PageLeaveReinstall`, Z. 318-321 „In update mode, always proceeds without uninstalling"); Startmenue-/Desktop-Verknuepfungen werden im Update-Modus nicht neu angelegt (Z. 937-941, 966-970); Registry-Werte bleiben erhalten (Z. 863-864); `.onInstSuccess` startet die App nur bei `/P` oder `/S` und nur mit `/R`: `nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "$R0"` mit `$R0` = Wert hinter `/ARGS` (Z. 743-754) [VERIFIED]. → **`app.restart()` ist unter Windows nicht noetig und wird nie erreicht** (Prozess endet in `install`); unter Linux ist es Pflicht (Frage 5). Der Aufruf nach `download_and_install` ist trotzdem korrekt, weil plattformuebergreifend harmlos. - Laufender Prozess/Tray: `CheckIfAppIsRunning` (`utils.nsh:22-62`) sucht `tessera-desktop.exe` — bei `INSTALLMODE == currentUser` per `FindProcessCurrentUser`, killt ohne Rueckfrage bei `/P` oder `/S` (`KillProcessCurrentUser`, Sleep 500 ms). Da der Updater den Prozess bereits per `exit(0)` beendet hat, greift das nur im Rennen; ein verstecktes Fenster oder das Tray-Symbol spielen keine Rolle (Prozess-, nicht Fenster-Suche) [VERIFIED]. - `installMode: currentUser` (unser `tauri.conf.json:48`): Installer laeuft ohne UAC im Nutzerkontext, `ShellExecuteW "open"` verlangt keine Erhoehung; `SetContext`/`SHCTX` bleibt HKCU (`installer.nsi:105-112`) [VERIFIED: Template; Bedienprobe auf der Windows-VM ist der Nachweis]. - Bekannte Faelle: tauri#11392 („App::restart does not restart after update.download_and_install", Tray + `prevent_close`) wurde durch tauri PR #12313 (`RESTART_EXIT_CODE`, `restart_on_exit`) behoben [CITED: https://github.com/tauri-apps/tauri/issues/11392, https://github.com/tauri-apps/tauri/pull/12313]; im installierten Tauri 2.11.3 enthalten: `restart()` von einem Nebenthread setzt `restart_on_exit` und ruft `request_exit(RESTART_EXIT_CODE)` (= `i32::MAX`), was `RunEvent::ExitRequested { code: Some(i32::MAX) }` ausloest (`app.rs:77, 588-611, 1434-1437`) — unser Handler in `lib.rs:296-301` blockt nur `code: None` → Neustart geht durch [VERIFIED]. tauri#7560 („NSIS quiet update do not restart") ist „closed as not planned" (2023, v1) — mit `/R` im heutigen Template gegenstandslos [CITED: https://github.com/tauri-apps/tauri/issues/7560]. `window-state` speichert beim harten `exit(0)` unter Windows nicht (kein `RunEvent::Exit`) — Fensterposition kann nach einem Update einmal verloren gehen [ASSUMED, aus Plugin-Semantik abgeleitet]. ### 5. Linux AppImage - `updater_builder()` setzt `executable_path` auf `app.env().appimage` (= Umgebungsvariable `APPIMAGE`, `$REG/tauri-utils-2.9.3/src/lib.rs:269-287`); `build()` nimmt unter Linux diesen Pfad als `extract_path` (`updater.rs:373-379`) [VERIFIED]. - `install_appimage` (`updater.rs:1047-1118`): sucht ein temporaeres Verzeichnis **auf demselben Dateisystem** wie die AppImage (Reihenfolge `std::env::temp_dir()`, `dirs::cache_dir()` = `~/.cache`, Elternordner der AppImage), verschiebt die laufende Datei per `rename` als Sicherung dorthin, schreibt die neuen Bytes unter dem alten Pfad, uebernimmt die alten Rechte (Ausfuehrbit), stellt bei Fehler die Sicherung zurueck; passt kein Tempordner → `Error::TempDirNotOnSameMountPoint` [VERIFIED]. Braucht also Schreibrecht auf Datei UND Ordner. Laeuft die AppImage aus `~/Downloads`, ist das gegeben (Tempordner `~/.cache` liegt auf demselben Dateisystem wie `$HOME`; ist `/tmp` ein tmpfs, scheitert nur der erste Kandidat). Bei einer AppImage unter `/opt` ohne Schreibrecht schlaegt das Update fehl — Fehlertext im Tray zeigen. - Neustart: `app.restart()` → `tauri::process::restart` → `current_binary` liefert unter Linux **nur** den `APPIMAGE`-Pfad (`$REG/tauri-2.11.3/src/process.rs:48-56, 74-89`), startet also die neue Datei und beendet den alten Prozess [VERIFIED]. Der alte Prozess haelt die geloeschte Inode offen — unkritisch. - Ohne `APPIMAGE` (nackte Binary aus `target/release/`, `tauri dev`) faellt `extract_path` auf `current_exe()` und wuerde die Binary ueberschreiben — Update-Pfad im Dev-Modus nicht ausloesen (nur `check()` testen). ### 6. Integration bei uns - **Absolute Download-URL:** Die API sieht die Anfrage ueber NPM → Next.js-Rewrite (`apps/web/next.config.ts:34-43`, Ziel `http://api:3001`). Next' Proxy setzt `x-forwarded-host: req.headers.host` (`node_modules/.pnpm/next@15.5.19_*/node_modules/next/dist/server/lib/router-utils/proxy-request.js:26-36`) [VERIFIED], das Schema (`x-forwarded-proto`) kaeme nur aus NPMs Header-Vorlage [ASSUMED]. Verlaesslicher und ohne Proxy-Annahme: **der Client haengt `base=` an** (er kennt sie aus dem Store), die API validiert (`URL`-Parse, nur `http`/`https`, keine Credentials, nur Origin uebernehmen, Pfad verwerfen) und bildet `url = ${origin}/api-proxy/desktop/download/${platform}`. Reflektierte Eingabe ist unkritisch: der Client verifiziert die Signatur, eine fremde URL kann nur zu einem fehlgeschlagenen Download fuehren. Alternative ohne API-Aenderung: `Update.download_url` ist ein `pub`-Feld und darf nach `check()` vom Client auf `api_url(server, "/desktop/download/")` gesetzt werden (`updater.rs:655`) [VERIFIED] — als Notnagel dokumentieren, nicht als Hauptweg. - **Neuer Endpunkt** (statt `/desktop/latest` zu aendern, das die Web-UI weiter mit relativen URLs nutzt): `GET /desktop/update?target=&arch=¤t=&base=` (`@Public()`, statische Route VOR `download/:platform` — Route-Order-Falle, Memory `project_nest_route_order.md`). Logik: `target` per Whitelist auf Plattform (`windows`→`windows`, `linux`→`linux`), `arch` muss `x86_64` sein (sonst 204); Manifest lesen; fehlt Eintrag oder `signature` → **204**; sonst 200 mit `{ version, pub_date: buildTime, notes: "channel=;commit=", url, signature }`, `version` = `manifest.version` (live) bzw. `${manifest.version}-beta.g${manifest.commit}` (beta). Die 204-Entscheidung „gleicher Stand" bleibt beim Client-Comparator (die API kennt den Client-Commit nicht; den Placeholder `{{current_version}}` nur zum Loggen mitschicken). Optional zusaetzlich `&commit=` und die API antwortet 204 bei gleichem Commit — spart einen Download-Link, aendert an der Sicherheit nichts. - **`desktop-collect.sh`:** neben `*.exe`/`*.AppImage` die zugehoerige `*.sig` suchen (`find -name '*-setup.exe.sig'` / `'*.AppImage.sig'`; die bestehenden `-name '*.exe'`/`'*.AppImage'`-Zaehler matchen `.sig` nicht) und den Dateiinhalt (einzeilige Base64, ~200 Zeichen) per `--arg linuxSig "$(cat …)"` als `files..signature` in `manifest.json` schreiben; Datei selbst nicht kopieren (kein Nutzen, die API liefert JSON). Fehlt die `.sig` (Bau mit `--no-sign`), Feld weglassen und eine Warnzeile loggen; bei `GITHUB_REF` = Tag oder `main` hart abbrechen (Signatur ist dort Pflicht). `desktop.service.ts`: `isValidManifestFileEntry` um optionales `signature: string` erweitern, `DesktopManifestFile` in `packages/shared/src/index.ts:29-33` ebenso. - **Skip-Mechanismus (`desktop-stamp.sh check`):** prueft Groesse/sha256 der beiden Dateien und liest die Manifest-Felder — das `signature`-Feld liegt im gecachten Manifest und wird mit uebernommen; keine `.sig`-Datei zu pruefen. Zwei Ergaenzungen: `check` verlangt fuer beide Plattformen ein nicht-leeres `files.

.signature` (alter Cache-Stand ohne Signatur → `no_reuse`), und ein Schluesselwechsel wird automatisch zum Neubau, weil `pubkey` in `apps/desktop/src-tauri/tauri.conf.json` liegt und `apps/desktop` Teil von `DESKTOP_PATHS` ist (`desktop-stamp.sh:50`) [VERIFIED]. Die Signatur gilt fuer die Bytes der `-setup.exe`; `cp` in `desktop-collect.sh` aendert nichts daran — niemals nachtraeglich signieren/patchen. - **Gitea-Secrets per API:** Gitea 1.26.2 (`curl localhost:3002/api/v1/version`) bietet `PUT /api/v1/repos/{owner}/{repo}/actions/secrets/{secretname}` mit Body `{"data": "", "description": ""}`; Antwort 201 (angelegt) / 204 (aktualisiert) [VERIFIED: `localhost:3002/swagger.v1.json`, `CreateOrUpdateSecretOption`, `required: ["data"]`; CITED: https://docs.gitea.com/api/1.26/operations/update-repo-secret/]. Zugriff: `reqToken()` + `reqOwner()` (Token-Inhaber muss Repo-Eigentuemer sein; Kategorie `repository` → `write:repository`) [VERIFIED: `routers/api/v1/api.go` (release/v1.26) Z. 947-958, 1225]. Der vorhandene `REGISTRY_TOKEN` hat `repository: write` (docs/ci-cd-setup.md Z. 88) — genuegt, sofern er dem Repo-Eigentuemer `schalli` gehoert. Aufruf vom Dev-Host ueber `localhost:3002` (Memory `project_ci_registry_push.md`), nie ueber `git.vicolab.de`. Secrets: `TAURI_SIGNING_PRIVATE_KEY` (Dateiinhalt, eine Base64-Zeile) und `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` (Schluessel MIT Passwort erzeugen — ein leerer Secret-Wert ist in Gitea nicht sicher moeglich [ASSUMED]; `CI=true` als Fallback fuer leeres Passwort setzt act_runner wie GitHub [ASSUMED]). - **ci.yml:** Job `desktop` bekommt `env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}` und `TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.… }}` an beiden `tauri build`-Schritten. Der Skip-Pfad braucht die Secrets nicht. `publish-release.sh` kann die `.sig` optional als Release-Anhang mitgeben — nicht noetig, das Manifest traegt sie. ### 7. Versionsvergleich Beta - `version_comparator` ersetzt den Standard vollstaendig: `let should_update = match self.version_comparator { Some(c) => c(self.current_version.clone(), release.clone()), None => release.version > self.current_version }` (`updater.rs:576-579`) [VERIFIED]. Ohne eigenen Vergleich gilt SemVer: `1.2.0-beta.g38c1400 < 1.2.0` (Probe) → ein Beta-Client (Version `1.2.0`) saehe nie einen neueren Beta-Bau. Standardverhalten nur fuer Live sinnvoll. - Empfohlene Logik (spiegelt `lib.rs:259-261`, D-07/WR-02): ```rust // Source: eigene Ableitung aus updater.rs:576-579 + semver-Probe fn is_newer(current: &semver::Version, remote: &semver::Version, app_commit: &str) -> bool { let base = |v: &semver::Version| (v.major, v.minor, v.patch); if base(remote) > base(current) { return true; } if base(remote) < base(current) { return false; } // gleiche X.Y.Z: Beta-Stempel "beta.g" vs. env!("APP_COMMIT") match remote.pre.as_str().strip_prefix("beta.g") { Some(sha) => !app_commit.is_empty() && sha != app_commit, None => false, // Live, gleiche Version: kein Update } } ``` `current` ist immer reines X.Y.Z (D-07) — `current.pre` ist leer, darum genuegt der Tupelvergleich. `app_commit` = `env!("APP_COMMIT")` (7-stellig, `build.rs:18-32`) und `manifest.commit` = `git rev-parse --short=7` (`desktop-collect.sh:78`) — gleiches Format [VERIFIED]. Leerer `APP_COMMIT` (Quell-Tarball) → nur Versionsvergleich, wie heute. - Reine Funktion in `lib.rs` + Tests (Basis groesser, Basis kleiner, gleiche Basis/anderer Stempel, gleicher Stempel, Live ohne Pre, leerer Commit) — wie die bestehenden `mod tests`. ### 8. Gotchas (Abhaengigkeiten, TLS, Groesse, Zertifikate) - Plugin-Abhaengigkeiten: `reqwest = "0.13"` (Features `json`, `stream`, default-features = false), Standard-Features `rustls-tls` (= `reqwest/rustls-no-provider` + `rustls 0.23` mit `ring`), `system-proxy`, `zip`; `tauri = "2.10"` (unser 2.11.3 passt) [VERIFIED: `$REG/tauri-plugin-updater-2.11.0/Cargo.toml:66-116`]. Unser `reqwest = "0.12"` bleibt daneben bestehen → zwei reqwest-Majors und zwei TLS-Stacks (native-tls/schannel bzw. OpenSSL + rustls/ring) im Binary. Mehr Bauzeit/Groesse (Groessenordnung wenige MB [ASSUMED]), funktional unproblematisch. `rustls-no-provider` zieht `rustls-platform-verifier` (`$REG/reqwest-0.13.5/Cargo.toml:106-109`) → Zertifikatspruefung ueber den Betriebssystem-Speicher (Windows-Zertifikatspeicher, Linux CA-Bundle; das Plugin setzt unter Linux notfalls `SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt`, `updater.rs:440-448`) [VERIFIED] — Let's-Encrypt-Zertifikate von alpha/live werden akzeptiert. - Cross-Bau-Risiko: `ring 0.17.14` ist neu im Abhaengigkeitsgraphen. Das Crate liefert fuer `x86_64-pc-windows-msvc` vorassemblierte `.o`-Objekte mit (`$REG/ring-0.17.14/build.rs:340-349, 430-445`, `pregenerated/*-x86_64-nasm.o`) — kein `nasm` noetig; die C-Teile baut `cc` mit dem Clang aus `cargo-xwin` [VERIFIED: Code-Lesung; der tatsaechliche xwin-Bau ist nur in der Pipeline beweisbar, `cargo-xwin` ist auf dem Dev-Host nicht installiert]. Fallback bei Bauproblemen: `tauri-plugin-updater = { version = "2", default-features = false, features = ["native-tls", "system-proxy"] }` (schannel unter Windows, OpenSSL unter Linux — `libssl-dev` steht bereits in der apt-Liste, ci.yml Z. 120-125). **Nicht** das Projekt-`reqwest` auf 0.13 heben: dessen Default `default-tls = rustls` zieht `aws-lc-rs` (cmake/nasm-Bau), ein echtes xwin-Risiko [VERIFIED: `reqwest-0.13.5/Cargo.toml:45-51, 101-105`]. - Kein `tauri-plugin-http` noetig; der Updater bringt seinen Client mit [VERIFIED: keine Abhaengigkeit in `Cargo.toml`]. - Endpunkt-Schema: im Release-Bau wird `http://` abgelehnt (`Error::InsecureTransportProtocol`, `config.rs:160-179`) — `endpoints()` gibt dann `Err`; Fehler loggen, Tray-Eintrag gesperrt lassen. `dangerousInsecureTransportProtocol`/`dangerousAcceptInvalidCerts`/`dangerousAcceptInvalidHostnames` existieren als Config-Schalter (`config.rs:105-118`) — nicht setzen, nur im Handbuch als „nicht vorgesehen" nennen [VERIFIED]. Der Store erlaubt heute `http`-Adressen (`check_server`); ein Kunde mit `http://` bekommt einfach kein In-App-Update (Hinweis-Download bleibt). - `download()` haelt die ganze Datei (~100 MB) im RAM, bevor sie geschrieben wird (`updater.rs:730-742`) [VERIFIED] — akzeptabel. Next' Rewrite-Proxy hat ein 30-s-**Inaktivitaets**-Timeout (`proxyTimeout` → `ClientRequest.setTimeout`, `next/dist/compiled/http-proxy`), kein Gesamtlimit — ein laufender Stream bricht nicht ab [VERIFIED]; NPM-Groessengrenzen wie in Kap. 10 des Betriebshandbuchs. - SmartScreen: Die vom Updater geschriebene `…-installer.exe` erhaelt keine Mark-of-the-Web (kein Browser-Download, `std::fs::write` ohne `Zone.Identifier`) — voraussichtlich kein SmartScreen-Dialog beim In-App-Update, D-09 (keine Code-Signierung) bleibt bestehen [ASSUMED — auf der Windows-VM pruefen]. - Windows-Reste: der Tempordner `%TEMP%\Tessera--updater-*` wird nicht aufgeraeumt (`.keep()`, Z. 956-964) — ~100 MB je Update; im Betriebshandbuch erwaehnen, kein Handlungsbedarf. - `Update::install` unter Windows beendet den Prozess aus dem Tokio-Thread heraus; unser `RunEvent::ExitRequested`-Handler wird dabei nicht durchlaufen (kein `prevent_exit`-Konflikt) [VERIFIED: `updater.rs:876`]. ## Architektur / Datenfluss ``` Client-Start / Serverwechsel (spawn_version_check, jn2) └─ updater_builder().endpoints([ {server}/api-proxy/desktop/update?target={{target}}&arch={{arch}}¤t={{current_version}}&base={server} ]) .version_comparator(is_newer(.., APP_COMMIT)).timeout(..).build()?.check().await │ NPM → Next /api-proxy → API GET /desktop/update │ manifest.json (files.

.signature vorhanden?) ── nein → 204 → Ok(None) │ ja → 200 { version: X.Y.Z | X.Y.Z-beta.g, url: {base}/api-proxy/desktop/download/

, signature, pub_date, notes } ├─ Some(update) → app.state::().set(update); Tray "update" = "Version … installieren" / "Neuen Beta-Stand installieren", enabled └─ None/Err → Tray gesperrt (Err loggen) Tray-Klick "update" └─ update.download_and_install(|chunk,total| Tray-Text "… lädt 42 %", || Tray-Text "… wird installiert").await ├─ Windows: %TEMP%\…installer.exe, ShellExecuteW "/P /UPDATE /R /ARGS", exit(0) → NSIS installiert, RunAsUser startet App neu └─ Linux: AppImage in place ersetzt → app.restart() (RESTART_EXIT_CODE, laeuft an prevent_exit vorbei) Fehler → Tray-Text zurueck + Benachrichtigung mit Fehlertext; Fallback bleibt der Browser-Download (heutiger Weg) ``` Aufsetzpunkt ist der Stand nach Quick jn2 (`TrayIconBuilder::with_id("main")`, `TrayItems { connected, update }`, `apply_server`, `spawn_version_check`): `spawn_version_check` tauscht den reqwest-Aufruf gegen `check()`, `TrayItems` bekommt keinen neuen Handle, aber `app.manage(PendingUpdate(Mutex>))`; der Menue-Zweig `"update"` startet den Download statt `opener`. Der Browser-Link (Einstellungen → Desktop-App) bleibt als zweiter Eintrag oder als Fallback bei Fehler erhalten — Produktentscheidung fuer den Plan. ## Code-Skizzen ### tauri.conf.json (Ergaenzung) ```json // Source: v2.tauri.app/plugin/updater/ + config.rs:80-92 (Schluesselnamen camelCase) "bundle": { "createUpdaterArtifacts": true, ... }, "plugins": { "updater": { "pubkey": "", "windows": { "installMode": "passive" } } } ``` Kein `endpoints`-Eintrag (Laufzeit). `pubkey` ist oeffentlich und gehoert ins Repo. ### Cargo.toml ```toml # Source: $REG/tauri-plugin-updater-2.11.0/Cargo.toml (Features); Legitimitaet: OK (crates.io seit 2023, 361k/Woche, tauri-apps/plugins-workspace) tauri-plugin-updater = "2" ``` Per `cargo add tauri-plugin-updater@2` einpflegen (Cargo.lock konsistent, wie 18-04 mit opener). Cargo waehlt 2.11.0; `3.0.0-alpha.*` wird von `"2"` nicht erfasst. ### lib.rs (Kern) ```rust // Source: updater.rs:211-233, 365-388, 432, 761; lib.rs:58-121 (Plugin); Doku-Beispiel v2.tauri.app/plugin/updater/ use tauri_plugin_updater::UpdaterExt; // in run(): .plugin(tauri_plugin_updater::Builder::new().build()) async fn check_for_update(app: &AppHandle, server: &str) -> tauri_plugin_updater::Result> { let endpoint = format!( "{}?target={{{{target}}}}&arch={{{{arch}}}}¤t={{{{current_version}}}}&base={}", api_url(server, "/desktop/update"), urlencoding_of(server) // percent-encodieren, z. B. mit url::form_urlencoded ); let app_commit = env!("APP_COMMIT"); app.updater_builder() .endpoints(vec![endpoint.parse()?])? // Err bei http:// im Release-Bau .timeout(Duration::from_secs(15)) .version_comparator(move |current, release| is_newer(¤t, &release.version, app_commit)) .build()? .check() .await } // Tray-Klick "update": // let update = state.take(); update.download_and_install(|got, total| {..}, || {..}).await?; app.restart(); ``` `{{target}}` usw. muessen wortwoertlich in der URL stehen — das Plugin ersetzt sie auch in Query-Parametern (`updater.rs:481-486`). ### NestJS `GET /desktop/update` (Skizze) ```ts // Source: eigene Ableitung; Formatvorgabe updater.rs:1454-1497 (Felder version/url/signature/pub_date/notes) @Public() @Get('update') // VOR download/:platform registrieren (Route-Order) update(@Query() q, @Res({ passthrough: true }) res) { const platform = q.target === 'windows' ? 'windows' : q.target === 'linux' ? 'linux' : null; const manifest = this.desktopService.getManifest(); const entry = platform && manifest?.files[platform]; const origin = this.desktopService.safeOrigin(q.base); // URL-Parse, http/https, nur origin if (!entry?.signature || !origin || q.arch !== 'x86_64') { res.status(204); return; } const version = manifest.channel === 'beta' ? `${manifest.version}-beta.g${manifest.commit}` : manifest.version; return { version, pub_date: manifest.buildTime, notes: `channel=${manifest.channel};commit=${manifest.commit}`, url: `${origin}/api-proxy/desktop/download/${platform}`, signature: entry.signature }; } ``` ### CI-Schluessel anlegen (einmalig, Dev-Host) ```bash # Source: tauri-cli-2.11.3/src/signer/generate.rs:14-49; Gitea swagger.v1.json (updateRepoSecret) pnpm --filter @tessera/desktop exec tauri signer generate -w ~/.tauri/tessera-desktop.key -p '' # -> ~/.tauri/tessera-desktop.key (GEHEIM, ausserhalb des Repos) und ~/.tauri/tessera-desktop.key.pub (in tauri.conf.json) curl -sS -X PUT -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \ --data "$(jq -n --arg d "$(cat ~/.tauri/tessera-desktop.key)" '{data:$d}')" \ http://localhost:3002/api/v1/repos/schalli/tessera-ctl/actions/secrets/TAURI_SIGNING_PRIVATE_KEY # 201/204 ``` Verlust des privaten Schluessels = keine Updates mehr fuer installierte Clients („if you lose this key you will NOT be able to publish new updates" [CITED: v2.tauri.app/plugin/updater/]) — Sicherungsort im Betriebshandbuch festhalten (Kap. 10). Keine Passwort-Hinweise an den User (Memory `feedback_no_password_leak_warnings.md`). ## Common Pitfalls 1. **`pubkey` fehlt / Bau ohne Secret:** App startet nicht (Plugin-Config) bzw. Bau bricht ab („no private key"). Lokale Baue: `tauri build --no-sign`; die Skripte muessen den Fall „keine .sig" sauber als 204 abbilden, aber auf `main`/Tags hart abbrechen. 2. **Fuehrende Null im Commit-Stempel:** `1.2.0-beta.0123456` ist kein SemVer → `check()` liefert Err → nie ein Update. Immer `beta.g`. 3. **Relative `url`:** `url::Url` verlangt absolut; `/desktop/latest` bleibt fuer die Web-UI relativ, der Updater bekommt einen eigenen Endpunkt. 4. **Route-Shadowing:** `update` statisch VOR `download/:platform` (Unit-Tests fangen es nicht — Memory). 5. **`http://`-Server im Release:** `endpoints()` gibt Err — abfangen, nicht `dangerousInsecureTransportProtocol` setzen. 6. **Alter Cache-Stand ohne Signatur:** `desktop-stamp.sh check` muss `signature` je Plattform verlangen, sonst wird ein unsigniertes Paket uebernommen und der Endpunkt liefert dauerhaft 204. 7. **Dev-Modus (`tauri dev`, keine AppImage):** `download_and_install` wuerde die Binary im `target/` ueberschreiben — Update nur mit gebautem AppImage/Installer ausloesen. 8. **Zwei reqwest-Majors:** `reqwest 0.12` (Projekt) und `0.13` (Plugin) koexistieren; Projekt-`reqwest` NICHT auf 0.13 heben (aws-lc-rs im Cross-Bau). 9. **Endpunkt-Placeholder in der Query:** `{{target}}` darf nicht vorab URL-kodiert werden — als Rohtext in den String; `url::Url::parse` laesst `{`/`}` in der Query stehen und das Plugin ersetzt beide Schreibweisen (`updater.rs:481-486`). ## Environment Availability | Abhaengigkeit | Benoetigt von | Verfuegbar | Version | Fallback | |---|---|---|---|---| | `cargo`/Rust (Dev-Host) | lokaler Bau/Tests | ✓ | cargo 1.96.0 | — | | `cargo-xwin` (Dev-Host) | lokaler Cross-Check | ✗ | — | Nachweis in der Pipeline (Runner installiert es, ci.yml Z. 156-160) | | `@tauri-apps/cli` | `tauri signer generate`, `--no-sign` | ✓ | 2.11.3 (pnpm) | — | | Gitea API | Secrets anlegen | ✓ | 1.26.2 (`localhost:3002`) | UI Settings → Actions → Secrets | | `tauri-plugin-updater` (crates.io) | Client | ✓ | 2.11.0, Legitimitaet OK | — | ## Validation Architecture | Property | Value | |---|---| | Rust | `cargo fmt --check && cargo check && cargo clippy && cargo test --lib` in `apps/desktop/src-tauri` (`CARGO_BUILD_JOBS=4`) | | API | Vitest 3.2.6, `pnpm --filter @tessera/api test -- desktop` (Datei `apps/api/src/desktop/desktop.service.spec.ts` erweitern) | | Skripte | lokale Probe `sh .gitea/scripts/desktop-collect.sh --require linux` nach `tauri build --bundles appimage` (mit Schluessel in der Umgebung) + `jq -e '.files.linux.signature' desktop-dist/manifest.json` | | Verhalten | Testart | Datei | |---|---|---| | `is_newer`: 6 Faelle (Basis >, <, gleich+anderer Stempel, gleicher Stempel, Live ohne Pre, leerer Commit) | unit (Rust) | `lib.rs mod tests` ❌ neu | | Endpunkt-URL-Bildung mit Rohplatzhaltern und kodiertem `base` | unit (Rust) | `lib.rs mod tests` ❌ neu | | `/desktop/update`: 204 ohne Manifest / ohne Signatur / falsche arch / ungueltiges base; 200 live vs beta-Version; `url` absolut aus `base` | unit (API) | `desktop.service.spec.ts` ❌ erweitern | | Manifest-Validierung akzeptiert optionales `signature` | unit (API) | `desktop.service.spec.ts` ❌ erweitern | | Ende-zu-Ende Linux: lokales AppImage (Version A) gegen lokale API mit Manifest (Version B) → Datei ersetzt, Neustart | manuell (Dev-Host, grafische Sitzung) | — | | Ende-zu-Ende Windows: CI-Paket auf der Test-VM, Tray-Klick → Fortschritt → Installer passiv → App neu gestartet, Tray zurueck, Version geaendert; SmartScreen-Verhalten notieren | manuell (Orchestrator/User, Windows-VM 8233) | — | ## Security Domain | ASVS | Gilt | Kontrolle | |---|---|---| | V5 Eingabevalidierung | ja | `target`/`arch` Whitelist, `base` nur `http(s)`-Origin ohne Credentials/Pfad; Dateiname weiterhin nur aus dem Manifest (T-18-01/02 bleiben) | | V6 Kryptografie | ja | minisign (Ed25519) durch das Plugin — nie eigene Signaturpruefung; privater Schluessel nur als Gitea-Secret, `.key` nie ins Repo (`.gitignore`-Eintrag `*.key` unter `apps/desktop/`) | | V10 Schadcode/Integritaet | ja | Signaturpruefung vor Installation (`updater.rs:740`); Transport nur https (Plugin-Zwang im Release) | | Muster | STRIDE | Mitigation | |---|---|---| | Manipuliertes Paket ueber MITM/kompromittierte API | Tampering | Signatur mit Offline-Schluessel; falsche Signatur → `Error::Minisign`, keine Installation | | Offene Weiterleitung ueber `base` | Spoofing | Origin-Validierung; selbst bei Missbrauch nur fehlgeschlagener Download (Signatur) | | Downgrade-Angriff (aelteres signiertes Paket) | Tampering | Comparator installiert nur hoehere Basisversion bzw. anderen Beta-Stempel; ein Angreifer braeuchte ohnehin den Schluessel | ## Assumptions Log | # | Annahme | Abschnitt | Risiko | |---|---|---|---| | A1 | Der xwin-Cross-Bau von `ring`/`rustls` gelingt mit dem vorhandenen Clang-Setup | 8 | Pipeline rot → Fallback `native-tls`-Feature (dokumentiert) | | A2 | Vom Updater gestartete Installer-EXE loest keinen SmartScreen-Dialog aus (keine MOTW) | 8 | Nur Bedienkomfort; Handbuchtext | | A3 | act_runner setzt `CI=true` (leeres Passwort-Fallback) | 6 | Umgangen durch explizites Passwort-Secret | | A4 | Gitea erlaubt keinen leeren Secret-Wert | 6 | Umgangen durch Schluessel mit Passwort | | A5 | NPM setzt `X-Forwarded-Proto` | 6 | Irrelevant, weil `base` vom Client kommt | | A6 | `window-state` speichert beim `exit(0)`-Update nicht | 4 | Kosmetik | | A7 | Binary-Zuwachs „wenige MB" durch zweiten HTTP/TLS-Stack | 8 | Kosmetik; nach erstem CI-Bau messen | ## Open Questions 1. **Produktfrage:** Soll der Browser-Download-Eintrag im Tray neben dem In-App-Update bleiben (Fallback), oder nur bei Fehler erscheinen? Empfehlung: ein Eintrag, der bei Fehlschlag des In-App-Updates die Einstellungsseite oeffnet. 2. **Wo liegt die Sicherung des privaten Schluessels?** (Betriebshandbuch Kap. 10; nicht im Repo, nicht auf dem Runner.) 3. **Erster Rollout:** Bereits installierte Clients (1.2.0 ohne Plugin) koennen sich nicht selbst aktualisieren — einmal noch der Browser-Weg; im CHANGELOG nennen. ## Sources ### Primary (HIGH) - `$REG/tauri-plugin-updater-2.11.0/src/{lib.rs,updater.rs,config.rs,error.rs}`, `Cargo.toml`, `permissions/default.toml`, `CHANGELOG.md` - `$REG/tauri-2.11.3/src/{app.rs,plugin.rs,process.rs}`, `$REG/tauri-utils-2.9.3/src/{lib.rs,config.rs,platform.rs}` - `$REG/tauri-bundler-2.9.4/src/bundle.rs`, `src/bundle/windows/nsis/{installer.nsi,utils.nsh,mod.rs}` - `tauri-cli-2.11.3` (crates.io-Tarball): `src/bundle.rs`, `src/build.rs`, `src/interface/rust.rs`, `src/helpers/updater_signature.rs`, `src/signer/generate.rs`, `Cargo.lock` - `$REG/reqwest-0.13.5/Cargo.toml`, `$REG/ring-0.17.14/build.rs` + `pregenerated/` - Gitea 1.26.2 `http://localhost:3002/swagger.v1.json`; `go-gitea/gitea` `release/v1.26` `routers/api/v1/api.go` - Repo: `apps/desktop/src-tauri/{src/lib.rs,tauri.conf.json,Cargo.toml,build.rs,capabilities/default.json}`, `.gitea/workflows/ci.yml`, `.gitea/scripts/{desktop-collect.sh,desktop-version.sh,desktop-stamp.sh,publish-images.sh}`, `apps/api/src/desktop/*`, `apps/web/next.config.ts`, `packages/shared/src/index.ts`, `.planning/quick/260917-jn2-*/260917-jn2-PLAN.md`, `18-RESEARCH.md`, `18-04-SUMMARY.md`, `docs/anleitung-betrieb.md` Kap. 10, `docs/ci-cd-setup.md` - Probe: `scratchpad/semver-probe` (semver 1.x, Ausgabe oben) ### Secondary (MEDIUM) - https://v2.tauri.app/plugin/updater/ (Signierung, Config, Antwortformat, installMode, Rust-Beispiel) - https://docs.gitea.com/api/1.26/operations/update-repo-secret/ - https://github.com/tauri-apps/tauri/pull/12313, https://github.com/tauri-apps/tauri/issues/11392, https://github.com/tauri-apps/tauri/issues/7560 ### Tertiary (LOW) - WebSearch-Treffer zu NSIS/Tray-Problemen (nur zur Auffindung der oben genannten Issues genutzt) ## Metadata - Standard stack: HIGH — Plugin-Quelltext gelesen, Legitimitaet OK - Architecture: HIGH — Ablauf aus Code abgeleitet, Integrationspunkte im Repo gelesen - Pitfalls: HIGH fuer SemVer/Config/NSIS (Code + Probe), MEDIUM fuer Cross-Bau, LOW fuer SmartScreen - Research date: 2026-09-17 — gueltig ~30 Tage (Plugin 2.x stabil; 3.0.0-alpha laeuft parallel, wird von `"2"` nicht gewaehlt)