feat(api): GET /desktop/update — signierte Desktop-Pakete im Format des Tauri-Updaters ausliefern
- Neuer oeffentlicher Endpunkt (vor download/:platform): base-Origin wird
per safeOrigin validiert (nur http/https, kein Pfad/Query/Fragment/
Userinfo, sonst 400) und nur zum Bau der absoluten Download-URL genutzt,
nie serverseitig abgerufen
- 204 ohne Body bei fremder Plattform/Architektur, fehlendem Manifest,
fehlender Signatur oder fehlendem/ungueltigem updateVersion; sonst
{ version, pub_date (nur RFC 3339), url, signature, notes }
- Manifest-Felder signature (je Plattform) und updateVersion optional in
@tessera/shared, Eintragspruefung akzeptiert nur String-Signaturen
- 10 neue Spec-Tests, Test 11 erweitert (23 gesamt); latest/download
unveraendert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
import { Controller, Get, Inject, Param, StreamableFile } from '@nestjs/common';
|
||||
import type { DesktopLatestResponse } from '@tessera/shared';
|
||||
import {
|
||||
BadRequestException,
|
||||
Controller,
|
||||
Get,
|
||||
Inject,
|
||||
Param,
|
||||
Query,
|
||||
Res,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import type { DesktopLatestResponse, DesktopUpdateResponse } from '@tessera/shared';
|
||||
import type { Response } from 'express';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
import { DesktopService } from './desktop.service';
|
||||
import { DesktopService, safeOrigin } from './desktop.service';
|
||||
|
||||
@Controller('desktop')
|
||||
export class DesktopController {
|
||||
@@ -22,6 +32,36 @@ export class DesktopController {
|
||||
return this.desktopService.getLatest();
|
||||
}
|
||||
|
||||
// Bewusst oeffentlich (D-10, wie latest/download): der Desktop-Client prueft
|
||||
// beim Start vor jeder Anmeldung. Statische Route VOR `download/:platform`
|
||||
// (Konvention Route-Order, auch wenn sich die beiden hier nicht
|
||||
// ueberschatten). 204 ist der vom Updater-Plugin definierte Status fuer
|
||||
// "kein Update"; `passthrough` + `res.status(204)` funktioniert, weil Nest
|
||||
// den Standardstatus VOR dem Handler setzt und die Antwort danach ohne
|
||||
// eigenen Statuscode schreibt -- der Handler-Status gewinnt. `current`
|
||||
// wird nicht deklariert, weil der Service es nicht auswertet.
|
||||
@Public()
|
||||
@Get('update')
|
||||
update(
|
||||
@Query('target') target: unknown,
|
||||
@Query('arch') arch: unknown,
|
||||
@Query('base') base: unknown,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): DesktopUpdateResponse | undefined {
|
||||
const origin = safeOrigin(base);
|
||||
if (!origin) {
|
||||
throw new BadRequestException(
|
||||
'base must be an http(s) origin without path, query or credentials',
|
||||
);
|
||||
}
|
||||
const result = this.desktopService.getUpdate({ target, arch, origin });
|
||||
if (!result) {
|
||||
res.status(204);
|
||||
return undefined;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Bewusst oeffentlich (D-10): der Download selbst braucht keine Anmeldung,
|
||||
// gleicher Grund wie getLatest oben.
|
||||
@Public()
|
||||
|
||||
Reference in New Issue
Block a user