internal/Get-MtGitHubAppDeviceToken.ps1

function Get-MtGitHubAppDeviceToken {
    <#
    .SYNOPSIS
    Gets a GitHub App user access token using OAuth device flow.

    .DESCRIPTION
    Starts the GitHub App device flow for the Maester CLI GitHub App and polls until
    the user authorizes the app, denies the request, or the device code expires.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Consistent with other Connect-* interactive flows')]
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true)]
        [string] $ClientId
    )

    $headers = @{
        Accept       = 'application/json'
        'User-Agent' = 'Maester-GitHubCis'
    }

    try {
        $deviceResponse = Invoke-WebRequest -Uri 'https://github.com/login/device/code' -Method POST -Headers $headers -Body @{ client_id = $ClientId } -ContentType 'application/x-www-form-urlencoded' -UseBasicParsing -ErrorAction Stop
        $deviceData = $deviceResponse.Content | ConvertFrom-Json -ErrorAction Stop
    } catch {
        Write-Host "`nFailed to start GitHub device authentication: $($_.Exception.Message)" -ForegroundColor Red
        return [pscustomobject]@{ AccessToken = $null; FailureReason = 'GitHubDeviceFlowStartFailed' }
    }

    $requiredFields = @('device_code', 'user_code', 'verification_uri')
    foreach ($field in $requiredFields) {
        if ($deviceData.PSObject.Properties.Name -notcontains $field -or [string]::IsNullOrWhiteSpace([string]$deviceData.$field)) {
            Write-Host "`nGitHub device authentication returned an incomplete response." -ForegroundColor Red
            return [pscustomobject]@{ AccessToken = $null; FailureReason = 'GitHubDeviceFlowStartFailed' }
        }
    }

    $interval = 5
    if ($deviceData.PSObject.Properties.Name -contains 'interval' -and $deviceData.interval -as [int]) {
        $interval = [int]$deviceData.interval
    }

    $expiresIn = 900
    if ($deviceData.PSObject.Properties.Name -contains 'expires_in' -and $deviceData.expires_in -as [int]) {
        $expiresIn = [int]$deviceData.expires_in
    }

    Write-Host ''
    Write-Host 'GitHub authentication required for Maester.' -ForegroundColor Yellow
    Write-Host "! First copy your one-time code: $($deviceData.user_code)" -ForegroundColor Yellow
    $null = Read-Host "Press Enter to open $($deviceData.verification_uri) in your browser"
    $openedBrowser = Open-MtBrowserUrl -Uri $deviceData.verification_uri
    if (-not $openedBrowser) {
        Write-Host "Open $($deviceData.verification_uri) and enter code $($deviceData.user_code)" -ForegroundColor Yellow
    }
    Write-Host 'Waiting for GitHub authorization...' -ForegroundColor DarkGray

    $deadline = (Get-Date).ToUniversalTime().AddSeconds($expiresIn)
    while ((Get-Date).ToUniversalTime() -lt $deadline) {
        Start-Sleep -Seconds $interval

        try {
            $tokenResponse = Invoke-WebRequest -Uri 'https://github.com/login/oauth/access_token' -Method POST -Headers $headers -Body @{
                client_id   = $ClientId
                device_code = $deviceData.device_code
                grant_type  = 'urn:ietf:params:oauth:grant-type:device_code'
            } -ContentType 'application/x-www-form-urlencoded' -UseBasicParsing -ErrorAction Stop
            $tokenData = $tokenResponse.Content | ConvertFrom-Json -ErrorAction Stop
        } catch {
            Write-Host "`nFailed to complete GitHub device authentication: $($_.Exception.Message)" -ForegroundColor Red
            return [pscustomobject]@{ AccessToken = $null; FailureReason = 'GitHubDeviceFlowFailed' }
        }

        if ($tokenData.PSObject.Properties.Name -contains 'access_token' -and -not [string]::IsNullOrWhiteSpace([string]$tokenData.access_token)) {
            $expiresAt = $null
            if ($tokenData.PSObject.Properties.Name -contains 'expires_in' -and $tokenData.expires_in -as [int]) {
                $expiresAt = (Get-Date).ToUniversalTime().AddSeconds([int]$tokenData.expires_in)
            }
            return [pscustomobject]@{
                AccessToken   = [string]$tokenData.access_token
                ExpiresAt     = $expiresAt
                FailureReason = $null
            }
        }

        $errorName = if ($tokenData.PSObject.Properties.Name -contains 'error') { [string]$tokenData.error } else { $null }
        switch ($errorName) {
            'authorization_pending' {
                continue
            }
            'slow_down' {
                if ($tokenData.PSObject.Properties.Name -contains 'interval' -and $tokenData.interval -as [int]) {
                    $interval = [int]$tokenData.interval
                } else {
                    $interval += 5
                }
                continue
            }
            { $_ -in @('expired_token', 'token_expired') } {
                Write-Host "`nGitHub device authentication expired. Run Connect-MtGitHub again to get a new code." -ForegroundColor Red
                return [pscustomobject]@{ AccessToken = $null; FailureReason = 'GitHubDeviceFlowExpired' }
            }
            'access_denied' {
                Write-Host "`nGitHub device authentication was denied." -ForegroundColor Red
                return [pscustomobject]@{ AccessToken = $null; FailureReason = 'GitHubDeviceFlowDenied' }
            }
            'device_flow_disabled' {
                Write-Host "`nGitHub device flow is not enabled for the Maester GitHub App." -ForegroundColor Red
                return [pscustomobject]@{ AccessToken = $null; FailureReason = 'GitHubDeviceFlowDisabled' }
            }
            default {
                $details = if ($errorName) { " GitHub returned '$errorName'." } else { '' }
                Write-Host "`nGitHub device authentication failed.$details" -ForegroundColor Red
                return [pscustomobject]@{ AccessToken = $null; FailureReason = 'GitHubDeviceFlowFailed' }
            }
        }
    }

    Write-Host "`nGitHub device authentication expired. Run Connect-MtGitHub again to get a new code." -ForegroundColor Red
    return [pscustomobject]@{ AccessToken = $null; FailureReason = 'GitHubDeviceFlowExpired' }
}