Private/Invoke-DuneApiRequest.ps1

<#
.SYNOPSIS
Send an authenticated HTTP request to the Dune API.
 
.DESCRIPTION
Core API request function used by all Dune cmdlets. Handles authentication headers (bearer token or web session), pagination via the ExtractItems switch, and error parsing. Automatically asserts that a valid session exists before sending requests.
 
.PARAMETER Uri
The relative API URI path (e.g. 'deployments', 'resources/123'). The base URL is prepended from the session.
 
.PARAMETER Method
The HTTP method. Valid values: GET, POST, PATCH, DELETE, PUT. Defaults to GET.
 
.PARAMETER ExtractItems
If set, enables automatic pagination and extracts the items array from paginated responses. Without this switch, the raw WebResponse is returned.
 
.PARAMETER Body
The request body object. Automatically serialized to JSON.
 
.EXAMPLE
PS> Invoke-DuneApiRequest -Uri "deployments" -ExtractItems
Returns all deployments with automatic pagination.
 
.EXAMPLE
PS> Invoke-DuneApiRequest -Uri "resources/123" -Method PATCH -Body @{State = "Active"}
Sends a PATCH request to update a resource.
#>

function Invoke-DuneApiRequest {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Uri,

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

        [Parameter()]
        [switch]$ExtractItems, # enables paging and extract items / will not return StatusCode

        [Parameter()]
        $Body

        # [Parameter(ParameterSetName='WebSession')]
        # [Microsoft.PowerShell.Commands.WebRequestSession]$WebSession,

        # [Parameter(ParameterSetName='Token')]
        # $Token,

        # [Parameter()]
        # [switch]$SkipAuth
    )

    begin {
        Write-Debug "$($MyInvocation.MyCommand)|begin"
        Assert-DuneSession
        $Url = $("{0}/{1}" -f $DuneSession.DuneApiUrl, $Uri.Trim('/'))
        # $AuthUrl = "{0}{1}" -f $DuneSession.DuneApiUrl, "/auth/credentials"
        # if (-not ($SkipAuth) -and ($DuneSession.Type -eq 'Credential' -and -not $DuneSession.AuthSession -or ($DuneSession.AuthSession.Cookies.GetCookies($AuthUrl)).Expired -contains $True <#-or $DuneSession.SessionUrl -ne $ENV:SB_API_URL -or $DuneSessionUser -ne $ENV:SB_API_USER -or $DuneSessionPass -ne $ENV:SB_API_PASS -or $DuneSessionTenant -ne $ENV:SB_TENANT#>)) {
        # Invoke-DuneApiAuth
        # }
        $Headers = @{
            "X-Tenant"     = $DuneSession.Tenant
            "Accept"       = "application/json"
            "Content-Type" = "application/json"
        }
        if ($DuneSession.Type -eq 'SocialLogin') {
            # Convert SecureString to plain text in a way compatible with PowerShell 5.
            if ($DuneSession.Token -is [System.Security.SecureString]) {
                $PlainToken = $null
                $convertCmd = Get-Command ConvertFrom-SecureString -ErrorAction SilentlyContinue
                if ($convertCmd -and $convertCmd.Parameters.Keys -contains 'AsPlainText') {
                    $PlainToken = $DuneSession.Token | ConvertFrom-SecureString -AsPlainText
                }
                else {
                    $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($DuneSession.Token)
                    try { $PlainToken = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) }
                    finally { if ($bstr) { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } }
                }
            }
            else {
                $PlainToken = $DuneSession.Token
            }
            $Headers.Authorization = "Bearer $PlainToken"
        }
    }

    process {
        $PagingUrl = $Url

        $ResultObjects = @()
        do {

            $WebRequest = @{
                Uri             = $PagingUrl
                Method          = $Method
                Headers         = $Headers
                UseBasicParsing = $true
            }
            if ($DuneSession.Type -eq 'Credential') { $WebRequest.WebSession = $DuneSession.AuthSession }

            If ($DebugPreference -ne 'Continue') { $WebRequest.Verbose = $false }
            If ($Body) {
                $WebRequest.Body = $($Body | ConvertTo-Json -Compress -Depth 16)
            }
            if (([System.Uri]$DuneSession.DuneApiUrl).Host -eq 'localhost') { $WebRequest.SkipCertificateCheck = $true}
            Write-Debug "$($MyInvocation.MyCommand)|process|WebRequest: $($WebRequest | ConvertTo-Json -Depth 16 -Compress)"
            try {
                $Response = Invoke-WebRequest @WebRequest
                if ($Response.StatusCode -notlike "2??") {
                    Write-Error ("{0} {1} returned statuscode: {2} ({3})" -f $Method, $Uri, $Response.StatusCode, $Response.StatusDescription) -ErrorAction Stop
                }
                if ($ExtractItems) {
                    $Results = $Response.Content | ConvertFrom-Json
                    if (($Results | Get-Member).Name -contains 'total' -and ($Results | Get-Member).Name -contains 'items') {
                        $Total = $Results.total
                        $Results = $Results.Items
                    }
                    $ResultObjects += $Results
                    if ($Total -and ($ResultObjects.Count -ne $Total)) {
                        Write-Debug ("$($MyInvocation.MyCommand)|process|Use paging for {0} {1}. (CurrentReturnedItems {2} / TotalReturnItems {3})" -f $Method, $Uri, $ResultObjects.Count, $Total)
                        $PagingUrl = $PagingUrl | Add-UriQueryParam "skip=$($ResultObjects.Count)"
                    }
                }
                else {
                    return $Response
                }
            }
            catch {
                if ($_.Exception.Message) {
                    if ($_.ErrorDetails.Message) {
                        try {
                            $ErrorDetailsMessage = $_.ErrorDetails.Message | ConvertFrom-Json
                            throw ("{0} {1} returned error: {2} ({3})" -f $Method, $Uri, $_.Exception.Message, $ErrorDetailsMessage.responseStatus.message)
                        }
                        catch {
                            $ErrorDetailsMessage = $_.ErrorDetails.Message
                            throw ("{0} {1} returned error: {2} ({3})" -f $Method, $Uri, $_.Exception.Message, $ErrorDetailsMessage)
                        }
                    }
                    else {
                        throw ("{0} {1} returned error: {2}" -f $Method, $Uri, $_.Exception.Message)
                    }
                }
            }
        } until ((-not $ExtractItems) -or (-not $Total) -or ($ResultObjects.Count -eq $Total))
        return $ResultObjects
    }

    end {
        Write-Debug "$($MyInvocation.MyCommand)|end"
    }
}