Pax8API/Private/Start-Pax8OpenApiUpdateCheck.ps1

function Start-Pax8OpenApiUpdateCheck {
    [CmdletBinding()]
    param (
        [int]$MinimumIntervalHours = 12
    )

    if ($env:PAX8API_DISABLE_UPDATE_CHECK -match '^(1|true|yes)$') {
        return
    }

    $localManifest = Get-Pax8LocalSpecManifest
    if (-not $localManifest) {
        return
    }

    $cachePath = Get-Pax8UpdateCheckCachePath
    if (Test-Path -LiteralPath $cachePath) {
        try {
            $cached = Get-Content -LiteralPath $cachePath -Raw | ConvertFrom-Json
            if ($cached.checkedAt) {
                $lastChecked = [datetimeoffset]::Parse([string]$cached.checkedAt)
                if ($lastChecked -gt [datetimeoffset]::UtcNow.AddHours(-1 * $MinimumIntervalHours)) {
                    return
                }
            }
        } catch {
            Write-Verbose "Ignoring unreadable Pax8 update-check cache: $($_.Exception.Message)"
        }
    }

    $manifestJson = $localManifest | ConvertTo-Json -Depth 20 -Compress
    $jobScript = {
        param($LocalManifestJson, $CachePath)

        try {
            $localManifest = $LocalManifestJson | ConvertFrom-Json
            $remoteSpecs = foreach ($spec in @($localManifest.specs)) {
                $uri = [string]$spec.url
                $response = Invoke-WebRequest -Uri $uri -UseBasicParsing -TimeoutSec 30
                $bytes = [System.Text.Encoding]::UTF8.GetBytes([string]$response.Content)
                $stream = [System.IO.MemoryStream]::new($bytes)
                try {
                    $hash = (Get-FileHash -InputStream $stream -Algorithm SHA256).Hash.ToLowerInvariant()
                } finally {
                    $stream.Dispose()
                }
                $json = $response.Content | ConvertFrom-Json
                [pscustomobject]@{
                    file = [string]$spec.file
                    url = $uri
                    sha256 = $hash
                    bytes = $bytes.Length
                    title = [string]$json.info.title
                    version = [string]$json.info.version
                    pathCount = ($json.paths.PSObject.Properties | Measure-Object).Count
                    operationCount = (($json.paths.PSObject.Properties.Value | ForEach-Object { $_.PSObject.Properties.Name }) | Where-Object { $_ -in @('get', 'post', 'put', 'patch', 'delete') } | Measure-Object).Count
                }
            }

            $localByFile = @{}
            foreach ($spec in @($localManifest.specs)) {
                $localByFile[[string]$spec.file] = $spec
            }

            $changed = @()
            foreach ($remoteSpec in @($remoteSpecs)) {
                $localSpec = $localByFile[[string]$remoteSpec.file]
                if (-not $localSpec -or $localSpec.sha256 -ne $remoteSpec.sha256 -or [int]$localSpec.operationCount -ne [int]$remoteSpec.operationCount) {
                    $changed += $remoteSpec.file
                }
            }

            [pscustomobject]@{
                checkedAt = [datetimeoffset]::UtcNow.ToString('o')
                source = 'https://devx.pax8.com/openapi'
                updateAvailable = [bool]($changed.Count -gt 0)
                changedSpecs = @($changed)
                localGeneratedAt = [string]$localManifest.generatedAt
                generatedOperationCount = [int]$localManifest.generatedOperationCount
                localOperationCount = [int](($localManifest.specs | Measure-Object -Property operationCount -Sum).Sum)
                remoteOperationCount = [int](($remoteSpecs | Measure-Object -Property operationCount -Sum).Sum)
            } | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $CachePath -Encoding utf8NoBOM
        } catch {
            [pscustomobject]@{
                checkedAt = [datetimeoffset]::UtcNow.ToString('o')
                source = 'https://devx.pax8.com/openapi'
                updateAvailable = $false
                error = $_.Exception.Message
            } | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $CachePath -Encoding utf8NoBOM
        }
    }

    if (Get-Command Start-ThreadJob -ErrorAction SilentlyContinue) {
        Start-ThreadJob -Name 'Pax8OpenApiUpdateCheck' -ScriptBlock $jobScript -ArgumentList $manifestJson, $cachePath | Out-Null
    } elseif (Get-Command Start-Job -ErrorAction SilentlyContinue) {
        Start-Job -Name 'Pax8OpenApiUpdateCheck' -ScriptBlock $jobScript -ArgumentList $manifestJson, $cachePath | Out-Null
    }
}