2026-09-06 17:04:11 +08:00
|
|
|
param([Parameter(Mandatory=$true)][string]$Archive)
|
|
|
|
|
|
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
|
$archivePath = (Resolve-Path -LiteralPath $Archive).Path
|
|
|
|
|
$archiveDirectory = Split-Path -Parent $archivePath
|
|
|
|
|
$sidecarPath = $archivePath + '.sha256'
|
|
|
|
|
if (-not (Test-Path -LiteralPath $sidecarPath -PathType Leaf)) { throw 'Archive checksum sidecar is missing' }
|
|
|
|
|
$sidecarHash = ((Get-Content -LiteralPath $sidecarPath -Raw).Trim() -split '\s+')[0]
|
|
|
|
|
$archiveHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
|
|
|
if ($sidecarHash -ne $archiveHash) { throw 'Archive checksum sidecar does not match' }
|
|
|
|
|
|
|
|
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
|
|
|
$zip = [IO.Compression.ZipFile]::OpenRead($archivePath)
|
|
|
|
|
try {
|
|
|
|
|
$forbidden = @($zip.Entries | Where-Object {
|
|
|
|
|
$_.FullName -match '(^|/)(\.env|node_modules|runtime)(/|$)' -or
|
|
|
|
|
$_.FullName -match '\.(pem|key|p12|pfx)$'
|
|
|
|
|
})
|
|
|
|
|
if ($forbidden.Count -gt 0) { throw ('Bundle contains forbidden entries: ' + ($forbidden.FullName -join ', ')) }
|
|
|
|
|
$entryCount = $zip.Entries.Count
|
|
|
|
|
} finally {
|
|
|
|
|
$zip.Dispose()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$verificationRoot = Join-Path $archiveDirectory ('.verify-' + [Guid]::NewGuid().ToString('N'))
|
|
|
|
|
New-Item -ItemType Directory -Path $verificationRoot | Out-Null
|
|
|
|
|
try {
|
|
|
|
|
Expand-Archive -LiteralPath $archivePath -DestinationPath $verificationRoot
|
|
|
|
|
$payload = @(Get-ChildItem -LiteralPath $verificationRoot -Directory)
|
|
|
|
|
if ($payload.Count -ne 1) { throw 'Bundle must contain exactly one top-level directory' }
|
|
|
|
|
$manifestPath = Join-Path $payload[0].FullName 'deploy-manifest.json'
|
|
|
|
|
$checksumsPath = Join-Path $payload[0].FullName 'SHA256SUMS'
|
|
|
|
|
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
2026-10-08 10:06:44 +08:00
|
|
|
$expectedMigration = (Get-ChildItem -LiteralPath (Join-Path $payload[0].FullName 'migrations') -Filter '*.up.sql' | ForEach-Object { [int]($_.Name -split '_')[0] } | Measure-Object -Maximum).Maximum
|
|
|
|
|
if ($manifest.schema_version -ne 'server-deployment-bundle-v1' -or $manifest.schema_migration_target -ne $expectedMigration -or $expectedMigration -le 0 -or $manifest.hardware_runtime -ne 'volc_hardware_agent') {
|
2026-09-06 17:04:11 +08:00
|
|
|
throw 'Deployment manifest metadata is invalid'
|
|
|
|
|
}
|
|
|
|
|
$verifiedFiles = 0
|
|
|
|
|
foreach ($line in Get-Content -LiteralPath $checksumsPath) {
|
|
|
|
|
if ($line -notmatch '^([0-9a-f]{64}) (.+)$') { throw "Invalid checksum line: $line" }
|
|
|
|
|
$target = [IO.Path]::GetFullPath((Join-Path $payload[0].FullName $Matches[2]))
|
|
|
|
|
if (-not $target.StartsWith($payload[0].FullName + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Checksum target escapes bundle root' }
|
|
|
|
|
if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { throw "Checksum target is missing: $($Matches[2])" }
|
|
|
|
|
if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne $Matches[1]) { throw "Checksum mismatch: $($Matches[2])" }
|
|
|
|
|
$verifiedFiles++
|
|
|
|
|
}
|
|
|
|
|
if ($verifiedFiles -ne ($manifest.files.Count + 1)) { throw 'Checksum coverage does not match manifest plus manifest file' }
|
|
|
|
|
[ordered]@{ archive = $archivePath; sha256 = $archiveHash; zip_entries = $entryCount; verified_files = $verifiedFiles; candidate_version = $manifest.candidate_version; schema_migration_target = $manifest.schema_migration_target } | ConvertTo-Json -Compress
|
|
|
|
|
} finally {
|
|
|
|
|
$resolvedVerificationRoot = [IO.Path]::GetFullPath($verificationRoot)
|
|
|
|
|
$resolvedArchiveDirectory = [IO.Path]::GetFullPath($archiveDirectory)
|
|
|
|
|
if ($resolvedVerificationRoot.StartsWith($resolvedArchiveDirectory + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -and (Test-Path -LiteralPath $resolvedVerificationRoot)) {
|
|
|
|
|
Remove-Item -LiteralPath $resolvedVerificationRoot -Recurse -Force
|
|
|
|
|
}
|
|
|
|
|
}
|