Quick 260917-kgc (Plan/Recherche/Bericht/Verifikation) und Schnellfixa6d1a64in der Quick-Task-Tabelle; Nachweise in allen sechs Berichten nachgetragen (Playwright lokal, CI-Laeufe 382-384, Windows-Test-VM: In-App-Update7479cb4->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
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=¤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<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)
UpdaterExtist fuer jedenManagerimplementiert (App,AppHandle, Fenster):fn updater_builder(&self) -> UpdaterBuilderundfn updater(&self) -> Result<Updater>[VERIFIED:$REG/tauri-plugin-updater-2.11.0/src/lib.rs:58-121].updater_builder()haengt automatisch an: Windowscurrent_exe_args(fuer/ARGS), Linuxexecutable_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, Defaulttrue),configure_client(Fn(reqwest::ClientBuilder) -> ClientBuilder),build() -> Result<Updater>;build()gibtError::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(alsoSend + Sync) — kann inapp.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, danachverify_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 }mitRemoteReleaseInner::Dynamic(ReleaseManifestPlatform { url: Url, signature: String })oderStatic { platforms: HashMap<String, ReleaseManifestPlatform> }(Z. 70-96) [VERIFIED].plugins.updater.endpointsintauri.conf.jsonist 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.pubkeyist Pflicht:pub pubkey: Stringohne Default (config.rs:137). Fehltplugins.updaterganz, uebergibt TauriJsonValue::Null($REG/tauri-2.11.3/src/plugin.rs:1007,unwrap_or_default()) →serde_json::from_valuescheitert → „Error deserializing 'plugins.updater' within your Tauri configuration" (plugin.rs:800-805) → unser.build(...).expect(..)inlib.rs:287-288panict beim Start. Zusaetzlich verlangt die CLI beim Bau mitcreateUpdaterArtifacts != falseden 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 ueberUpdaterExtlaeuft am ACL vorbei —capabilities/default.jsonbleibt unveraendert [VERIFIED].
2. Antwortformat des Endpunkts
- Dynamisches Format: JSON mit
version(aliasname), optionalnotes, optionalpub_date(RFC 3339, sonst Deserialisierungsfehler),url,signature; fehltplatforms, wirdurl+signatureverlangt („theurlfield 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 endetcheck()mitError::ReleaseNotFound(Z. 573) [VERIFIED]. Doku: 204 „No Content", Felderurl/version/signaturePflicht [CITED: https://v2.tauri.app/plugin/updater/]. urlist typisierturl::Url→ muss absolut sein (relative Pfade wie/desktop/download/windowsscheitern beim Parsen) [VERIFIED:updater.rs:72-76]. Unser/desktop/latestliefert heute bewusst relative URLs (desktop.service.ts,getLatest()) — fuer den Updater braucht es einen eigenen Endpunkt mit absoluter URL (siehe Frage 6).versionmuss gueltiges SemVer sein; ein fuehrendesvwird abgeschnitten (parse_version, Z. 1514-1521).version_comparatorbekommt den geparstensemver::Version(kein Rohstring); Prerelease liegt inrelease.version.pre. Probe (Scratchpad, semver 1.x):1.2.0-beta.38c1400OK,1.2.0-beta.1234567OK,1.2.0-beta.0123456→Err("invalid leading zero in pre-release identifier"),1.2.0-beta.g0123456OK,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) → immerg-Praefix.- Client-Version:
current_version = app.package_info().version(updater.rs:197), also1.2.0austauri.conf.json(desktop-version.shschreibt 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-Agenttauri-plugin-updater/2.11.0; der Download liestContent-Lengthfuer die Fortschrittsanzeige (updater.rs:434-437, 687-690, 722-727) [VERIFIED]. UnserStreamableFilesetztlength: entry.size→Content-Lengthvorhanden [VERIFIED:apps/api/src/desktop/desktop.controller.ts,download()].
3. Artefakte und Signatur
bundle.createUpdaterArtifacts: true(TypUpdater::Bool;"v1Compatible"istUpdater::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 mitupdater=falsegebaut (bundle.rs:178) — die normaleTessera_X.Y.Z_x64-setup.exeIST das Update-Artefakt, ebensoTessera_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>.sigdaneben 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.sigundtarget/release/bundle/appimage/Tessera_1.2.0_amd64.AppImage.sig[VERIFIED]. Keincfg(windows)/Host-Gating insign_updaters→ imcargo-xwin-Cross-Bau entsteht die.siggenauso [VERIFIED: Code-Lesung; Pipeline-Nachweis steht aus]. - Umgebung:
TAURI_SIGNING_PRIVATE_KEY— Wert ist Inhalt ODER Pfad (Code prueftPath::exists(),bundle.rs:277-289);TAURI_SIGNING_PRIVATE_KEY_PASSWORDoptional — fehlt sie, gilt mit--ci/CI-Umgebung leeres Passwort, sonst interaktive Abfrage (bundle.rs:272-275, 290-292). Die vom Generator ausgegebene VariableTAURI_SIGNING_PRIVATE_KEY_PATH(signer/generate.rs:60) wird im Bau-Code NICHT gelesen — nurTAURI_SIGNING_PRIVATE_KEY[VERIFIED].pubkeyintauri.conf.jsondarf Inhalt oder Dateipfad sein (bundle.rs:261-269); beim Signieren warnt die CLI, wennkeynumvon 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+pubkeygesetzt → Abbruch: „A public key has been found, but no private key. Make sure to setTAURI_SIGNING_PRIVATE_KEYenvironment 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.exeschreiben (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), dannShellExecuteW(NULL, "open", <exe>, <parameter>, SW_SHOW); Fehler (<=32) wird zurueckgegeben; sonststd::process::exit(0)[VERIFIED]. - Parameter (
updater_parameters, Z. 879-907):nsis_args(install_mode)+/UPDATE+ beirestart_after_install(Defaulttrue)/R(nicht beibasicUi) +/ARGS <aktuelle Prozessargumente, escaped>+installerArgsaus Config.installMode:passive→/P(Default),quiet→/S,basicUi→ keine Flags (config.rs:41-56; Config-Schluesselplugins.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):.onInitliest/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);.onInstSuccessstartet die App nur bei/Poder/Sund 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 ininstall); unter Linux ist es Pflicht (Frage 5). Der Aufruf nachdownload_and_installist trotzdem korrekt, weil plattformuebergreifend harmlos. - Laufender Prozess/Tray:
CheckIfAppIsRunning(utils.nsh:22-62) suchttessera-desktop.exe— beiINSTALLMODE == currentUserperFindProcessCurrentUser, killt ohne Rueckfrage bei/Poder/S(KillProcessCurrentUser, Sleep 500 ms). Da der Updater den Prozess bereits perexit(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(unsertauri.conf.json:48): Installer laeuft ohne UAC im Nutzerkontext,ShellExecuteW "open"verlangt keine Erhoehung;SetContext/SHCTXbleibt 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 setztrestart_on_exitund ruftrequest_exit(RESTART_EXIT_CODE)(=i32::MAX), wasRunEvent::ExitRequested { code: Some(i32::MAX) }ausloest (app.rs:77, 588-611, 1434-1437) — unser Handler inlib.rs:296-301blockt nurcode: None→ Neustart geht durch [VERIFIED]. tauri#7560 („NSIS quiet update do not restart") ist „closed as not planned" (2023, v1) — mit/Rim heutigen Template gegenstandslos [CITED: https://github.com/tauri-apps/tauri/issues/7560].window-statespeichert beim hartenexit(0)unter Windows nicht (keinRunEvent::Exit) — Fensterposition kann nach einem Update einmal verloren gehen [ASSUMED, aus Plugin-Semantik abgeleitet].
5. Linux AppImage
updater_builder()setztexecutable_pathaufapp.env().appimage(= UmgebungsvariableAPPIMAGE,$REG/tauri-utils-2.9.3/src/lib.rs:269-287);build()nimmt unter Linux diesen Pfad alsextract_path(updater.rs:373-379) [VERIFIED].install_appimage(updater.rs:1047-1118): sucht ein temporaeres Verzeichnis auf demselben Dateisystem wie die AppImage (Reihenfolgestd::env::temp_dir(),dirs::cache_dir()=~/.cache, Elternordner der AppImage), verschiebt die laufende Datei perrenameals 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~/.cacheliegt auf demselben Dateisystem wie$HOME; ist/tmpein tmpfs, scheitert nur der erste Kandidat). Bei einer AppImage unter/optohne Schreibrecht schlaegt das Update fehl — Fehlertext im Tray zeigen.- Neustart:
app.restart()→tauri::process::restart→current_binaryliefert unter Linux nur denAPPIMAGE-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 austarget/release/,tauri dev) faelltextract_pathaufcurrent_exe()und wuerde die Binary ueberschreiben — Update-Pfad im Dev-Modus nicht ausloesen (nurcheck()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, Zielhttp://api:3001). Next' Proxy setztx-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 haengtbase=<server_url>an (er kennt sie aus dem Store), die API validiert (URL-Parse, nurhttp/https, keine Credentials, nur Origin uebernehmen, Pfad verwerfen) und bildeturl = ${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_urlist einpub-Feld und darf nachcheck()vom Client aufapi_url(server, "/desktop/download/<platform>")gesetzt werden (updater.rs:655) [VERIFIED] — als Notnagel dokumentieren, nicht als Hauptweg. - Neuer Endpunkt (statt
/desktop/latestzu aendern, das die Web-UI weiter mit relativen URLs nutzt):GET /desktop/update?target=&arch=¤t=&base=(@Public(), statische Route VORdownload/:platform— Route-Order-Falle, Memoryproject_nest_route_order.md). Logik:targetper Whitelist auf Plattform (windows→windows,linux→linux),archmussx86_64sein (sonst 204); Manifest lesen; fehlt Eintrag odersignature→ 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/*.AppImagedie zugehoerige*.sigsuchen (find -name '*-setup.exe.sig'/'*.AppImage.sig'; die bestehenden-name '*.exe'/'*.AppImage'-Zaehler matchen.signicht) und den Dateiinhalt (einzeilige Base64, ~200 Zeichen) per--arg linuxSig "$(cat …)"alsfiles.<platform>.signatureinmanifest.jsonschreiben; Datei selbst nicht kopieren (kein Nutzen, die API liefert JSON). Fehlt die.sig(Bau mit--no-sign), Feld weglassen und eine Warnzeile loggen; beiGITHUB_REF= Tag odermainhart abbrechen (Signatur ist dort Pflicht).desktop.service.ts:isValidManifestFileEntryum optionalessignature: stringerweitern,DesktopManifestFileinpackages/shared/src/index.ts:29-33ebenso.- Skip-Mechanismus (
desktop-stamp.sh check): prueft Groesse/sha256 der beiden Dateien und liest die Manifest-Felder — dassignature-Feld liegt im gecachten Manifest und wird mit uebernommen; keine.sig-Datei zu pruefen. Zwei Ergaenzungen:checkverlangt fuer beide Plattformen ein nicht-leeresfiles.<p>.signature(alter Cache-Stand ohne Signatur →no_reuse), und ein Schluesselwechsel wird automatisch zum Neubau, weilpubkeyinapps/desktop/src-tauri/tauri.conf.jsonliegt undapps/desktopTeil vonDESKTOP_PATHSist (desktop-stamp.sh:50) [VERIFIED]. Die Signatur gilt fuer die Bytes der-setup.exe;cpindesktop-collect.shaendert nichts daran — niemals nachtraeglich signieren/patchen. - Gitea-Secrets per API: Gitea 1.26.2 (
curl localhost:3002/api/v1/version) bietetPUT /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; Kategorierepository→write:repository) [VERIFIED:routers/api/v1/api.go(release/v1.26) Z. 947-958, 1225]. Der vorhandeneREGISTRY_TOKENhatrepository: write(docs/ci-cd-setup.md Z. 88) — genuegt, sofern er dem Repo-Eigentuemerschalligehoert. Aufruf vom Dev-Host ueberlocalhost:3002(Memoryproject_ci_registry_push.md), nie uebergit.vicolab.de. Secrets:TAURI_SIGNING_PRIVATE_KEY(Dateiinhalt, eine Base64-Zeile) undTAURI_SIGNING_PRIVATE_KEY_PASSWORD(Schluessel MIT Passwort erzeugen — ein leerer Secret-Wert ist in Gitea nicht sicher moeglich [ASSUMED];CI=trueals Fallback fuer leeres Passwort setzt act_runner wie GitHub [ASSUMED]). - ci.yml: Job
desktopbekommtenv: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}undTAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.… }}an beidentauri build-Schritten. Der Skip-Pfad braucht die Secrets nicht.publish-release.shkann die.sigoptional als Release-Anhang mitgeben — nicht noetig, das Manifest traegt sie.
7. Versionsvergleich Beta
version_comparatorersetzt 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 (Version1.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 } }currentist immer reines X.Y.Z (D-07) —current.preist leer, darum genuegt der Tupelvergleich.app_commit=env!("APP_COMMIT")(7-stellig,build.rs:18-32) undmanifest.commit=git rev-parse --short=7(desktop-collect.sh:78) — gleiches Format [VERIFIED]. LeererAPP_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 bestehendenmod tests.
8. Gotchas (Abhaengigkeiten, TLS, Groesse, Zertifikate)
- Plugin-Abhaengigkeiten:
reqwest = "0.13"(Featuresjson,stream, default-features = false), Standard-Featuresrustls-tls(=reqwest/rustls-no-provider+rustls 0.23mitring),system-proxy,zip;tauri = "2.10"(unser 2.11.3 passt) [VERIFIED:$REG/tauri-plugin-updater-2.11.0/Cargo.toml:66-116]. Unserreqwest = "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-providerziehtrustls-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 notfallsSSL_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.14ist neu im Abhaengigkeitsgraphen. Das Crate liefert fuerx86_64-pc-windows-msvcvorassemblierte.o-Objekte mit ($REG/ring-0.17.14/build.rs:340-349, 430-445,pregenerated/*-x86_64-nasm.o) — keinnasmnoetig; die C-Teile bautccmit dem Clang auscargo-xwin[VERIFIED: Code-Lesung; der tatsaechliche xwin-Bau ist nur in der Pipeline beweisbar,cargo-xwinist 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-devsteht bereits in der apt-Liste, ci.yml Z. 120-125). Nicht das Projekt-reqwestauf 0.13 heben: dessen Defaultdefault-tls = rustlsziehtaws-lc-rs(cmake/nasm-Bau), ein echtes xwin-Risiko [VERIFIED:reqwest-0.13.5/Cargo.toml:45-51, 101-105]. - Kein
tauri-plugin-httpnoetig; der Updater bringt seinen Client mit [VERIFIED: keine Abhaengigkeit inCargo.toml]. - Endpunkt-Schema: im Release-Bau wird
http://abgelehnt (Error::InsecureTransportProtocol,config.rs:160-179) —endpoints()gibt dannErr; Fehler loggen, Tray-Eintrag gesperrt lassen.dangerousInsecureTransportProtocol/dangerousAcceptInvalidCerts/dangerousAcceptInvalidHostnamesexistieren als Config-Schalter (config.rs:105-118) — nicht setzen, nur im Handbuch als „nicht vorgesehen" nennen [VERIFIED]. Der Store erlaubt heutehttp-Adressen (check_server); ein Kunde mithttp://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.exeerhaelt keine Mark-of-the-Web (kein Browser-Download,std::fs::writeohneZone.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::installunter Windows beendet den Prozess aus dem Tokio-Thread heraus; unserRunEvent::ExitRequested-Handler wird dabei nicht durchlaufen (keinprevent_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.<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}}}}¤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)
// 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
pubkeyfehlt / 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 aufmain/Tags hart abbrechen.- Fuehrende Null im Commit-Stempel:
1.2.0-beta.0123456ist kein SemVer →check()liefert Err → nie ein Update. Immerbeta.g<sha7>. - Relative
url:url::Urlverlangt absolut;/desktop/latestbleibt fuer die Web-UI relativ, der Updater bekommt einen eigenen Endpunkt. - Route-Shadowing:
updatestatisch VORdownload/:platform(Unit-Tests fangen es nicht — Memory). http://-Server im Release:endpoints()gibt Err — abfangen, nichtdangerousInsecureTransportProtocolsetzen.- Alter Cache-Stand ohne Signatur:
desktop-stamp.sh checkmusssignatureje Plattform verlangen, sonst wird ein unsigniertes Paket uebernommen und der Endpunkt liefert dauerhaft 204. - Dev-Modus (
tauri dev, keine AppImage):download_and_installwuerde die Binary imtarget/ueberschreiben — Update nur mit gebautem AppImage/Installer ausloesen. - Zwei reqwest-Majors:
reqwest 0.12(Projekt) und0.13(Plugin) koexistieren; Projekt-reqwestNICHT auf 0.13 heben (aws-lc-rs im Cross-Bau). - Endpunkt-Placeholder in der Query:
{{target}}darf nicht vorab URL-kodiert werden — als Rohtext in den String;url::Url::parselaesst{/}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
- 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.
- Wo liegt die Sicherung des privaten Schluessels? (Betriebshandbuch Kap. 10; nicht im Repo, nicht auf dem Runner.)
- 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/gitearelease/v1.26routers/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.mdKap. 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)