Private/ApiClient/Invoke-SecretsHubApi.ps1

<#
.SYNOPSIS
Invokes Secrets Hub REST API.

.DESCRIPTION
Central function for making API calls with error handling and retry logic.
#>

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

        [Parameter()]
        [string]$Method = 'GET',

        [Parameter()]
        [hashtable]$Body,

        [Parameter()]
        [hashtable]$QueryParameters,

        [Parameter()]
        [hashtable]$AdditionalHeaders,

        [Parameter()]
        [switch]$Beta,

        [Parameter()]
        [int]$MaxRetries = 3,

        [Parameter()]
        [int]$RetryDelay = 1
    )

    process {
        if (-not $script:SecretsHubSession) {
            throw "Not connected to Secrets Hub. Use Connect-SecretsHub first."
        }

        try {
            # Build full URI
            $FullUri = $script:SecretsHubSession.BaseUrl + $Uri.TrimStart('/')

            # Add query parameters
            if ($QueryParameters) {
                $QueryString = ($QueryParameters.GetEnumerator() | ForEach-Object {
                    "$($_.Key)=$([System.Web.HttpUtility]::UrlEncode($_.Value))"
                }) -join '&'
                $FullUri += "?$QueryString"
            }

            # Prepare headers
            $Headers = $script:SecretsHubSession.Headers.Clone()

            # Add beta header if needed
            if ($Beta) {
                $Headers['Accept'] = 'application/x.secretshub.beta+json'
            }

            # Add additional headers
            if ($AdditionalHeaders) {
                foreach ($Header in $AdditionalHeaders.GetEnumerator()) {
                    $Headers[$Header.Key] = $Header.Value
                }
            }

            # Prepare request parameters
            $RequestParams = @{
                Uri = $FullUri
                Method = $Method
                Headers = $Headers
                ErrorAction = 'Stop'
            }

            # Add body if provided
            if ($Body) {
                $RequestParams.Body = ($Body | ConvertTo-Json -Depth 10 -Compress)
                Write-Verbose "Request body: $($RequestParams.Body)"
            }

            # Retry logic
            $Attempt = 0
            do {
                $Attempt++
                try {
                    Write-Verbose "API call: $Method $FullUri (Attempt $Attempt)"
                    $Response = Invoke-RestMethod @RequestParams
                    return $Response
                }
                catch {
                    $StatusCode = $null
                    if ($_.Exception.Response) {
                        $StatusCode = [int]$_.Exception.Response.StatusCode
                    }

                    # Retry on transient errors
                    if ($Attempt -lt $MaxRetries -and ($StatusCode -eq 429 -or $StatusCode -ge 500)) {
                        Write-Warning "API call failed with status $StatusCode, retrying in $RetryDelay seconds..."
                        Start-Sleep -Seconds $RetryDelay
                        $RetryDelay *= 2  # Exponential backoff
                        continue
                    }

                    # Parse error response if available
                    $ErrorDetails = $null
                    try {
                        if ($_.Exception.Response) {
                            $Reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
                            $ErrorBody = $Reader.ReadToEnd()
                            $ErrorDetails = $ErrorBody | ConvertFrom-Json
                        }
                    }
                    catch {
                        # Log error parsing failure but continue with original error
                        Write-Verbose "Failed to parse error response: $($_.Exception.Message)"
                    }

                    # Throw with enhanced error information
                    if ($ErrorDetails) {
                        throw "API Error [$($ErrorDetails.code)]: $($ErrorDetails.message) - $($ErrorDetails.description)"
                    }
                    else {
                        throw "API Error: $($_.Exception.Message)"
                    }
                }
            } while ($Attempt -lt $MaxRetries)
        }
        catch {
            Write-Verbose "API call failed: $($_.Exception.Message)"
            throw
        }
    }
}