manifestPath = $rootDir . '/manifest/versions.json'; $this->buildsDir = $rootDir . '/builds'; $this->configPath = $rootDir . '/api/config.php'; } private function out(string $line): void { $this->log[] = $line; } /** * Recompute sizes / sha256 for every version that has its ZIP uploaded, * bump 'latest' to the highest signed version, then sign the manifest * with the Ed25519 private key from config.php (if available). * * @return array{ok:bool, log:string[]} */ public function run(): array { if (!is_file($this->manifestPath)) { $this->out("Manifest introuvable : {$this->manifestPath}"); return ['ok' => false, 'log' => $this->log]; } $manifest = json_decode(file_get_contents($this->manifestPath), true); if (!is_array($manifest)) { $this->out("Manifest illisible (JSON invalide)"); return ['ok' => false, 'log' => $this->log]; } $hashedVersions = []; foreach ($manifest['versions'] as &$v) { $version = $v['version'] ?? '?'; $url = $v['download']['url'] ?? ''; if ($url === '') { $this->out(" [skip] $version : pas d'URL dans le manifest"); continue; } $filename = basename(parse_url($url, PHP_URL_PATH) ?: ''); $zip = "{$this->buildsDir}/$filename"; if (!is_file($zip)) { $candidates = glob("{$this->buildsDir}/*{$version}*.zip", GLOB_NOSORT) ?: []; $candidates = array_values(array_filter($candidates, 'is_file')); if (count($candidates) === 1) { $zip = $candidates[0]; $this->out(" [info] $version : URL pointait vers '{$filename}', utilisé '" . basename($zip) . "' à la place"); } else { $this->out(" [skip] $version : ZIP introuvable pour $url"); if (count($candidates) > 1) { $this->out(" Plusieurs candidats : " . implode(', ', array_map('basename', $candidates))); } continue; } } $size = filesize($zip); $sha = hash_file('sha256', $zip); $this->out(" [hash] $version : " . basename($zip) . " ($size octets) sha256={$sha}"); $v['download']['sizeBytes'] = $size; $v['download']['sha256'] = $sha; $hashedVersions[] = $version; } unset($v); // Bump auto de `latest` sur la plus haute version effectivement uploadée if (!empty($hashedVersions)) { usort($hashedVersions, 'version_compare'); $newLatest = end($hashedVersions); if (($manifest['latest'] ?? null) !== $newLatest) { $this->out(" [latest] " . ($manifest['latest'] ?? '(none)') . " -> {$newLatest}"); $manifest['latest'] = $newLatest; } } // Signature Ed25519 if ($this->configPath && is_file($this->configPath)) { $config = require $this->configPath; $sk = $config['ed25519']['private_key_hex'] ?? ''; if ($sk !== '' && strlen($sk) === 128) { $manifest['signature'] = null; $cryptoPath = dirname($this->configPath) . '/lib/Crypto.php'; if (is_file($cryptoPath)) require_once $cryptoPath; if (class_exists('\PSLauncher\Crypto')) { $payload = \PSLauncher\Crypto::canonicalJson($manifest); $manifest['signature'] = \PSLauncher\Crypto::signEd25519($payload, $sk); $this->out(" [sign] manifest signé (Ed25519)"); } else { $this->out(" [warn] Classe \\PSLauncher\\Crypto introuvable, manifest non signé"); } } else { $this->out(" [warn] ed25519.private_key_hex non configuré, manifest non signé"); } } $jsonOut = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; if (file_put_contents($this->manifestPath, $jsonOut) === false) { $this->out("Échec d'écriture du manifest : {$this->manifestPath}"); return ['ok' => false, 'log' => $this->log]; } $this->out("Manifest mis à jour (" . count($hashedVersions) . " version(s))."); return ['ok' => true, 'log' => $this->log]; } }