DEUX features liées :
1. CHANNELS : chaque license peut être attribuée à un manifest distinct
(« channel »). Permet de servir des versions différentes selon le
client. Le serveur lit ?channel=X et sert manifest/versions-{X}.json
avec fallback transparent sur versions.json. NULL = default.
2. BÊTA : nouveau flag isBeta + betaNotes par version dans le manifest.
Visible uniquement par les licenses avec can_see_betas=1. Affichage
d'une pill orange « BÊTA » sur la row + tooltip avec les notes pour
les testeurs. Les installations locales déjà présentes restent
visibles même si l'accès BÊTA est retiré ensuite (on n'efface pas
le disque du client).
DB :
002_channel_betas.sql ajoute channel + can_see_betas sur licenses.
Idempotent (ALTER TABLE IF NOT EXISTS), zero data migration.
Serveur PHP :
- ValidateLicense.php signe channel + canSeeBetas dans la réponse
(ordre des clés CRITIQUE pour matcher le canonical client).
- Manifest.php : whitelist regex anti-traversal sur ?channel=, fallback
silencieux sur versions.json si channel inconnu (évite leak de la
liste de channels par probing).
- SignManifest.php prend un channel optionnel → l'admin peut signer
chaque manifest indépendamment.
- admin/licenses.php : dropdown channel + checkbox bêta sur create,
bouton détails repliable par-row pour edit.
- admin/versions.php : channel switcher en tête, badge BÊTA sur chaque
row, dialog repliable « Bêta » avec checkbox + notes des testeurs.
Client C# :
- License.Channel + License.CanSeeBetas (dans le canonical signé).
- VersionManifest.IsBeta + BetaNotes.
- ManifestService prend un channelProvider via DI, lu depuis license
cachée à chaque fetch (lazy, pas de circular dep).
- MainViewModel.RebuildList filtre les versions IsBeta si !CanSeeBetas
(mais conserve les installées locales — on ne retire pas l'accès
rétroactivement à ce qui est déjà sur disque).
- VersionRowViewModel : props IsBeta / BetaNotes / BetaTooltip.
- MainWindow.xaml : pill orange à côté du n° version pour le featured
et les rows compactes, tooltip dynamique avec les notes testeurs.
Backward compat signature :
Anciennes licenses cachées (signées sans channel/canSeeBetas) sont
toujours validées via un fallback canonical legacy dans VerifySignature.
Sans ce fallback, le passage à v0.26 invaliderait toutes les caches
hors-ligne et bloquerait les users en mobilité.
Migration côté admin : jouer 002_channel_betas.sql sur la base, déployer
les fichiers PHP, créer manifest/versions-{channel}.json pour les
nouveaux channels (l'admin versions.php propose un input « Créer/utiliser
un nouveau channel »). Les licenses existantes restent en channel=NULL
= default = comportement actuel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
4.0 KiB
C#
123 lines
4.0 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
namespace PSLauncher.Models;
|
|
|
|
public sealed class RemoteManifest
|
|
{
|
|
[JsonPropertyName("schemaVersion")]
|
|
public int SchemaVersion { get; set; }
|
|
|
|
[JsonPropertyName("product")]
|
|
public string Product { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("latest")]
|
|
public string Latest { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("publishedAt")]
|
|
public DateTime? PublishedAt { get; set; }
|
|
|
|
[JsonPropertyName("minLauncherVersion")]
|
|
public string? MinLauncherVersion { get; set; }
|
|
|
|
[JsonPropertyName("versions")]
|
|
public List<VersionManifest> Versions { get; set; } = new();
|
|
|
|
[JsonPropertyName("signature")]
|
|
public string? Signature { get; set; }
|
|
|
|
[JsonPropertyName("launcher")]
|
|
public LauncherInfo? Launcher { get; set; }
|
|
}
|
|
|
|
public sealed class LauncherInfo
|
|
{
|
|
[JsonPropertyName("version")]
|
|
public string Version { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("minRequired")]
|
|
public string? MinRequired { get; set; }
|
|
|
|
[JsonPropertyName("download")]
|
|
public DownloadDescriptor Download { get; set; } = new();
|
|
|
|
[JsonPropertyName("releaseNotesUrl")]
|
|
public string? ReleaseNotesUrl { get; set; }
|
|
}
|
|
|
|
public sealed class VersionManifest
|
|
{
|
|
[JsonPropertyName("version")]
|
|
public string Version { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("releasedAt")]
|
|
public DateTime ReleasedAt { get; set; }
|
|
|
|
[JsonPropertyName("executable")]
|
|
public string Executable { get; set; } = "PROSERVE_UE_5_5.exe";
|
|
|
|
[JsonPropertyName("installFolderTemplate")]
|
|
public string InstallFolderTemplate { get; set; } = "PROSERVE v{version}";
|
|
|
|
[JsonPropertyName("download")]
|
|
public DownloadDescriptor Download { get; set; } = new();
|
|
|
|
[JsonPropertyName("releaseNotesUrl")]
|
|
public string? ReleaseNotesUrl { get; set; }
|
|
|
|
[JsonPropertyName("minLicenseDate")]
|
|
public DateTime? MinLicenseDate { get; set; }
|
|
|
|
[JsonPropertyName("availableForDownload")]
|
|
public bool AvailableForDownload { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Si true, version taggée BÊTA — affichage d'un badge orange dans le
|
|
/// launcher + filtrage : seules les licenses avec <c>canSeeBetas=true</c>
|
|
/// voient cette version, les autres l'ignorent (dans <c>RebuildList</c>).
|
|
/// Default false → comportement standard (visible par tous).
|
|
/// </summary>
|
|
[JsonPropertyName("isBeta")]
|
|
public bool IsBeta { get; set; }
|
|
|
|
/// <summary>
|
|
/// Note libre destinée aux testeurs, affichée en tooltip quand on hover le
|
|
/// badge BÊTA. Optionnel.
|
|
/// </summary>
|
|
[JsonPropertyName("betaNotes")]
|
|
public string? BetaNotes { get; set; }
|
|
|
|
[JsonPropertyName("heroImageUrl")]
|
|
public string? HeroImageUrl { get; set; }
|
|
|
|
public string GetInstallFolderName() => InstallFolderTemplate.Replace("{version}", Version);
|
|
}
|
|
|
|
public sealed class DownloadDescriptor
|
|
{
|
|
[JsonPropertyName("url")]
|
|
public string Url { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("sizeBytes")]
|
|
public long SizeBytes { get; set; }
|
|
|
|
[JsonPropertyName("sha256")]
|
|
public string Sha256 { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Algo de vérif d'intégrité du ZIP téléchargé.
|
|
/// Valeurs : "sha256" (défaut, voir <see cref="Sha256"/>) — "none" (pas de vérif).
|
|
/// La sécurité est déjà assurée par la signature Ed25519 du manifest et HTTPS ;
|
|
/// le hash sert surtout à détecter la corruption disque/réseau sur 14 Go.
|
|
/// Pour les très gros builds où on accepte le compromis, mettre "none" évite
|
|
/// 30 s à 3 min de calcul à la fin du DL.
|
|
/// </summary>
|
|
[JsonPropertyName("hashAlgorithm")]
|
|
public string? HashAlgorithm { get; set; }
|
|
|
|
/// <summary>True si on doit vérifier le hash (sha256 non vide, non "REPLACE", algo != "none").</summary>
|
|
public bool ShouldVerifyHash =>
|
|
!string.IsNullOrEmpty(Sha256)
|
|
&& !Sha256.StartsWith("REPLACE", StringComparison.OrdinalIgnoreCase)
|
|
&& !string.Equals(HashAlgorithm, "none", StringComparison.OrdinalIgnoreCase);
|
|
}
|