diff --git a/apps/api/src/desktop/desktop.service.spec.ts b/apps/api/src/desktop/desktop.service.spec.ts index 26a3a2e..caa429f 100644 --- a/apps/api/src/desktop/desktop.service.spec.ts +++ b/apps/api/src/desktop/desktop.service.spec.ts @@ -161,7 +161,33 @@ describe('DesktopService/DesktopController — HTTP-Durchstich (Phase 18)', () = expect(res.status).toBe(404); }); - it('Test 8 (bewusst oeffentlich): getLatest und download tragen @Public()', () => { + it('Test 9 (WR-03, ungueltiger Eintrag im Manifest): name fehlt -- getLatest wirft NotFoundException statt "undefined" als Datei zu suchen', () => { + // Bewusst am `writeManifest()`-Helper vorbei direkt geschrieben -- dessen + // Parametertyp verlangt `name`, hier soll aber genau dessen Fehlen + // geprueft werden (kaputtes Manifest, kein TS-Typfehler im Test). + fs.writeFileSync( + path.join(tempDir, 'manifest.json'), + JSON.stringify({ + version: '1.1.0', + channel: 'dev', + commit: 'abc1234', + buildTime: '2026-09-16T00:00:00Z', + files: { linux: { size: 123, sha256: 'a'.repeat(64) } }, + }), + ); + const service = new DesktopService(); + expect(() => service.getLatest()).toThrow(NotFoundException); + }); + + it('Test 10 (WR-03, ungueltiger Eintrag im Manifest): sha256 ist kein 64-stelliger Hex-String -- 404', async () => { + writeManifest({ + linux: { name: PACKAGE_NAME, size: packageSize, sha256: 'not-a-hash' }, + }); + const res = await fetch(`${baseUrl}/desktop/download/linux`); + expect(res.status).toBe(404); + }); + + it('Test 11 (bewusst oeffentlich): getLatest und download tragen @Public()', () => { expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.getLatest)).toBe(true); expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.download)).toBe(true); }); diff --git a/apps/api/src/desktop/desktop.service.ts b/apps/api/src/desktop/desktop.service.ts index 3c8072d..b829b3c 100644 --- a/apps/api/src/desktop/desktop.service.ts +++ b/apps/api/src/desktop/desktop.service.ts @@ -15,6 +15,30 @@ import * as path from 'path'; */ const PLATFORMS = ['windows', 'linux'] as const; +/** sha256 als Hex-String -- genau 64 Zeichen, 0-9/a-f (Gross-/Kleinschreibung egal). */ +const SHA256_HEX_RE = /^[a-f0-9]{64}$/i; + +/** + * Grundform eines einzelnen Datei-Eintrags im Manifest (WR-03, Code-Review + * Phase 18): `getManifest()` prueft bislang nur die Kopf-Form, nicht die + * einzelnen Plattform-Eintraege -- ein kaputter/unvollstaendiger Eintrag + * wuerde sonst unbemerkt bis in `getPackage()` durchgereicht (z. B. + * `entry.name === undefined`, das sich zu `"undefined"` coerct und dort + * fehlleitend als Dateiname gesucht wird). + */ +function isValidManifestFileEntry(entry: unknown): entry is DesktopManifestFile { + if (typeof entry !== 'object' || entry === null) { + return false; + } + const candidate = entry as Record; + return ( + typeof candidate.name === 'string' && + typeof candidate.size === 'number' && + typeof candidate.sha256 === 'string' && + SHA256_HEX_RE.test(candidate.sha256) + ); +} + @Injectable() export class DesktopService { private readonly logger = new Logger(DesktopService.name); @@ -45,10 +69,24 @@ export class DesktopService { try { const raw = fs.readFileSync(manifestPath, 'utf-8'); const parsed = JSON.parse(raw) as DesktopManifest; - if (typeof parsed.version !== 'string' || typeof parsed.files !== 'object' || parsed.files === null) { + if ( + typeof parsed.version !== 'string' || + typeof parsed.files !== 'object' || + parsed.files === null || + Array.isArray(parsed.files) + ) { this.logger.warn(`manifest.json unter ${manifestPath} hat unerwartete Form`); return null; } + for (const platform of PLATFORMS) { + const entry = parsed.files[platform]; + if (entry !== undefined && !isValidManifestFileEntry(entry)) { + this.logger.warn( + `manifest.json unter ${manifestPath} hat einen ungueltigen Eintrag fuer Plattform ${platform}`, + ); + return null; + } + } return parsed; } catch (error) { this.logger.warn(`manifest.json unter ${manifestPath} konnte nicht gelesen werden: ${error}`);