Files
tessera-ctl/.planning/quick/260917-kgc-desktop-client-update-in-der-app-herunte/260917-kgc-RESEARCH.md
T
schalli efbd6e8974
Tessera CI/CD / Lint & Type Check (push) Successful in 53s
Tessera CI/CD / Tests (push) Successful in 1m5s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m22s
docs(quick-260917-kgc): Aktenstand — Update in der Desktop-App, alle Nachweise erbracht, Wiedereinstieg bereinigt
Quick 260917-kgc (Plan/Recherche/Bericht/Verifikation) und Schnellfix a6d1a64
in der Quick-Task-Tabelle; Nachweise in allen sechs Berichten nachgetragen
(Playwright lokal, CI-Laeufe 382-384, Windows-Test-VM: In-App-Update
7479cb4 -> a6d1a64). Ueberholte .continue-here-Dateien entfernt,
Desktop-Client-Todo geschlossen.

Dieser Push aendert nichts unter apps/desktop -- er ist zugleich der
Beweisfall 2 des CI-Desktop-Skips (Pakete aus dem Zwischenspeicher).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
2026-09-18 11:29:14 +02:00

43 KiB

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>; 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=&current=&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<sha7> (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<Updater> [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<Url>) -> Result<Self> (Z. 224, validiert https), header(k, v) -> Result<Self>, headers(HeaderMap), timeout(Duration), pubkey(..), installer_arg(s), restart_after_install(bool) (Windows, Default true), configure_client(Fn(reqwest::ClientBuilder) -> ClientBuilder), build() -> Result<Updater>; 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<Option<Update>> (Z. 432). Update (pub-Felder): body: Option<String>, current_version: String, version: String, date: Option<OffsetDateTime>, 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<Option<Update>>) liegen [VERIFIED: updater.rs:642-676].
  • update.download(on_chunk: FnMut(usize, Option<u64>), on_finish: FnOnce()) -> Result<Vec<u8>> (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<String>, pub_date: Option<OffsetDateTime>, data: RemoteReleaseInner } mit RemoteReleaseInner::Dynamic(ReleaseManifestPlatform { url: Url, signature: String }) oder Static { platforms: HashMap<String, ReleaseManifestPlatform> } (Z. 70-96) [VERIFIED].
  • plugins.updater.endpoints in tauri.conf.json ist optional (#[serde(default)] pub endpoints: Vec<Url>, 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 <datei>.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 <pfad> [-p <passwort>] [--ci] [--force] → schreibt <pfad> (privat) und <pfad>.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-<version>-updater-<rand>\Tessera-<version>-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", <exe>, <parameter>, 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 <aktuelle Prozessargumente, escaped> + 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=<server_url> 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/<platform>") 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=&current=&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=<c>;commit=<sha7>", 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=<APP_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.<platform>.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.<p>.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": "<wert>", "description": "<optional>"}; 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):
    // 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<sha7>" 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-<version>-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}}&current={{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.<p>.signature vorhanden?) ── nein → 204 → Ok(None)
         │     ja → 200 { version: X.Y.Z | X.Y.Z-beta.g<sha7>, url: {base}/api-proxy/desktop/download/<p>, signature, pub_date, notes }
         ├─ Some(update) → app.state::<PendingUpdate>().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<Option<Update>>)); 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)

// Source: v2.tauri.app/plugin/updater/ + config.rs:80-92 (Schluesselnamen camelCase)
"bundle": { "createUpdaterArtifacts": true, ... },
"plugins": {
  "updater": {
    "pubkey": "<Inhalt der .pub-Datei, eine Base64-Zeile>",
    "windows": { "installMode": "passive" }
  }
}

Kein endpoints-Eintrag (Laufzeit). pubkey ist oeffentlich und gehoert ins Repo.

Cargo.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)

// 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<Option<tauri_plugin_updater::Update>> {
    let endpoint = format!(
        "{}?target={{{{target}}}}&arch={{{{arch}}}}&current={{{{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(&current, &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)

// 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)

# 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 '<passwort>'
# -> ~/.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<sha7>.
  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)

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)