Don't trust manifest.latest, no-cache the manifest fetch
Two robustness fixes after a real-world miss where v1.4.7 was uploaded but the launcher kept reporting v1.4.6 as latest: 1. UpdateChecker: ignore the `latest` field of the manifest entirely. Always pick the highest SemVer in the versions[] array (filtered by availableForDownload). Removes a class of "I forgot to bump latest" bugs at the server. 2. ManifestService: send Cache-Control: no-cache, no-store + Pragma: no-cache when fetching. The user explicitly clicked "Check for updates", they want fresh data — bypass any intermediate cache (browser-style HTTP cache, OVH static handler default 2-day expires). 3. sign-manifest.php: after hashing the uploaded ZIPs, auto-update `manifest.latest` to the highest version that actually has a ZIP on the server. Prevents the same drift the client now ignores, but keeps the field meaningful for any consumer that reads it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ if (!is_file($manifestPath)) {
|
||||
$manifest = json_decode(file_get_contents($manifestPath), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$updated = 0;
|
||||
$hashedVersions = [];
|
||||
foreach ($manifest['versions'] as &$v) {
|
||||
$version = $v['version'] ?? '?';
|
||||
$url = $v['download']['url'] ?? '';
|
||||
@@ -60,9 +61,23 @@ foreach ($manifest['versions'] as &$v) {
|
||||
$v['download']['sizeBytes'] = $size;
|
||||
$v['download']['sha256'] = $sha;
|
||||
$updated++;
|
||||
$hashedVersions[] = $version;
|
||||
}
|
||||
unset($v);
|
||||
|
||||
// Met à jour automatiquement le champ `latest` avec la plus haute version
|
||||
// effectivement uploadée (celle pour laquelle on a calculé un hash).
|
||||
if (!empty($hashedVersions)) {
|
||||
usort($hashedVersions, function ($a, $b) {
|
||||
return version_compare($a, $b);
|
||||
});
|
||||
$newLatest = end($hashedVersions);
|
||||
if (($manifest['latest'] ?? null) !== $newLatest) {
|
||||
echo " [latest] {$manifest['latest']} -> {$newLatest}\n";
|
||||
$manifest['latest'] = $newLatest;
|
||||
}
|
||||
}
|
||||
|
||||
// (v0.4) Signature Ed25519 — pour l'instant on laisse 'signature' = null.
|
||||
// $config = require __DIR__ . '/../api/config.php';
|
||||
// $sk = sodium_hex2bin($config['ed25519']['private_key_hex']);
|
||||
|
||||
@@ -27,7 +27,15 @@ public sealed class ManifestService : IManifestService
|
||||
{
|
||||
var url = TrimSlash(_serverBaseUrlProvider()) + "/manifest";
|
||||
_logger.LogInformation("Fetching manifest: {Url}", url);
|
||||
using var resp = await _http.GetAsync(url, ct).ConfigureAwait(false);
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
// Bypass des caches intermédiaires — l'utilisateur a explicitement cliqué « Vérifier ».
|
||||
req.Headers.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue
|
||||
{
|
||||
NoCache = true,
|
||||
NoStore = true,
|
||||
};
|
||||
req.Headers.Pragma.ParseAdd("no-cache");
|
||||
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var manifest = await resp.Content.ReadFromJsonAsync<RemoteManifest>(JsonOptions, ct).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException("Empty manifest");
|
||||
|
||||
@@ -26,8 +26,11 @@ public sealed class UpdateChecker : IUpdateChecker
|
||||
try
|
||||
{
|
||||
var manifest = await _manifestService.FetchAsync(ct).ConfigureAwait(false);
|
||||
var latest = manifest.Versions.FirstOrDefault(v => v.Version == manifest.Latest)
|
||||
?? manifest.Versions
|
||||
|
||||
// On ignore le champ `manifest.Latest` (trop facile à oublier au serveur).
|
||||
// On prend toujours la plus haute version SemVer disponible et téléchargeable.
|
||||
var latest = manifest.Versions
|
||||
.Where(v => v.AvailableForDownload)
|
||||
.OrderByDescending(v => SemVer.Parse(v.Version))
|
||||
.FirstOrDefault();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user