Public/Connect-DattoApi.ps1

function Connect-DattoApi {
<#
.SYNOPSIS
Creates an authenticated Datto REST API connection.
.DESCRIPTION
Creates a named in-memory connection to https://api.datto.com by default. The public key is stored as the PSCredential username and the secret key remains a SecureString while at rest in module memory. Credentials are never written to disk by this module.
.PARAMETER Credential
A PSCredential whose username is the Datto public key and whose password is the Datto secret key.
.PARAMETER PublicKey
The Datto API public key.
.PARAMETER SecretKey
The Datto API secret key as a SecureString.
.PARAMETER Name
A unique friendly name for the connection.
.PARAMETER BaseUri
The Datto API origin. HTTPS is required unless -AllowInsecureHttp is explicitly used for a local test server.
.PARAMETER TimeoutSeconds
The HTTP request timeout in seconds.
.PARAMETER MaxRetryCount
The maximum number of retries for idempotent requests after transient transport failures or HTTP 408, 429, 500, 502, 503, or 504 responses.
.PARAMETER MaxRetryDelaySeconds
The upper bound for each retry delay.
.PARAMETER SetDefault
Makes this connection the default connection.
.PARAMETER Force
Replaces an existing connection with the same name.
.PARAMETER AllowInsecureHttp
Allows an HTTP base URI. This is intended only for isolated local mock testing and should not be used with production credentials.
.EXAMPLE
$credential = Get-Credential -UserName 'PUBLIC_KEY'
Connect-DattoApi -Credential $credential
.EXAMPLE
$secret = Read-Host 'Datto secret key' -AsSecureString
Connect-DattoApi -PublicKey 'PUBLIC_KEY' -SecretKey $secret -Name 'Production' -SetDefault
.OUTPUTS
DattoBackupApi.Connection
#>

    [CmdletBinding(DefaultParameterSetName = 'Credential', SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Credential')]
        [ValidateNotNull()]
        [pscredential]$Credential,

        [Parameter(Mandatory, ParameterSetName = 'Keys')]
        [ValidateNotNullOrEmpty()]
        [string]$PublicKey,

        [Parameter(Mandatory, ParameterSetName = 'Keys')]
        [ValidateNotNull()]
        [securestring]$SecretKey,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Name = 'Default',

        [Parameter()]
        [ValidateNotNull()]
        [uri]$BaseUri = 'https://api.datto.com',

        [Parameter()]
        [ValidateRange(1, 3600)]
        [int]$TimeoutSeconds = 100,

        [Parameter()]
        [ValidateRange(0, 10)]
        [int]$MaxRetryCount = 3,

        [Parameter()]
        [ValidateRange(1, 300)]
        [int]$MaxRetryDelaySeconds = 60,

        [Parameter()]
        [switch]$SetDefault,

        [Parameter()]
        [switch]$Force,

        [Parameter(DontShow)]
        [switch]$AllowInsecureHttp
    )

    if ($PSCmdlet.ParameterSetName -eq 'Keys') {
        $Credential = [pscredential]::new($PublicKey, $SecretKey)
    }

    if ([string]::IsNullOrWhiteSpace($Name)) {
        throw [System.ArgumentException]::new('Connection Name cannot be empty or whitespace.')
    }
    if ([string]::IsNullOrWhiteSpace($Credential.UserName)) {
        throw [System.ArgumentException]::new('The PSCredential username must contain the Datto public key.')
    }
    if ($Credential.UserName.Contains(':')) {
        throw [System.ArgumentException]::new('The Datto public key cannot contain a colon because it is used as the HTTP Basic username.')
    }
    if ($Credential.Password.Length -eq 0) {
        throw [System.ArgumentException]::new('The Datto secret key cannot be empty.')
    }

    if (-not $BaseUri.IsAbsoluteUri) {
        throw [System.ArgumentException]::new('BaseUri must be an absolute URI.')
    }
    if ($BaseUri.Scheme -ne 'https' -and -not ($AllowInsecureHttp -and $BaseUri.Scheme -eq 'http')) {
        throw [System.ArgumentException]::new('BaseUri must use HTTPS. Use -AllowInsecureHttp only with an isolated local mock server.')
    }
    if (-not [string]::IsNullOrEmpty($BaseUri.UserInfo)) {
        throw [System.ArgumentException]::new('BaseUri must not contain embedded credentials.')
    }
    if (-not [string]::IsNullOrEmpty($BaseUri.Query) -or -not [string]::IsNullOrEmpty($BaseUri.Fragment)) {
        throw [System.ArgumentException]::new('BaseUri must not contain a query string or fragment.')
    }
    if ($BaseUri.AbsolutePath -ne '/') {
        throw [System.ArgumentException]::new('BaseUri must identify an API origin and cannot contain a path.')
    }
    $baseHost = $BaseUri.DnsSafeHost.Trim([char[]]'[]')
    if ($AllowInsecureHttp -and $BaseUri.Scheme -eq 'http' -and -not ($baseHost -in @('localhost', '127.0.0.1', '::1'))) {
        throw [System.ArgumentException]::new('Insecure HTTP connections are restricted to localhost addresses.')
    }

    $existing = Find-DattoApiConnectionStateByName -Name $Name
    if ($null -ne $existing -and -not $Force) {
        throw [System.InvalidOperationException]::new("A Datto API connection named '$Name' already exists. Use -Force to replace it.")
    }

    if (-not $PSCmdlet.ShouldProcess("Datto API connection '$Name'", 'Create or replace in-memory connection')) {
        return
    }

    $handler = $null
    $client = $null
    try {
        $handler = [System.Net.Http.HttpClientHandler]::new()
        $handler.AllowAutoRedirect = $false
        $handler.AutomaticDecompression = [Net.DecompressionMethods]::GZip -bor [Net.DecompressionMethods]::Deflate
        $client = [System.Net.Http.HttpClient]::new($handler, $true)
        $client.Timeout = [TimeSpan]::FromSeconds($TimeoutSeconds)
        $null = $client.DefaultRequestHeaders.UserAgent.TryParseAdd(('DattoBackupApi/{0}' -f $script:DattoApiModuleVersion))
        $null = $client.DefaultRequestHeaders.UserAgent.TryParseAdd(('PowerShell/{0}' -f $PSVersionTable.PSVersion))

        $id = [guid]::NewGuid().ToString()
        $normalisedBaseUri = [uri]($BaseUri.AbsoluteUri.TrimEnd('/'))
        $state = [ordered]@{
            Id                   = $id
            Name                 = $Name
            BaseUri              = $normalisedBaseUri
            Credential           = $Credential
            Client               = $client
            Handler              = $handler
            CreatedAtUtc         = [datetimeoffset]::UtcNow
            LastUsedAtUtc        = $null
            TimeoutSeconds       = $TimeoutSeconds
            MaxRetryCount        = $MaxRetryCount
            MaxRetryDelaySeconds = $MaxRetryDelaySeconds
            RateLimit            = [ordered]@{
                Remaining    = $null
                ResetEpoch   = $null
                ResetAtUtc   = $null
                Cost          = $null
                ObservedAtUtc = $null
            }
        }
    }
    catch {
        if ($null -ne $client) {
            try { $client.Dispose() } catch { Write-Debug ('HttpClient cleanup failed: {0}' -f $_.Exception.Message) }
        }
        elseif ($null -ne $handler) {
            try { $handler.Dispose() } catch { Write-Debug ('HttpClientHandler cleanup failed: {0}' -f $_.Exception.Message) }
        }
        throw
    }

    # Register the fully constructed replacement before disposing the previous
    # connection so a construction failure cannot destroy a working session.
    $script:DattoApiState.Connections[$id] = $state
    $wasDefault = $false
    if ($null -ne $existing) {
        $wasDefault = ([string]$script:DattoApiState.DefaultConnectionId -eq [string]$existing.Id)
        Remove-DattoApiConnectionInternal -ConnectionState $existing
        $null = $script:DattoApiState.Connections.Remove([string]$existing.Id)
    }

    if ($SetDefault -or $wasDefault -or [string]::IsNullOrWhiteSpace([string]$script:DattoApiState.DefaultConnectionId)) {
        $script:DattoApiState.DefaultConnectionId = $id
    }

    return (Get-DattoApiConnectionView -ConnectionState $state)
}