Private/Read-WindowsDeviceLinkConfiguration.ps1

function Read-WindowsDeviceLinkConfiguration {
    [CmdletBinding()]
    param([Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Configuration)

    $value = $Configuration.Trim().TrimStart([char]0xFEFF)
    $source = 'Inline JSON'
    if ($value.StartsWith('{') -or $value.StartsWith('[')) {
        $json = $value
    }
    elseif ($value -match '^[a-zA-Z][a-zA-Z0-9+.-]*://') {
        $uri = $null
        if (-not [uri]::TryCreate($value,[UriKind]::Absolute,[ref]$uri) -or
            $uri.Scheme -ne 'https' -or $uri.UserInfo -or $uri.Fragment) {
            throw 'Configuration URLs must use HTTPS without embedded credentials or a fragment.'
        }
        [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
        try {
            # Do not follow redirects to another origin or to HTTP. Configuration is data, never code.
            $response = Invoke-WebRequest -Uri $uri.AbsoluteUri -Method Get -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 15 -ErrorAction Stop
            if ([int]$response.StatusCode -ne 200) { throw 'Unexpected HTTP status.' }
            $json = [string]$response.Content
        }
        catch { throw 'Unable to download configuration. Use a trusted HTTPS endpoint that returns JSON directly (no redirects).' }
        $source = $uri.GetLeftPart([UriPartial]::Path)
    }
    else {
        try {
            $path = Resolve-Path -LiteralPath $Configuration -ErrorAction Stop
            if ($path.Provider.Name -ne 'FileSystem' -or -not (Test-Path -LiteralPath $path.ProviderPath -PathType Leaf)) {
                throw 'Not a file.'
            }
            if ((Get-Item -LiteralPath $path.ProviderPath).Length -gt 1MB) { throw 'File too large.' }
            $json = Get-Content -LiteralPath $path.ProviderPath -Raw -Encoding UTF8 -ErrorAction Stop
            $source = $path.ProviderPath
        }
        catch { throw 'Unable to read configuration. Supply an existing JSON file (up to 1 MiB), an HTTPS URL, or inline JSON.' }
    }

    if ([string]::IsNullOrWhiteSpace($json) -or [Text.Encoding]::UTF8.GetByteCount($json) -gt 1MB) {
        throw 'Configuration must contain JSON and must not exceed 1 MiB.'
    }
    # The root must be an object; ConvertFrom-Json can unwrap single-element arrays.
    if (-not $json.Trim().TrimStart([char]0xFEFF).StartsWith('{')) { throw 'Configuration must be a JSON object.' }
    try { $document = ConvertFrom-Json -InputObject $json -ErrorAction Stop }
    catch { throw 'Configuration contains invalid JSON.' }
    ConvertTo-WindowsDeviceLinkTenantCatalog -InputObject $document -Source $source
}