Private/Invoke-SCARequest.ps1

function Invoke-SCARequest {
    <#
    .SYNOPSIS
        Central REST client used by every public, API-backed psSCA cmdlet.
    .DESCRIPTION
        Resolves the target session and URI, attaches auth/diagnostic headers, serializes the
        request body, and executes the call with retry/backoff for transient failures. Verbose
        and Debug output is redacted before it is written, and any HTTP failure is translated into
        a structured error via Resolve-SCAError.

        Retries apply to GET requests automatically, and to other methods only when -Idempotent is
        passed, so a transient 503 on a POST that creates an access request is never silently
        retried into a duplicate.
    .PARAMETER Session
        A psSCA.Session object, a session name, or omitted to use the current default session.
    .PARAMETER Service
        The CyberArk service that owns the endpoint.
    .PARAMETER Method
        HTTP method to use.
    .PARAMETER Path
        Endpoint path, which may contain {name} placeholders resolved via -PathParameters.
    .PARAMETER PathParameters
        Values to substitute into {name} placeholders in -Path.
    .PARAMETER QueryParameters
        Query string parameters.
    .PARAMETER Body
        Request body object, serialized to JSON.
    .PARAMETER Operation
        Logical operation name (usually the calling cmdlet) used in diagnostics and errors.
    .PARAMETER TypeName
        PSTypeName stamped onto the returned object(s).
    .PARAMETER Idempotent
        Allows retry-on-transient-failure for non-GET methods that are safe to repeat (e.g. a
        search endpoint implemented as POST).
    .OUTPUTS
        System.Object
    #>

    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter()]
        [AllowNull()]
        [object]$Session,

        [Parameter(Mandatory)]
        [ValidateSet('SCA', 'UAP', 'UAR', 'CDS', 'CEM', 'Compass')]
        [string]$Service,

        [Parameter(Mandatory)]
        [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE')]
        [string]$Method,

        [Parameter(Mandatory)]
        [string]$Path,

        [hashtable]$PathParameters,

        [hashtable]$QueryParameters,

        [object]$Body,

        [Parameter(Mandatory)]
        [string]$Operation,

        [string]$TypeName,

        [switch]$Idempotent
    )

    $resolvedSession = Test-SCASession -Session $Session
    $uri = Resolve-SCAUri -Session $resolvedSession -Service $Service -Path $Path -PathParameters $PathParameters -QueryParameters $QueryParameters

    $plainToken = [System.Net.NetworkCredential]::new('', $resolvedSession.AccessToken).Password
    $headers = @{
        Authorization = "Bearer $plainToken"
        Accept        = 'application/json'
        'User-Agent'  = "psSCA/$script:PSSCAModuleVersion"
    }

    $bodyJson = $null
    if ($null -ne $Body) {
        $bodyJson = ConvertTo-SCARequestBody -InputObject $Body
        $headers['Content-Type'] = 'application/json'
    }

    Write-Verbose "psSCA: $Method $uri"
    Write-Debug "psSCA request headers: $((Protect-SCASecret -InputObject $headers) | ConvertTo-Json -Compress)"
    if ($bodyJson) {
        Write-Debug "psSCA request body: $(Protect-SCASecret -InputObject $bodyJson)"
    }

    $retryableStatusCodes = 429, 502, 503, 504
    $maxAttempts = $script:PSSCADefaultRetryCount + 1
    $canRetry = ($Method -eq 'GET') -or $Idempotent.IsPresent

    for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
        try {
            $requestParams = @{
                Uri             = $uri
                Method          = $Method
                Headers         = $headers
                TimeoutSec      = $script:PSSCADefaultTimeoutSec
                UseBasicParsing = $true
                ErrorAction     = 'Stop'
            }
            if ($bodyJson) {
                $requestParams['Body'] = $bodyJson
            }

            $response = Invoke-WebRequest @requestParams
            $parsed = ConvertFrom-SCAResponse -InputObject $response.Content -TypeName $TypeName

            $morePages = Get-SCAPagination -Response $parsed
            if ($morePages) {
                Write-Warning "psSCA: additional results are available for '$Operation' but automatic pagination is not yet implemented for this endpoint. See docs/API-COMPATIBILITY.md."
            }

            return $parsed
        }
        catch {
            $statusCode = $null
            if ($_.Exception -is [Microsoft.PowerShell.Commands.HttpResponseException]) {
                $statusCode = [int]$_.Exception.Response.StatusCode
            }

            $shouldRetry = $canRetry -and ($statusCode -in $retryableStatusCodes) -and ($attempt -lt $maxAttempts)

            if ($shouldRetry) {
                $retryAfterHeader = $null
                if ($_.Exception.Response -and $_.Exception.Response.Headers.RetryAfter) {
                    $retryAfterHeader = $_.Exception.Response.Headers.RetryAfter.Delta.TotalSeconds
                }
                $delaySeconds = if ($retryAfterHeader) {
                    $retryAfterHeader
                }
                else {
                    [Math]::Pow(2, $attempt - 1) * $script:PSSCADefaultRetryDelaySeconds
                }

                Write-Verbose "psSCA: received HTTP $statusCode from $Operation, retrying in $delaySeconds second(s) (attempt $attempt of $maxAttempts)."
                Start-Sleep -Seconds $delaySeconds
                continue
            }

            $errorRecord = Resolve-SCAError -ErrorRecord $_ -Service $Service -Operation $Operation -Uri $uri
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
    }
}