31 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, estimate, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | user_setup | estimate | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 18-desktop-client-fertigstellen | 01 | execute | 1 |
|
true |
|
|
|
Purpose: D-07, D-08 (Abbild-Seite) und D-10 aus 18-CONTEXT.md umsetzen und
die Architektur (Skript -> Abbild -> API) einmal durchgehend beweisen, bevor
die breiteren Plaene folgen.
Output: Zwei CI-Skripte, das API-Modul apps/api/src/desktop/ mit
HTTP-Durchstich-Spec, geteilte Typen, Dockerfile-Erweiterung, Basislinie
1.1.0.
Kein Datenbank-Schema betroffen: Diese Phase aendert weder
schema.prisma noch Migrationen — kein Schema-Push noetig.
Identitaetsfrage (Plattform): platform ist ein geschlossener Wertevorrat
'windows' | 'linux' (Typ DesktopPlatform in packages/shared, Konstante
PLATFORMS im Dienst), kein freier String. Eine dritte Plattform waere eine
bewusste Erweiterung an genau diesen zwei Stellen.
Externe Schnittstellen (Gitea REST, einzige in dieser Phase): Bereits in
Gebrauch: GET /repos/{owner}/{repo}/releases/tags/{tag}, POST .../releases,
PATCH .../releases/{id}. Neu in diesem Plan:
GET /repos/{owner}/{repo}/releases/{id}/assets,
DELETE /repos/{owner}/{repo}/releases/{id}/assets/{asset_id},
POST /repos/{owner}/{repo}/releases/{id}/assets?name={name} (multipart-Feld
attachment). Keine weitere Gitea-Faehigkeit ist im Umfang. Am 2026-09-16
gegen die laufende Instanz geprueft: Gitea 1.26.2; Release-Anhaenge sind
standardmaessig ohne Typ-Beschraenkung und bis 2048 MB erlaubt
([repository.release], Voreinstellung).
Vom Client aus ist die API nur ueber den Web-Ursprung erreichbar: Im
Betrieb steht die API nicht unter dem Web-Hostnamen, sondern hinter dem
Next.js-Rewrite /api-proxy/* (apps/web/next.config.ts; die Web-Oberflaeche
nutzt zur Bauzeit NEXT_PUBLIC_API_URL=/api-proxy). Deshalb sind alle
url-Felder der Antwort von /desktop/latest relativ zur API-Basis
(/desktop/download/linux); die Web-Oberflaeche stellt API_URL davor, der
Client (18-04) spricht {server}/api-proxy/desktop/latest.
Artifacts this phase produces
Phase 18 gesamt (dieser Plan erzeugt die mit * markierten):
.gitea/scripts/desktop-version.sh* — Version aus dem letzten Tag setzen.gitea/scripts/desktop-collect.sh* — Pakete einsammeln,manifest.json- 18-02:
.gitea/workflows/ci.yml— Jobdesktop,publishmit Cache-Restore (18-05 ergaenzt Windows);.gitea/scripts/publish-images.sh— harte Pruefung aufdesktop-dist/manifest.json;.gitea/scripts/publish-release.sh— Funktionupload_asset, Upload aller Manifest-Dateien desktop-dist/.gitkeep*,.gitignore* — Platzhalter-Verzeichnis fuer die Paketeapps/api/Dockerfile* —COPY desktop-dist ./desktop-distpackages/shared/src/index.ts* —DesktopPlatform,DesktopManifestFile,DesktopManifest,DesktopLatestFile,DesktopLatestResponseapps/api/src/desktop/desktop.module.ts*,desktop.controller.ts* (DesktopController.getLatest,DesktopController.download),desktop.service.ts* (DesktopService.getManifest,getLatest,getPackage,PLATFORMS),desktop.service.spec.ts*apps/api/src/app.module.ts* —DesktopModuleregistriertapps/desktop/src-tauri/tauri.conf.json*,Cargo.toml*,Cargo.lock*,apps/desktop/package.json* — Basislinie1.1.0- 18-03:
apps/web/src/lib/desktop.ts(loadDesktopLatest,desktopDownloadUrl,formatFileSize),desktop.test.ts,components/desktop/desktop-download-links.tsx(+Test),app/(auth)/login/page.tsx,app/(portal)/settings/general/desktop/page.tsx,components/settings/desktop-app-settings.tsx(+Test),components/settings/settings-sidebar.tsx,messages/de.json,messages/en.json - 18-04:
apps/desktop/src-tauri/src/lib.rs(Kommandoscheck_server,save_server_url; Trayupdate,autostart),Cargo.toml(tauri-plugin-opener),capabilities/default.json,apps/desktop/src/setup.html,icons/* - 18-05:
ci.yml(Windows-Cross-Bau),desktop-collect.sh --require linux,windows - 18-06:
docs/anleitung-anwender.md,docs/anleitung-betrieb.md,docs/anleitung-entwicklung.md,docs/ci-cd-setup.md,CHANGELOG.md,.planning/REQUIREMENTS.md
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/18-desktop-client-fertigstellen/18-CONTEXT.md @.planning/phases/18-desktop-client-fertigstellen/18-RESEARCH.md @.planning/phases/18-desktop-client-fertigstellen/18-PATTERNS.md@.gitea/scripts/publish-images.sh @apps/api/Dockerfile @apps/api/src/health/health.controller.ts @apps/api/src/health/health.controller.spec.ts @apps/api/src/dkv/dkv.service.ts @packages/shared/src/index.ts
Task 1: Ein Linux-Paket aus dem Bau bis zum Download aus der API — eine Strecke Auf dem Entwicklungsrechner sind Rust/Cargo (1.96) und die Tauri-Linux-Abhaengigkeiten installiert (libwebkit2gtk-4.1-dev, libayatana-appindicator3-dev, librsvg2-dev, libgtk-3-dev — laut 18-RESEARCH.md "Environment Availability" vorhanden), und der lokale Docker-Stack aus `docker-compose.yml` laeuft (Container `tessera-ctl-api-1` auf Port 3001, `tessera-ctl-web-1` auf Port 3000). Die Antwortform von `GET /desktop/latest` (Feld `version`, `files.{windows,linux}.{name,size,sha256,url}`) wird von installierten Clients gelesen; Aenderungen muessen abwaertskompatibel (nur additiv) bleiben, sonst verlieren alte Clients den Update-Hinweis. .gitea/scripts/desktop-collect.sh, .gitignore, desktop-dist/.gitkeep, packages/shared/src/index.ts, apps/api/src/desktop/desktop.module.ts, apps/api/src/desktop/desktop.controller.ts, apps/api/src/desktop/desktop.service.ts, apps/api/src/desktop/desktop.service.spec.ts, apps/api/src/app.module.ts, apps/api/Dockerfile .planning/phases/18-desktop-client-fertigstellen/18-RESEARCH.md (Abschnitte "Pattern 2", "Code Examples 7", "Common Pitfalls 1 und 4"), .planning/phases/18-desktop-client-fertigstellen/18-PATTERNS.md (Abschnitte desktop.module/controller/service/spec, Dockerfile, shared), apps/api/src/health/health.controller.ts, apps/api/src/health/health.controller.spec.ts, apps/api/src/health/health.module.ts, apps/api/src/dkv/dkv.service.ts (Zeilen 85-110 und 700-732), apps/api/src/auth/decorators/public.decorator.ts, apps/api/Dockerfile, .gitea/scripts/publish-images.sh (Kopfkommentar und case-Block als Stilvorlage), packages/shared/src/index.ts, .dockerignore - `GET /desktop/latest` liefert bei vorhandenem Manifest 200 mit `{ version, channel, commit, buildTime, files: { linux: { name, size, sha256, url: "/desktop/download/linux" } } }`; ohne Manifest 404. - `GET /desktop/download/linux` liefert 200, `Content-Disposition: attachment; filename="{name aus Manifest}"`, `Content-Type: application/octet-stream`, `Content-Length` = `size`, und der Inhalt hat exakt den SHA-256 aus dem Manifest. - `GET /desktop/download/mac` und `GET /desktop/download/..%2F..%2Fetc%2Fpasswd` enden mit 400 — auch dann, wenn das Verzeichnis gar nicht existiert (Whitelist greift vor jedem Dateisystemzugriff). - Listet das Manifest die angefragte Plattform nicht, kommt 404; traegt ein Manifest-Eintrag einen Namen mit Pfadzeichen, kommt ebenfalls 404 (Verteidigung in der Tiefe, T-18-02). - Beide Handler tragen `@Public()` (Reflect-Metadaten `isPublic === true`). - `desktop-collect.sh` findet das AppImage im Tauri-Bundle-Verzeichnis, kopiert es nach `desktop-dist/Tessera-{version}.AppImage` und schreibt `desktop-dist/manifest.json` mit korrekter Groesse und korrektem SHA-256. **Platzhalter-Verzeichnis.** `desktop-dist/.gitkeep` (leer) anlegen und in `.gitignore` unter einer neuen Ueberschrift "Desktop-Pakete aus dem Bau (Phase 18)" die zwei Zeilen `desktop-dist/*` und `!desktop-dist/.gitkeep` ergaenzen. Grund: Das Dockerfile kopiert `desktop-dist/` immer; ohne versionierten Platzhalter scheitert jeder lokale `docker build`, und die API soll bei leerem Verzeichnis sauber 404 liefern (D-10). `.dockerignore` braucht keine Aenderung — die Zeile `dist` trifft nur das Wurzelverzeichnis `dist`, nicht `desktop-dist` (per D-08 muss der Ordner in den Bau-Kontext).Geteilte Typen (packages/shared/src/index.ts). Direkt unter
VersionResponse im selben flachen Stil ergaenzen, mit deutschem
Kopfkommentar (Quelle: manifest.json, geschrieben nur von
desktop-collect.sh im CI, D-08): export type DesktopPlatform = 'windows' | 'linux';
DesktopManifestFile { name: string; size: number; sha256: string };
DesktopManifest { version: string; channel: string; commit: string; buildTime: string; files: Partial<Record<DesktopPlatform, DesktopManifestFile>> };
DesktopLatestFile extends DesktopManifestFile { url: string };
DesktopLatestResponse mit denselben vier Kopf-Feldern und
files: Partial<Record<DesktopPlatform, DesktopLatestFile>>. files ist
bewusst Partial, weil dieser Plan nur Linux liefert und Windows erst mit
18-05 dazukommt.
Sammel-Skript .gitea/scripts/desktop-collect.sh (POSIX sh, set -eu,
deutscher Kopfkommentar im Stil von publish-images.sh, kennt kein Secret).
Aufruf sh .gitea/scripts/desktop-collect.sh --require linux (Kommaliste,
spaeter linux,windows). Umgebung: GITHUB_REF (Kanalentscheidung exakt wie
in publish-images.sh: refs/tags/v* -> Kanal live, kein Suffix;
refs/heads/main -> Kanal beta, Suffix -beta.{7-stelliger SHA}; alles
andere -> Kanal dev, kein Suffix, damit lokale Proben die Freigabe-Namen
tragen), DESKTOP_DIST (Vorgabe desktop-dist), TAURI_DIR (Vorgabe
apps/desktop/src-tauri). Ablauf: Version per jq -r .version aus
$TAURI_DIR/tauri.conf.json lesen und gegen ^[0-9]+\.[0-9]+\.[0-9]+$
pruefen (sonst Exit 1 — Pitfall 2, NSIS nimmt nur numerische Versionen);
git rev-parse --short=7 HEAD; alte *.AppImage, *.exe, manifest.json
im Zielordner entfernen (Platzhalter bleibt); Linux: genau eine Datei
$TAURI_DIR/target/release/bundle/appimage/*.AppImage per find/ls
ermitteln — bei null oder mehr als einer Datei und geforderter Plattform Exit 1
mit klarer Meldung (Pitfall 4: niemals den Tauri-Vorgabenamen annehmen);
kopieren nach Tessera-${VERSION}${SUFFIX}.AppImage; Windows analog aus
$TAURI_DIR/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe nach
Tessera-Setup-${VERSION}${SUFFIX}.exe (in diesem Plan noch nicht gefordert,
Zweig aber schon anlegen); je Datei size ueber stat -c %s und sha256
ueber sha256sum | cut -d' ' -f1; manifest.json ausschliesslich mit jq -n
und --arg/--argjson bauen (Felder version, channel, commit,
buildTime als UTC-ISO-Zeit, files nur mit tatsaechlich vorhandenen
Plattformen); zum Schluss je Datei eine Zeile linux: {Name} ({Bytes} Bytes, sha256 {Hash}) ausgeben. Datei ausfuehrbar machen (chmod +x) wie die
Nachbarskripte.
Lokales AppImage als Testobjekt. Liegt unter
apps/desktop/src-tauri/target/release/bundle/appimage/ noch das AppImage aus
Phase 6, reicht es fuer diesen Durchstich; sonst zuerst
pnpm --filter @tessera/desktop exec tauri build --bundles appimage
laufen lassen (dauert einige Minuten). Danach das Sammel-Skript aufrufen; es
muss desktop-dist/Tessera-0.0.1.AppImage und desktop-dist/manifest.json
erzeugen (die Basislinie 1.1.0 kommt erst in Task 2).
API-Modul apps/api/src/desktop/. desktop.module.ts nach dem Vorbild
health.module.ts mit controllers: [DesktopController] und
providers: [DesktopService]; in app.module.ts importieren und hinter
HealthModule in die imports-Liste aufnehmen.
desktop.service.ts (@Injectable(), Imports fs/path wie
dkv.service.ts): Konstante PLATFORMS = ['windows', 'linux'] as const
(Wertevorrat = DesktopPlatform). Verzeichnis im Konstruktor bestimmen:
process.env.DESKTOP_DIST_DIR (getrimmt, nicht leer) hat Vorrang, sonst
path.resolve(__dirname, '..', '..', '..', '..', 'desktop-dist') — gleiche
Vier-Ebenen-Aufloesung wie userFilesDir in dkv.service.ts, ergibt im
Abbild /app/desktop-dist und lokal die Monorepo-Wurzel. Methoden:
getManifest(): DesktopManifest | null (liest manifest.json, null wenn
Datei fehlt oder JSON.parse scheitert oder version kein String bzw. files
kein Objekt ist — mit Logger.warn, nie werfen);
getLatest(): DesktopLatestResponse (wirft NotFoundException('Desktop packages are not available on this server')
ohne Manifest; sonst Kopf-Felder uebernehmen und je vorhandener Plattform
url: '/desktop/download/' + platform ergaenzen);
getPackage(platform: string): { stream: fs.ReadStream; entry: DesktopManifestFile }
in genau dieser Reihenfolge: (1) PLATFORMS.includes(platform) sonst
BadRequestException('Unknown platform') — vor jedem Dateisystemzugriff;
(2) Manifest holen, sonst 404; (3) manifest.files[platform] fehlt -> 404;
(4) entry.name muss ^[A-Za-z0-9._-]+$ erfuellen, sonst 404 (kein Name aus
der Anfrage, aber auch ein manipuliertes Manifest darf nicht aus dem Ordner
hinausfuehren); (5) path.join(dir, entry.name) muss existieren, sonst 404;
(6) fs.createReadStream zurueckgeben.
desktop.controller.ts (@Controller('desktop'), Konstruktor mit
DesktopService): @Public() @Get('latest') getLatest() mit einem
Kommentar, warum oeffentlich (D-10: die Anmeldeseite zeigt den Link vor jeder
Anmeldung; gleicher Grund wie HealthController.getVersion, T-KU1-03);
@Public() @Get('download/:platform') download(@Param('platform') platform: string): StreamableFile
— new StreamableFile(stream, { type: 'application/octet-stream', disposition: 'attachment; filename="' + entry.name + '"', length: entry.size })
(Optionen-Objekt von StreamableFile aus @nestjs/common; kein @Res, kein
Puffern der ganzen Datei — Installer sind zwei Groessenordnungen groesser als
die DKV-Exporte, deshalb bewusst anders als dkv.controller.ts). Kein
@Roles() an beiden Handlern.
desktop.service.spec.ts (Kopfkommentar und nummerierte it('Test N (…)')
im Stil von health.controller.spec.ts, import 'reflect-metadata' zuerst).
Keine fs-Mocks — stattdessen ein echtes Temp-Verzeichnis
(fs.mkdtempSync(path.join(os.tmpdir(), 'tessera-desktop-'))) mit einer
kleinen Zufallsdatei (z. B. 64 KiB aus crypto.randomBytes) und einem von
Hand geschriebenen manifest.json, dessen sha256 im Test unabhaengig ueber
crypto.createHash('sha256') berechnet wird. Fuer den HTTP-Durchstich
process.env.DESKTOP_DIST_DIR auf das Temp-Verzeichnis setzen, dann
NestFactory.create(DesktopModule, { logger: false }), await app.listen(0),
Port aus app.getHttpServer().address().port, Aufrufe mit dem globalen
fetch; im afterAll app.close() und Temp-Verzeichnis entfernen. Faelle:
Test 1 latest -> 200 und Form wie in <behavior>; Test 2 Dienst ohne
Manifest (zweites, leeres Temp-Verzeichnis, eigene DesktopService-Instanz
nach Umsetzen der Umgebungsvariable) -> NotFoundException; Test 3
download/linux -> Header und Body-Hash wie in <behavior>; Test 4 mac und
..%2F..%2Fetc%2Fpasswd -> 400; Test 5 Dienst mit nicht existierendem
Verzeichnis und Plattform mac -> BadRequestException (nicht
NotFoundException) — beweist die Reihenfolge Whitelist vor Dateisystem;
Test 6 Manifest nur mit windows -> download/linux 404; Test 7 Manifest mit
Namen ../x.AppImage -> 404; Test 8 @Public() auf getLatest und
download per Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.getLatest).
Erwartungswerte von Hand hinschreiben, nicht ueber den Pruefling erzeugen.
Dockerfile (apps/api/Dockerfile). In der runner-Stufe unmittelbar vor
USER nestjs die Zeile COPY desktop-dist ./desktop-dist mit deutschem
Kommentar (Phase 18, D-08: Pakete werden vom CI in den Bau-Kontext gelegt,
lokal nur der Platzhalter; nur lesend, keine chown noetig).
Durchstich im laufenden Stack. Nach den Tests das API-Abbild lokal neu
bauen und den Container ersetzen (docker compose build api und danach
docker compose up -d --force-recreate api — up allein baut nicht neu,
Projektwissen "Deploy-Fallstricke"); dann curl http://localhost:3001/desktop/latest
und die Kopfzeilen von /desktop/download/linux pruefen, zusaetzlich ueber
den Web-Rewrite http://localhost:3000/api-proxy/desktop/latest. Danach bleibt
der lokale Stack in diesem Zustand (mit Paketen) stehen.
<acceptance_criteria>
- git ls-files --error-unmatch desktop-dist/.gitkeep endet mit 0 nach dem Commit; grep -c '^!desktop-dist/.gitkeep$' .gitignore ergibt 1.
- grep -c 'export type DesktopPlatform' packages/shared/src/index.ts ergibt 1; grep -c 'export interface DesktopLatestResponse' packages/shared/src/index.ts ergibt 1.
- grep -c "DesktopModule" apps/api/src/app.module.ts ergibt mindestens 2 (Import und imports-Eintrag).
- grep -c '^COPY desktop-dist ./desktop-dist' apps/api/Dockerfile ergibt 1.
- grep -v '^\s*//' apps/api/src/desktop/desktop.controller.ts | grep -c '@Public()' ergibt 2.
- grep -v '^\s*//' apps/api/src/desktop/desktop.service.ts | grep -c "PLATFORMS = \['windows', 'linux'\] as const" ergibt 1.
- pnpm --filter @tessera/api exec vitest run src/desktop meldet 8 Tests bestanden, 0 fehlgeschlagen.
- sh .gitea/scripts/desktop-collect.sh --require linux erzeugt desktop-dist/manifest.json; jq -r .files.linux.name desktop-dist/manifest.json ergibt Tessera-0.0.1.AppImage (bzw. die aktuelle Version aus tauri.conf.json) und der SHA-256 im Manifest ist gleich sha256sum der Datei.
- curl -s http://localhost:3001/desktop/latest | jq -r .files.linux.url ergibt /desktop/download/linux; curl -sI http://localhost:3001/desktop/download/linux enthaelt content-disposition: attachment; filename="Tessera- und den Status 200.
</acceptance_criteria>
cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run src/desktop && pnpm --filter @tessera/api type-check
<fails_when>vitest meldet "failed" oder einen Exit-Code ungleich 0, oder tsc gibt Fehlerzeilen aus.</fails_when>
cd /home/vicolab/projects/tessera-ctl && sh .gitea/scripts/desktop-collect.sh --require linux && test "$(sha256sum "desktop-dist/$(jq -r .files.linux.name desktop-dist/manifest.json)" | cut -d' ' -f1)" = "$(jq -r .files.linux.sha256 desktop-dist/manifest.json)" && echo MANIFEST-OK
<fails_when>Das Skript endet mit Exit 1, manifest.json fehlt, oder die Zeile MANIFEST-OK erscheint nicht (Hash-Abweichung).</fails_when>
cd /home/vicolab/projects/tessera-ctl && curl -sf http://localhost:3001/desktop/latest | jq -e '.files.linux.url == "/desktop/download/linux"' && curl -sI http://localhost:3001/desktop/download/linux | grep -i 'content-disposition: attachment; filename="Tessera-' && curl -sf http://localhost:3000/api-proxy/desktop/latest | jq -e .version
<fails_when>curl liefert einen Nicht-2xx-Status (Exit 22), jq -e findet das Feld nicht, oder die Kopfzeile content-disposition: attachment; filename="Tessera- fehlt — dann liefert das neu gebaute Abbild die Pakete nicht aus.</fails_when>
Acht Spec-Tests gruen, Typpruefung fehlerfrei. Das lokal eingesammelte
AppImage liegt mit passendem Manifest in desktop-dist/, das neu gebaute
API-Abbild liefert es unter /desktop/download/linux mit attachment-Header
aus, und /desktop/latest ist auch ueber /api-proxy/ des Web-Containers
erreichbar.
Basislinie einchecken. Das Skript einmal lokal ausfuehren (aktueller
letzter Tag ist v1.1.0), danach cargo check im Verzeichnis
apps/desktop/src-tauri laufen lassen, damit Cargo.lock den Eintrag des
eigenen Pakets auf 1.1.0 zieht; apps/desktop/package.json von Hand auf
"version": "1.1.0" setzen. Alle vier Dateien werden mit dem Skript
committet — die eingecheckten Werte sind nur die Basislinie fuer lokale Baue,
die Wahrheit im CI ist der Tag (Kopfkommentar des Skripts sagt genau das).
Frisches AppImage mit der Basislinie. pnpm --filter @tessera/desktop exec tauri build --bundles appimage
erneut laufen lassen (bei warmem target/ wenige Minuten), vorher das alte
Bundle-Verzeichnis apps/desktop/src-tauri/target/release/bundle entfernen,
damit desktop-collect.sh genau eine Datei findet. Danach
sh .gitea/scripts/desktop-collect.sh --require linux — das Manifest traegt
jetzt 1.1.0 und den Namen Tessera-1.1.0.AppImage.
<acceptance_criteria>
- sh .gitea/scripts/desktop-version.sh --print gibt genau 1.1.0 aus (bei Tag-Stand v1.1.0).
- jq -r .version apps/desktop/src-tauri/tauri.conf.json ergibt 1.1.0; grep -c '^version = "1.1.0"' apps/desktop/src-tauri/Cargo.toml ergibt 1; grep -c '"version": "1.1.0"' apps/desktop/package.json ergibt 1.
- grep -A1 'name = "tessera-desktop"' apps/desktop/src-tauri/Cargo.lock | grep -c 'version = "1.1.0"' ergibt 1.
- jq -r .files.linux.name desktop-dist/manifest.json ergibt Tessera-1.1.0.AppImage.
- Negativprobe: DESKTOP_TAG=v1.2.3-beta sh .gitea/scripts/desktop-version.sh --print endet mit Exit 1 und schreibt nichts; DESKTOP_TAG=v2.0.0 sh .gitea/scripts/desktop-version.sh --print gibt 2.0.0 aus und schreibt ebenfalls nichts (Dateien bleiben bei 1.1.0).
</acceptance_criteria>
cd /home/vicolab/projects/tessera-ctl && test "$(sh .gitea/scripts/desktop-version.sh --print)" = "1.1.0" && test "$(jq -r .version apps/desktop/src-tauri/tauri.conf.json)" = "1.1.0" && grep -q '^version = "1.1.0"' apps/desktop/src-tauri/Cargo.toml && test "$(jq -r .files.linux.name desktop-dist/manifest.json)" = "Tessera-1.1.0.AppImage" && echo VERSION-OK
<fails_when>Eine der Pruefungen schlaegt fehl und VERSION-OK erscheint nicht — Skript, Basislinie oder Manifest tragen nicht 1.1.0.</fails_when>
cd /home/vicolab/projects/tessera-ctl && if DESKTOP_TAG=v1.2.3-beta sh .gitea/scripts/desktop-version.sh --print >/dev/null 2>&1; then echo "Vorabversion wurde akzeptiert"; exit 1; fi && test "$(jq -r .version apps/desktop/src-tauri/tauri.conf.json)" = "1.1.0" && echo REJECT-OK
<fails_when>Das Skript akzeptiert v1.2.3-beta (Exit 0) oder hat trotz --print die Datei veraendert — REJECT-OK fehlt.</fails_when>
cd /home/vicolab/projects/tessera-ctl/apps/desktop/src-tauri && cargo check 2>&1 | tail -1 | grep -q 'Finished'
<fails_when>cargo check endet nicht mit einer Finished-Zeile (Kompilierfehler nach der Versionsaenderung).</fails_when>
Skript, Basislinie 1.1.0 in allen vier Dateien, cargo check gruen, und
desktop-dist/ traegt Tessera-1.1.0.AppImage samt Manifest.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
Internet -> API (/desktop/latest, /desktop/download/:platform) |
Oeffentliche, unauthentifizierte Endpunkte; der Pfadparameter ist Angreifereingabe. |
CI-Runner -> API-Abbild (desktop-dist/) |
Das Manifest und die Pakete entstehen im Runner und werden unveraendert ins Abbild kopiert. |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-18-01 | Tampering / Information Disclosure | DesktopService.getPackage (Pfad-Traversal ueber :platform) |
high | mitigate | Whitelist PLATFORMS vor jedem Dateisystemzugriff; Dateiname kommt ausschliesslich aus manifest.json; zusaetzlich Namensmuster ^[A-Za-z0-9._-]+$. Spec-Tests 4, 5 und 7 pinnen das. |
| T-18-02 | Tampering | manifest.json (veraltet oder manipuliert) |
medium | mitigate | Nur desktop-collect.sh im CI schreibt die Datei; sie liegt im unveraenderlichen Abbild, kein Laufzeitpfad schreibt nach /app/desktop-dist/; Namensmuster-Pruefung als Verteidigung in der Tiefe. SHA-256 ist Integritaets-Metadatum, keine Signatur (D-09). |
| T-18-04 | Information Disclosure | GET /desktop/latest (Version, Kanal, Commit oeffentlich) |
low | accept | Gleiche Abwaegung wie GET /health/version (T-KU1-03): keine Komponentenversionen, privates Repository; die Anmeldeseite braucht die Daten vor der Anmeldung (D-10). |
| T-18-05 | Denial of Service | GET /desktop/download/:platform (grosse Datei, oeffentlich) |
low | accept | Streaming statt Puffern; Ratenbegrenzung ist Aufgabe des vorgeschalteten Nginx Proxy Managers (ASVS L1). |
| T-18-SC | Tampering | Paketinstallationen | low | accept | Dieser Plan installiert kein neues Paket (Legitimitaetstabelle in RESEARCH: cargo-xwin, tauri-plugin-opener beide OK, kommen in 18-04/18-05). |
| </threat_model> |
<success_criteria>
- Ein lokal gebautes AppImage wird nach dem Einsammeln vom neu gebauten API-Abbild ohne Anmeldung ausgeliefert (Strecke Skript -> Abbild -> API bewiesen).
- Unbekannte Plattformen und Traversal-Versuche enden mit 400, fehlende Pakete mit 404 — gepinnt durch die Spec.
- Die Versionsquelle ist der Freigabe-Tag; die Basislinie im Repository ist
1.1.0. </success_criteria>