GraphApiHelper.psm1

#region Public commands
function Add-GraphLargeFile
{
    <#
    .SYNOPSIS
    Uploads large files to Microsoft Graph using the resumable upload protocol
     
    .DESCRIPTION
    Uploads large files to Microsoft Graph (OneDrive, SharePoint, etc.) using the upload session API.
    This function handles files of any size by splitting them into chunks and uploading them sequentially.
     
    The upload uses 5MB chunks (320KB * 16). The function automatically creates an upload session and
    manages the chunked upload process for the current invocation.
     
    .PARAMETER LocalFilePath
    The full path to the local file to upload. The file must exist and be readable.
     
    .PARAMETER GraphFilePath
    The Microsoft Graph API path where the file should be uploaded, excluding the ':/createUploadSession' suffix.
    Example: 'https://graph.microsoft.com/v1.0/me/drive/root:/Documents/myfile.pdf'
     
    .EXAMPLE
    Add-GraphLargeFile -LocalFilePath 'C:\Files\presentation.pptx' -GraphFilePath 'https://graph.microsoft.com/v1.0/me/drive/root:/Documents/presentation.pptx'
     
    Uploads a PowerPoint file to the current user's OneDrive Documents folder.
     
    .EXAMPLE
    Add-GraphLargeFile -LocalFilePath 'C:\Videos\training.mp4' -GraphFilePath 'https://graph.microsoft.com/v1.0/sites/{site-id}/drive/root:/Videos/training.mp4' -Verbose
     
    Uploads a video file to a SharePoint site's Videos folder with verbose output showing upload progress.
 
    .OUTPUTS
    None
    The function streams upload chunk requests and does not emit the final driveItem object.
 
    .INPUTS
    None
    This command does not accept pipeline input.
     
    .NOTES
    - Uses 5MB chunks for optimal performance
    - Automatically handles upload session creation
    - Uses conflict behavior 'replace' so existing files are overwritten
    - Uses Invoke-GraphWithRetry internally for reliability
    - Enable -Verbose to see detailed upload progress
    - Uses the authentication factory configured via Set-GraphAadFactory
     
    .LINK
    https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $LocalFilePath,
        [Parameter(Mandatory)]
        $GraphFilePath
    )

    begin
    {
        $chunkSize = 320KB * 16 # 5MB chunks
        $graphUri = New-GraphUri -Uri "$GraphFilePath"
    }
    process
    {
        try {
            $item = Get-Item -Path $LocalFilePath
            $fileSize = $item.length
            $fileStream = [System.IO.File]::OpenRead($item.FullName)
            Write-Verbose "Filesize: $fileSize"
            Write-Verbose "Chunksize: $chunkSize"
            $payload =  @{
                item = @{
                    '@microsoft.graph.conflictBehavior' = 'replace' 
                }
            }
            Write-Verbose "Requesting upload session on $graphUri`:/createUploadSession"
            try {
                $uploadSession = Invoke-GraphWithRetry `
                    -RequestUri "$graphUri`:/createUploadSession" `
                    -method Post `
                    -body ($payload | ConvertTo-Json -Depth 10) `
                    -ErrorAction Stop
            }
            catch {
                Write-Error -ErrorRecord $_
                return
            }
            if($null -ne $uploadSession.uploadUrl)
            {
                Write-Verbose "Upload session created: $($uploadSession.uploadUrl)"
                $uploadUrl = $uploadSession.uploadUrl
                $offset = 0
                
                try
                {
                    while ($offset -lt $fileSize) {
                        $bytesToRead = [Math]::Min($chunkSize, $fileSize - $offset)
                        $buffer = New-Object byte[] $bytesToRead
                        $bytesRead = $fileStream.Read($buffer, 0, $bytesToRead)
            
                        if ($bytesRead -gt 0) {
                            $contentRange = "bytes $offset-$($offset + $bytesRead - 1)/$fileSize"
                            Write-Verbose "Writing range: $contentRange"
                            Invoke-GraphWithRetry `
                                -RequestUri $uploadUrl `
                                -method Put `
                                -body $buffer `
                                -headers @{ 'Content-Range' = $contentRange } `
                                -ErrorAction Stop `
                                -ContentType 'application/octet-stream' | out-null
                            $offset += $bytesRead
                        }
                    }
                }
                catch
                {
                    Write-Error -ErrorRecord $_
                }
            }
            else
            {
                Write-Error "Failed to create upload session. Response: $($uploadSession | ConvertTo-Json -Depth 10)"
            }
        }
        finally {
            if($null -ne $fileStream)
            {
                $fileStream.Close()
            }
        }
    }
}
function Add-GraphReference
{
    <#
    .SYNOPSIS
    Adds a reference to a Microsoft Graph object.
 
    .DESCRIPTION
    Adds a reference to a Microsoft Graph group, application, or service principal.
    This is typically used to add members or owners by creating the corresponding $ref link.
 
    .PARAMETER ObjectId
    The identifier of the Microsoft Graph object that will receive the reference.
 
    .PARAMETER objectType
    The Microsoft Graph object type. Valid values are groups, applications, and servicePrincipals.
 
    .PARAMETER ReferenceType
    The reference collection to update. Valid values are members and owners.
 
    .PARAMETER MemberId
    The identifier of the object being referenced, such as a user, group, or service principal.
 
    .PARAMETER PermissiveModify
    Suppresses errors when the reference already exists.
 
    .INPUTS
    System.String
    Accepts MemberId values from the pipeline.
 
    .OUTPUTS
    None
    This command performs a Graph API call and does not emit output.
 
    .EXAMPLE
    Add-GraphReference -ObjectId $groupId -MemberId $userId
 
    Adds the specified user as a member of the group.
 
    .EXAMPLE
    Add-GraphReference -ObjectId $groupId -ReferenceType owners -MemberId $userId -PermissiveModify
 
    Adds the specified user as a group owner and ignores the request if the reference already exists.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $ObjectId,
        [Parameter()]
        [ValidateSet('groups','applications','servicePrincipals')]
        [string]$objectType = 'groups',
        [Parameter()]
        [ValidateSet('members', 'owners')]
        [string]$ReferenceType = 'members',
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$MemberId,
        [switch]$PermissiveModify
    )

    begin
    {
        $uri = New-GraphUri -Uri "/$objectType/$ObjectId/$ReferenceType/`$ref"
    }
    process
    {
        $body = @{
            "@odata.id" = $script:graphConnection.GetReference($MemberId)
        } | ConvertTo-Json
        try
        {
            # we want this to throw, so to honor the -PermissiveModify switch
            Invoke-GraphWithRetry -Method Post -Uri $uri -Body $body -ErrorAction Stop
            Write-Verbose "User with ID $MemberId added to $ReferenceType of $ObjectId."
        }
        catch
        {
            $details = $_ | ConvertFrom-GraphErrorRecord
            if($details.error.message -match 'object references already exist' -and $PermissiveModify)
            {
                Write-Verbose -Message "User with ID $MemberId is already a $ReferenceType of $ObjectId."
            }
            else
            {
                Write-Error -ErrorRecord $_
            }
        }
    }
}
function ConvertFrom-GraphErrorRecord
{
    <#
    .SYNOPSIS
    Extracts Microsoft Graph error details from a PowerShell error record.
 
    .DESCRIPTION
    Parses the ErrorDetails payload from a PowerShell ErrorRecord and returns the
    deserialized Graph error object when it contains an error message.
 
    This helper is useful when handling failures from Invoke-GraphWithRetry and
    other commands that return Graph error payloads in JSON format.
 
    .PARAMETER ErrorRecord
    The PowerShell ErrorRecord to parse.
 
    .INPUTS
    System.Management.Automation.ErrorRecord
    Accepts error records from the pipeline.
 
    .OUTPUTS
    System.Object
    Returns the deserialized Graph error object when available.
 
    .EXAMPLE
    try {
        Invoke-GraphWithRetry -RequestUri 'https://graph.microsoft.com/v1.0/users/does-not-exist' -ErrorAction Stop
    }
    catch {
        $_ | ConvertFrom-GraphErrorRecord
    }
 
    Parses the Graph error payload from the caught exception.
 
    .EXAMPLE
    $details = $Error[0] | ConvertFrom-GraphErrorRecord
 
    Parses the most recent error record and returns Graph error details when present.
 
    .NOTES
    Returns nothing when the error details are not JSON or do not contain error.message.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )

    process
    {
        $details = $ErrorRecord.ErrorDetails | ConvertFrom-Json -ErrorAction SilentlyContinue
        if($null -ne $details.error.message)
        {
            $details
        }
    }
}
function Get-GraphAuthorizationHeader
{
    <#
    .SYNOPSIS
    Retrieves an authorization header for Microsoft Graph API calls
     
    .DESCRIPTION
    Obtains an access token from the configured AAD authentication factory with the Graph API scope
    and returns it as a hashtable containing the Authorization header.
    This command can be called directly but is primarily used by other module functions.
 
    .PARAMETER FactoryName
    Optional factory name override used to obtain the token. If omitted, the factory configured
    by Set-GraphAadFactory is used.
 
    .INPUTS
    None
    This command does not accept pipeline input.
     
    .OUTPUTS
    System.Collections.Hashtable
    Returns a hashtable with the Authorization header containing the Bearer token.
     
    .EXAMPLE
    $authHeader = Get-GraphAuthorizationHeader
     
    Retrieves the authorization header for Graph API calls.
 
    .EXAMPLE
    $authHeader = Get-GraphAuthorizationHeader -FactoryName 'ManagedIdentityFactory'
 
    Retrieves the authorization header by explicitly selecting a token factory.
     
    .NOTES
    This function uses the scopes configured via Set-GraphScopes and the factory configured via Set-GraphAadFactory.
    #>

    param (
        $FactoryName = $script:graphConnection.FactoryName
    )

    process
    {
        Get-AadToken -Factory $FactoryName -Scope $script:graphConnection.GraphScope -AsHashTable
    }
}
function Get-GraphData
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
    <#
    .SYNOPSIS
    Retrieves data from Microsoft Graph API with automatic pagination
     
    .DESCRIPTION
    Executes a Microsoft Graph API GET request and automatically handles pagination by following @odata.nextLink references.
    This function retrieves all pages of data and returns the complete dataset. It uses Invoke-GraphWithRetry internally,
    so it inherits automatic retry logic for throttling.
     
    The function intelligently handles both single objects and arrays of results from the Graph API.
     
    .PARAMETER RequestUri
    The complete Microsoft Graph API request URL including query parameters.
    Example: 'https://graph.microsoft.com/v1.0/users'
 
    .PARAMETER WithSelect
    Optional values for the $select query option.
    Example: 'id,displayName,userPrincipalName'
 
    .PARAMETER WithFilter
    Optional value for the $filter query option.
    Example: "accountEnabled eq true"
 
    .PARAMETER WithCount
    Adds $count=true to the request.
 
    .PARAMETER WithExpand
    Optional value for the $expand query option.
 
    .PARAMETER WithSearch
    Optional value for the $search query option.
 
    .PARAMETER Top
    Optional value for the $top query option.
 
    .PARAMETER Skip
    Optional value for the $skip query option.
 
    .PARAMETER RetryableErrorCodes
    HTTP status codes that should be treated as transient and retried by Invoke-GraphWithRetry.
    Default is 429.
     
    .PARAMETER OperationName
    The operation name to use for Application Insights logging. Default is 'Get-GraphData'.
 
    .PARAMETER AdditionalHeaders
    Additional HTTP headers to include in requests (for example ConsistencyLevel for advanced queries).
 
    .PARAMETER NoContinue
    When specified, retrieves only the first page and does not follow @odata.nextLink.
 
    .INPUTS
    None
    This command does not accept pipeline input.
     
    .OUTPUTS
    System.Object
    Returns Graph response objects, automatically handling pagination for collection responses.
     
    .EXAMPLE
    Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/users'
     
    Retrieves all users from Microsoft Graph, automatically paginating through all result pages.
     
    .EXAMPLE
    Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,''Sales'')'
     
    Retrieves all groups whose display name starts with 'Sales', handling pagination automatically.
     
    .EXAMPLE
    Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/me/messages?$top=50' -OperationName 'GetUserMessages'
     
    Retrieves all messages for the current user with custom operation name for Application Insights tracking.
 
    .EXAMPLE
    Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/users' -WithSelect 'id,displayName' -WithFilter "startswith(displayName,'A')" -WithCount -AdditionalHeaders @{ ConsistencyLevel = 'eventual' }
 
    Retrieves users with query options built from parameters and the required advanced query header.
 
    .EXAMPLE
    Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/users' -WhatIf
 
    Shows what request would be executed without calling Microsoft Graph.
 
    .EXAMPLE
    Get-GraphData -RequestUri 'https://graph.microsoft.com/v1.0/users' -RetryableErrorCodes 429,503
 
    Retrieves users while treating 429 and 503 responses as retryable transient failures.
     
    .NOTES
    - Automatically handles pagination via @odata.nextLink
    - Uses Invoke-GraphWithRetry internally for throttling protection
    - Suitable for large datasets that span multiple pages
    - Uses the authentication factory configured via Set-GraphAadFactory
    - Uses the Graph scopes configured via Set-GraphScopes
    - Supports -WhatIf and -Confirm via ShouldProcess
    #>

    param
    (
        [Parameter(Mandatory)]
        [Alias('Uri')]
        [string]$RequestUri,
        [Parameter()]
        [string[]]$WithSelect,
        [Parameter()]
        [string]$WithFilter,
        [Parameter()]
        [switch]$WithCount,
        [Parameter()]
        [string]$WithExpand,
        [Parameter()]
        [string]$WithSearch,
        [Parameter()]
        [Nullable[int]]$Top,
        [Parameter()]
        [Nullable[int]]$Skip,
        [Parameter()]
        [int[]]$RetryableErrorCodes = @(429),
        [Parameter()]
        [string]$OperationName = 'Get-GraphData',
        [Parameter()]
        [System.Collections.Hashtable]$AdditionalHeaders = @{},
        [Parameter()]
        [switch]$NoContinue
    )

    process
    {
        $uri = New-GraphUri -Uri $RequestUri -WithSelect $WithSelect -WithFilter $WithFilter -WithCount:$WithCount -WithExpand $WithExpand -WithSearch $WithSearch -Top $Top -Skip $Skip

        if (-not $PSCmdlet.ShouldProcess($uri, 'Get Microsoft Graph data with automatic pagination'))
        {
            return
        }

        while($true)
        {
            #get page of results
            $result = Invoke-GraphWithRetry `
                -RequestUri $uri `
                -method Get `
                -Headers $AdditionalHeaders `
                -OperationName $OperationName `
                -Confirm:$false `
                -ErrorAction $ErrorActionPreference `
                -RetryableErrorCodes $RetryableErrorCodes
            if($null -ne $result.value)
            {
                #returning array of results
                $result.value
            }
            else
            {
                #returning single object
                $result
            }
            $uri = $result.'@odata.nextLink'
            if([string]::IsNullOrEmpty($uri) -or $NoContinue)
            {
                #no more pages or we just wanted first page
                break;
            }
        }
    }
}
function Invoke-GraphBatch
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    <#
    .SYNOPSIS
    Sends a Microsoft Graph batch request.
 
    .DESCRIPTION
    Collects one or more batch request definitions, builds the Graph $batch payload,
    sends it through Invoke-GraphWithRetry, and returns batch response items.
 
    .PARAMETER BatchRequest
    One or more Graph batch request objects created by New-GraphBatchRequest.
 
    .PARAMETER RequestHeaders
    Additional HTTP headers for the outer $batch request.
 
    .PARAMETER RetryableErrorCodes
    HTTP status codes that should be treated as transient and retried by Invoke-GraphWithRetry.
    Default is 429.
 
    .PARAMETER OperationName
    The operation name to use for Application Insights logging. Default is 'Invoke-GraphBatch'.
 
    .OUTPUTS
    System.Object[]
    Returns response items from the Graph batch response.
 
    .EXAMPLE
    $requests = @(
        New-GraphBatchRequest -Id '1' -Method GET -Url '/me'
        New-GraphBatchRequest -Id '2' -Method GET -Url (New-GraphUri -Uri '/users' -Top 5 -Relative)
        New-GraphBatchRequest -Id '3' -Method POST -Url '/groups' -Body @{ displayName = 'Batch Group'; mailEnabled = $false; mailNickname = 'batch-group'; securityEnabled = $true }
    )
 
    Invoke-GraphBatch -BatchRequest $requests
 
    Sends three Graph API requests in one batch and returns the response items. Use New-GraphUri with -Relative to build query strings cleanly.
 
    .INPUTS
    System.Management.Automation.PSCustomObject[]
    Accepts batch request objects from the pipeline.
 
    .EXAMPLE
    @(
        New-GraphBatchRequest -Id '1' -Method GET -Url '/me'
        New-GraphBatchRequest -Id '2' -Method GET -Url '/organization'
    ) | Invoke-GraphBatch
 
    Sends request definitions from the pipeline.
 
    .EXAMPLE
    Invoke-GraphBatch -BatchRequest $requests -RetryableErrorCodes 429,503
 
    Sends batch requests while treating 429 and 503 responses from the outer batch call as retryable transient failures.
 
    .NOTES
    - Uses Invoke-GraphWithRetry internally for reliability.
    - Sends to the /$batch endpoint under the configured BaseUri.
    - Microsoft Graph batch requests support up to 20 subrequests per batch.
    #>

    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [Alias('Requests')]
        [PSCustomObject[]]$BatchRequest,
        [Parameter()]
        [int[]]$RetryableErrorCodes = @(429),
        [Parameter()]
        [System.Collections.Hashtable]$RequestHeaders = @{},
        [Parameter()]
        [string]$OperationName = 'Invoke-GraphBatch'
    )

    begin
    {
        $requests = [System.Collections.Generic.List[hashtable]]::new()
        $ids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    }

    process
    {
        foreach ($item in $BatchRequest)
        {
            if ($null -eq $item)
            {
                continue
            }

            $propertyNames = $item.PSObject.Properties.Name
            if ('id' -notin $propertyNames -or 'method' -notin $propertyNames -or 'url' -notin $propertyNames)
            {
                throw 'Each batch request must include id, method, and url properties. Use New-GraphBatchRequest to create requests.'
            }

            $id = [string]$item.id
            $method = [string]$item.method
            $url = [string]$item.url

            if ([string]::IsNullOrWhiteSpace($id) -or [string]::IsNullOrWhiteSpace($method) -or [string]::IsNullOrWhiteSpace($url))
            {
                throw 'Each batch request must include non-empty id, method, and url values.'
            }

            if (-not $ids.Add($id))
            {
                throw "Duplicate batch request id '$id' is not allowed."
            }

            $normalizedRequest = [ordered]@{
                id = [string]$id
                method = [string]$method.ToUpperInvariant()
                url = [string]$url
            }

            $headers = @{}
            $providedHeaders = $item.headers
            if ($null -ne $providedHeaders)
            {
                foreach ($key in $providedHeaders.Keys)
                {
                    $headers[$key] = $providedHeaders[$key]
                }
            }

            $bodyWasProvided = $false
            if ('body' -in $propertyNames)
            {
                $normalizedRequest.body = $item.body
                $bodyWasProvided = $true
            }

            if ($bodyWasProvided -and -not $headers.ContainsKey('Content-Type'))
            {
                $headers['Content-Type'] = 'application/json'
            }

            if ($headers.Count -gt 0)
            {
                $normalizedRequest.headers = $headers
            }

            if ('dependsOn' -in $propertyNames -and $null -ne $item.dependsOn -and $item.dependsOn.Count -gt 0)
            {
                $normalizedRequest.dependsOn = $item.dependsOn
            }

            [void]$requests.Add($normalizedRequest)
        }
    }

    end
    {
        if ($requests.Count -eq 0)
        {
            Write-Warning 'No batch requests were provided.'
            return
        }

        if ($requests.Count -gt 20)
        {
            throw "Microsoft Graph batch requests support a maximum of 20 subrequests per batch. Received $($requests.Count)."
        }

        $batchUri = New-GraphUri -Uri '/$batch'
        if (-not $PSCmdlet.ShouldProcess($batchUri, "Post Microsoft Graph batch request with $($requests.Count) subrequests"))
        {
            return
        }

        $payload = @{ requests = $requests }
        $result = Invoke-GraphWithRetry `
            -RequestUri $batchUri `
            -Method Post `
            -Body ($payload | ConvertTo-Json -Depth 20) `
            -ContentType 'application/json' `
            -Headers $RequestHeaders `
            -RetryableErrorCodes $RetryableErrorCodes `
            -OperationName $OperationName `
            -ErrorAction $ErrorActionPreference `
            -Confirm:$false

        if ($null -ne $result.responses)
        {
            $result.responses
        }
        else
        {
            $result
        }
    }
}
function Invoke-GraphWithRetry
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    <#
    .SYNOPSIS
    Invokes a Graph API with automatic retry logic for throttling
     
    .DESCRIPTION
    Executes a Microsoft Graph API request with built-in retry logic to handle transient HTTP responses.
    The function retries retryable status codes (429 by default), using Retry-After when provided,
    or incremental backoff based on DefaultBackOffSeconds and the retry attempt number.
    If the request returns paged results, it retrieves only a single page - callers should use Get-GraphData for automatic pagination.
     
    Supports Application Insights logging when an AILogger instance is provided when importing the module
     
    .PARAMETER RequestUri
    The complete Microsoft Graph API request URL including query parameters.
    Example: 'https://graph.microsoft.com/v1.0/users?$top=10'
     
    .PARAMETER Method
    The HTTP method to use for the request. Valid values are: Get, Post, Put, Patch, Delete.
    Default is 'Get'.
     
    .PARAMETER Body
    The request body for Post, Put, or Patch requests. Can be a string or object that will be sent with the request.
     
    .PARAMETER ContentType
    The content type for the request body. Default is 'application/json'.
     
    .PARAMETER Headers
    Additional HTTP headers to include in the request. The Authorization header will be automatically added.
     
    .PARAMETER OperationName
    The operation name to use for Application Insights logging. Default is 'Invoke-GraphWithRetry'.
 
    .PARAMETER RetryableErrorCodes
    HTTP status codes that should trigger retries. Default is 429.
 
    .PARAMETER MaxRetries
    Maximum retry threshold used by the retry loop before the error is written. Default is 100.
 
    .PARAMETER DefaultBackOffSeconds
    Fallback delay in seconds used when the response does not include Retry-After.
 
    .INPUTS
    None
    This command does not accept pipeline input.
     
    .OUTPUTS
    System.Object
    Returns the response from the Graph API call.
     
    .EXAMPLE
    Invoke-GraphWithRetry -RequestUri 'https://graph.microsoft.com/v1.0/users'
     
    Retrieves users from Microsoft Graph using the default GET method.
     
    .EXAMPLE
    $body = @{ displayName = 'Test Group' } | ConvertTo-Json
    Invoke-GraphWithRetry -RequestUri 'https://graph.microsoft.com/v1.0/groups' -Method Post -Body $body
     
    Creates a new group in Microsoft Graph.
     
    .EXAMPLE
    Invoke-GraphWithRetry -RequestUri 'https://graph.microsoft.com/v1.0/users/user@domain.com' -Method Delete
     
    Deletes a user from Microsoft Graph.
 
    .EXAMPLE
    Invoke-GraphWithRetry -RequestUri 'https://graph.microsoft.com/v1.0/users/user@domain.com' -Method Delete -WhatIf
 
    Shows what delete request would run without calling Microsoft Graph.
     
    .NOTES
    - Automatically retries status codes listed in RetryableErrorCodes (429 by default)
    - Uses Retry-After for 429 responses when available; otherwise uses incremental backoff
    - Uses the authentication factory configured via Set-GraphAadFactory
    - Uses the Graph scopes configured via Set-GraphScopes
    - Supports Application Insights telemetry when configured
    - Supports -WhatIf and -Confirm via ShouldProcess
    #>

    param
    (
        [Parameter(Mandatory)]
        [Alias('Uri')]
        [string]$RequestUri,
        [Parameter()]
        [ValidateSet('Get', 'Post', 'Put', 'Patch', 'Delete')]
        $method = 'Get',
        [Parameter()]
        $Body,
        [Parameter()]
        $ContentType = 'application/json',
        [parameter()]
        [System.Collections.Hashtable]
        $Headers = @{},
        [Parameter()]
        $OperationName = 'Invoke-GraphWithRetry',
        [Parameter()]
        [int[]]$RetryableErrorCodes = @(429),
        [Parameter()]
        [int]$MaxRetries = 100,
        [Parameter()]
        [int]$DefaultBackOffSeconds = 1
    )

    begin
    {
        $retries = 0
        $graphUri = New-GraphUri -Uri $RequestUri
    }
    process
    {
        if (-not $PSCmdlet.ShouldProcess($graphUri, "$method Microsoft Graph request"))
        {
            return
        }

        do
        {
            $authHeader = Get-GraphAuthorizationHeader
            Write-Verbose "Invoking Graph API: $graphUri with method $method. Attempt #$($retries + 1)"
            $headers['Authorization'] = $authHeader['Authorization']
            $resultCode = 'Ok'
            try {
                $requestStart = [DateTime]::UtcNow

                switch($method)
               {
                    {$_ -in @('Get', 'Delete')} {
                        $result = Invoke-RestMethod -method $method -Uri $graphUri -headers $headers -ErrorAction Stop -Verbose:$VerbosePreference
                        break;
                    }
                    {$_ -in @('Post', 'Patch', 'Put')} {
                        $result = Invoke-RestMethod -method $method -Uri $graphUri -body $body -headers $headers -ContentType $contentType -ErrorAction Stop -Verbose:$VerbosePreference
                        break;
                    }
                }
                if($script:graphConnection.AiLogger)
                {
                    Write-AiDependency -Target 'graph.microsoft.com' -DependencyType 'Graph API' -Name $OperationName -Data $graphUri -Start $requestStart -ResultCode 'Ok' -Success $true -Connection $script:graphConnection.AiLogger
                }
                $result
                break;  #do-while
            }
            catch {
                $err = $_
                if($null -ne $script:graphConnection.AiLogger)
                {
                    Write-AiException -Exception $err.Exception -Connection $script:graphConnection.AiLogger
                }
                if($null -ne $err.exception.Response.StatusCode)
                {
                    $resultCode = $err.exception.Response.StatusCode
                }
                else {
                    $resultCode = 'Unknown'
                }

                if($retries -le $MaxRetries -and ($err.exception.Response.StatusCode -in $RetryableErrorCodes))
                {
                    $retries++
                    switch($err.exception.Response.StatusCode)
                    {
                        429 {
                            $retryAfter = $err.exception.Response.Headers['Retry-After']
                            if($null -eq $retryAfter)
                            {
                                $retryAfter = $DefaultBackOffSeconds * $retries
                            }
                            $waitTime = [int]$retryAfter
                            break;
                        }
                        default {
                            $waitTime = $DefaultBackOffSeconds * $retries
                            break;
                        }
                    }
                    Write-Warning "Retrying because of status code $($err.exception.Response.StatusCode) for $waitTime secs"
                    start-sleep -Seconds $waitTime
                }
                else {
                    Write-Error -ErrorRecord $_
                    break;
                }
            }
            finally
            {
                if($null -ne $script:graphConnection.AiLogger)
                {
                    Write-AiDependency -Target 'graph.microsoft.com' -DependencyType 'Graph API' -Name $OperationName -Data $graphUri -Start $requestStart -ResultCode $resultCode -Success ($resultCode -eq 'Ok') -Connection $script:graphConnection.AiLogger
                }
            }
        } while($true)
    }
}
function New-GraphBatchRequest
{
    <#
    .SYNOPSIS
    Creates a Microsoft Graph batch request item.
 
    .DESCRIPTION
    Builds a normalized request object suitable for Invoke-GraphBatch and Graph /$batch payloads.
 
    .PARAMETER Method
    HTTP method for the subrequest. Allowed values: GET, POST, PUT, PATCH, DELETE.
 
    .PARAMETER Url
    Relative Graph URL for the subrequest.
    Example: '/me' or '/users?$top=5'.
 
    .PARAMETER Id
    Request identifier. This value is required by Graph batch requests.
 
    .PARAMETER Headers
    Optional headers for this subrequest.
    When Body is provided and Content-Type is not set, application/json is used.
 
    .PARAMETER Body
    Optional body for POST, PUT, or PATCH requests.
 
    .PARAMETER DependsOn
    Optional list of request IDs this request depends on.
 
    .INPUTS
    None
    This command does not accept pipeline input.
 
    .OUTPUTS
    System.Management.Automation.PSCustomObject
    Returns a batch request object.
 
    .EXAMPLE
    New-GraphBatchRequest -Method GET -Url '/me' -Id '1'
 
    Creates a batch request item that gets the signed-in user profile.
 
    .EXAMPLE
    New-GraphBatchRequest -Id '2' -Method PATCH -Url '/users/john.doe@contoso.com' -Body @{ jobTitle = 'Principal Engineer' }
 
    Creates a batch request item that updates a user.
 
    .NOTES
    Use this command together with Invoke-GraphBatch to send multiple Graph requests in a single round-trip.
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateSet('GET', 'POST', 'PUT', 'PATCH', 'DELETE')]
        [string]$Method,
        [Parameter(Mandatory)]
        [string]$Url,
        [Parameter(Mandatory)]
        [string]$Id,
        [Parameter()]
        [System.Collections.Hashtable]$Headers,
        [Parameter()]
        [AllowNull()]
        $Body,
        [Parameter()]
        [string[]]$DependsOn
    )

    process
    {
        if ([string]::IsNullOrWhiteSpace($Url))
        {
            throw 'Url cannot be empty.'
        }

        if ($Url.StartsWith('http', [System.StringComparison]::OrdinalIgnoreCase))
        {
            throw 'Url must be a relative Graph path for batch requests (for example /me or /users?$top=5).'
        }

        $request = [ordered]@{
            method = $Method.ToUpperInvariant()
            url = $Url
        }

        if ([string]::IsNullOrWhiteSpace($Id))
        {
            throw 'Id cannot be empty.'
        }
        $request.id = $Id

        $resolvedHeaders = @{}
        if ($null -ne $Headers)
        {
            foreach ($key in $Headers.Keys)
            {
                $resolvedHeaders[$key] = $Headers[$key]
            }
        }

        if ($PSBoundParameters.ContainsKey('Body'))
        {
            $request.body = $Body

            if (-not $resolvedHeaders.ContainsKey('Content-Type'))
            {
                $resolvedHeaders['Content-Type'] = 'application/json'
            }
        }

        if ($resolvedHeaders.Count -gt 0)
        {
            $request.headers = $resolvedHeaders
        }

        if ($null -ne $DependsOn -and $DependsOn.Count -gt 0)
        {
            $request.dependsOn = $DependsOn
        }

        $requestObject = [PSCustomObject]$request
        $requestObject.PSTypeNames.Insert(0, 'GraphApiHelper.GraphBatchRequest')
        $requestObject
    }
}
function New-GraphUri
{
    <#
    .SYNOPSIS
    Builds a Microsoft Graph request URL.
 
    .DESCRIPTION
    Returns a Microsoft Graph request URL using the same query option parameters as Get-GraphData,
    without sending a request.
 
    The command can build either absolute URLs (using the configured BaseUri) or relative paths
    (for example for Graph batch subrequests). Query options are appended to existing query strings
    and $search values are normalized for Graph search syntax.
 
    .PARAMETER Uri
    The base Microsoft Graph request URL or relative path.
    When an absolute URL is provided with -Relative, only PathAndQuery is returned.
 
    .PARAMETER WithSelect
    Optional values for the $select query option.
 
    .PARAMETER WithFilter
    Optional value for the $filter query option.
 
    .PARAMETER WithCount
    Adds $count=true to the request.
 
    .PARAMETER WithExpand
    Optional value for the $expand query option.
 
    .PARAMETER WithSearch
    Optional value for the $search query option.
    If the value does not start with '(' or '"', the value is automatically wrapped in double quotes.
 
    .PARAMETER Top
    Optional value for the $top query option.
 
    .PARAMETER Skip
    Optional value for the $skip query option.
 
    .PARAMETER Relative
    Returns a relative Graph path instead of prepending the configured BaseUri.
    Use this when building batch request URLs.
    If Uri is absolute, the host and scheme are removed.
 
    .INPUTS
    None
    This command does not accept pipeline input.
 
    .OUTPUTS
    System.String
    Returns the fully constructed request URL or relative Graph path.
 
    .EXAMPLE
    New-GraphUri -Uri '/users' -Top 25
 
    Returns an absolute Graph URL for /users with the $top query option.
 
    .EXAMPLE
    New-GraphUri -Uri '/users' -WithSelect 'id,displayName' -WithFilter "accountEnabled eq true"
 
    Returns a URL with $select and $filter query options.
 
    .EXAMPLE
    New-GraphUri -Uri '/users' -WithSearch '"displayName:alex"' -WithCount
 
    Returns a URL with $search and $count query options.
 
    .EXAMPLE
    New-GraphUri -Uri '/users' -WithSearch 'displayName:alex'
 
    Returns a URL where the search value is automatically quoted.
 
    .EXAMPLE
    New-GraphUri -Uri 'https://graph.microsoft.com/v1.0/users' -Relative
 
    Returns the relative path '/v1.0/users'.
 
    .EXAMPLE
    New-GraphUri -Uri '/users?$orderby=displayName' -Top 10
 
    Returns a URL that preserves existing query parameters and appends new ones using '&'.
 
    .EXAMPLE
    New-GraphUri -Uri '/users' -Top 5 -Relative
 
    Returns a relative path intended for batch subrequest URLs.
 
    .NOTES
    If a relative Uri is provided and -Relative is not used, Set-GraphBaseUri must be configured first.
    Values are not URL-encoded by this function. Pass already encoded values when required.
 
    .LINK
    https://learn.microsoft.com/graph/query-parameters
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]$Uri,
        [Parameter()]
        [string[]]$WithSelect,
        [Parameter()]
        [string]$WithFilter,
        [Parameter()]
        [switch]$WithCount,
        [Parameter()]
        [string]$WithExpand,
        [Parameter()]
        [string]$WithSearch,
        [Parameter()]
        [Nullable[int]]$Top,
        [Parameter()]
        [Nullable[int]]$Skip,
        [Parameter()]
        [switch]$Relative
    )

    process
    {
        if ($Uri.StartsWith('http'))
        {
            if ($Relative)
            {
                # Extract relative path from absolute URI
                $parsedUri = [System.Uri]::new($Uri)
                $Uri = $parsedUri.PathAndQuery
                if ([string]::IsNullOrEmpty($Uri))
                {
                    $Uri = '/'
                }
            }
            # else: Uri is already absolute, use as-is
        }
        else
        {
            # Uri is relative
            if (-not $Relative)
            {
                # Prepend BaseUri
                if(-not $script:graphConnection.BaseUri)
                {
                    throw "BaseUri is not set. Please call Set-GraphBaseUri first or provide a full Uri"
                }
                $Uri = "$($script:graphConnection.BaseUri.AbsoluteUri)/$($Uri.TrimStart('/'))"
            }
            # else: Uri is already relative, use as-is
        }

        $queryParams = [System.Collections.Generic.List[string]]::new()
        if($WithSelect.Count -gt 0)
        {
            $queryParams.Add("`$select=$($WithSelect -join ',')")
        }
        if(-not [string]::IsNullOrWhiteSpace($WithFilter))
        {
            $queryParams.Add("`$filter=$($WithFilter.Trim())")
        }
        if($WithCount)
        {
            $queryParams.Add('$count=true')
        }
        if(-not [string]::IsNullOrWhiteSpace($WithExpand))
        {
            $queryParams.Add("`$expand=$($WithExpand.Trim())")
        }
        if(-not [string]::IsNullOrWhiteSpace($WithSearch))
        {
            $clause = $WithSearch.Trim()
            if(-not ($clause.StartsWith('(')) -and -not ($clause.StartsWith('"')))
            {
                $clause = "`"$clause`""
            }
            $queryParams.Add("`$search=$clause")
        }
        if($null -ne $Top)
        {
            $queryParams.Add("`$top=$Top")
        }
        if($null -ne $Skip)
        {
            $queryParams.Add("`$skip=$Skip")
        }

        if($queryParams.Count -gt 0)
        {
            $separator = if($Uri.Contains('?')) { '&' } else { '?' }
            $Uri = $Uri + $separator + ($queryParams -join '&')
        }

        return $Uri
    }
}
function Remove-GraphReference
{
    <#
    .SYNOPSIS
    Removes a reference from a Microsoft Graph object.
 
    .DESCRIPTION
    Removes a reference from a Microsoft Graph group, application, or service principal.
    This is typically used to remove members or owners by deleting the corresponding $ref link.
 
    .PARAMETER ObjectId
    The identifier of the Microsoft Graph object that owns the reference.
 
    .PARAMETER objectType
    The Microsoft Graph object type. Valid values are groups, applications, and servicePrincipals.
 
    .PARAMETER ReferenceType
    The reference collection to update. Valid values are members and owners.
 
    .PARAMETER MemberId
    The identifier of the object being removed from the reference collection.
 
    .PARAMETER PermissiveModify
    Suppresses errors when the reference does not exist.
 
    .INPUTS
    System.String
    Accepts MemberId values from the pipeline.
 
    .OUTPUTS
    None
    This command performs a Graph API call and does not emit output.
 
    .EXAMPLE
    Remove-GraphReference -ObjectId $groupId -MemberId $userId
 
    Removes the specified user from the group members collection.
 
    .EXAMPLE
    Remove-GraphReference -ObjectId $groupId -ReferenceType owners -MemberId $userId -PermissiveModify
 
    Removes the specified user from the group owners collection and ignores the request if the reference is already missing.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $ObjectId,
        [Parameter()]
        [ValidateSet('groups','applications','servicePrincipals')]
        [string]$objectType = 'groups',
        [Parameter()]
        [ValidateSet('members', 'owners')]
        [string]$ReferenceType = 'members',
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$MemberId,
        [switch]$PermissiveModify
    )

    begin
    {
    }
    process
    {
        $uri = New-GraphUri -Uri "/$objectType/$ObjectId/$ReferenceType/$MemberId/`$ref"
        try
        {
            Invoke-GraphWithRetry -Method Delete -Uri $uri -ErrorAction Stop
            Write-Verbose "User with ID $MemberId removed from $ReferenceType of $ObjectId."
        }
        catch
        {
            $ex = $_.Exception
            if($ex.Response.StatusCode -eq 404 -and $PermissiveModify)
            {
                Write-Verbose -Message "User with ID $MemberId is not in $ReferenceType of $ObjectId."
            }
            else
            {
                Write-Error -ErrorRecord $_
            }
        }
    }
}
function Set-GraphAadFactory
{
    <#
    .SYNOPSIS
    Sets the AAD authentication factory for Graph API operations
     
    .DESCRIPTION
    Configures the authentication factory to be used for obtaining access tokens when making Graph API calls.
    The factory name corresponds to a factory registered with the AadAuthenticationFactory module.
    By default, the command validates that the factory exists before updating module state.
     
    .PARAMETER Name
    The name of the authentication factory to use. This should match a factory registered with AadAuthenticationFactory module.
    Common values include 'ManagedIdentityFactory' or custom factory names.
 
    .PARAMETER Force
    Skips validation that the specified factory exists and sets the value directly.
 
    .INPUTS
    None
    This command does not accept pipeline input.
 
    .OUTPUTS
    None
    This command updates module configuration and does not return an object.
     
    .EXAMPLE
    Set-GraphAadFactory -Name 'ManagedIdentityFactory'
     
    Configures the module to use managed identity for authentication.
     
    .EXAMPLE
    Set-GraphAadFactory -Name 'MyCustomFactory'
     
    Configures the module to use a custom authentication factory.
 
    .EXAMPLE
    Set-GraphAadFactory -Name 'FactoryRegisteredLater' -Force
 
    Sets the factory name without validating its current registration.
 
    .NOTES
    - When -Force is not specified, the command throws if the factory cannot be found.
    - The configured value is used by subsequent GraphApiHelper commands that request tokens.
    #>

    param
    (
        [Parameter(Mandatory)]
        [string]$Name,
        [switch]$Force
    )

    process
    {
        if($null -eq (Get-AadAuthenticationFactory -Name $Name) -and -not $Force)
        {
            throw "Authentication factory '$Name' not found. Please register it with the AadAuthenticationFactory module before using."
        }
        $script:graphConnection.FactoryName = $Name
    }
}
function Set-GraphAiLogger
{
    <#
    .SYNOPSIS
    Sets the Application Insights logger for telemetry
     
    .DESCRIPTION
    Configures the Application Insights logger instance to be used for logging telemetry data during Graph API operations.
     
    .PARAMETER Logger
    The AILogger instance to use for logging. This should be created using the ApplicationInsights module.
 
    .INPUTS
    None
    This command does not accept pipeline input.
 
    .OUTPUTS
    None
    This command updates module configuration and does not return an object.
     
    .EXAMPLE
    $aiLogger = New-AiLogger -InstrumentationKey 'your-instrumentation-key'
    Set-GraphAiLogger -Logger $aiLogger
     
    Configures the module to use the specified Application Insights logger for telemetry.
 
    .NOTES
    Invoke-GraphWithRetry uses this logger for dependency and exception telemetry when configured.
    #>

    param
    (
        [Parameter(Mandatory)]
        $Logger
    )

    process
    {
        $script:graphConnection.AiLogger = $Logger
    }
}
function Set-GraphBaseUri
{
    <#
    .SYNOPSIS
    Sets the base URI used for Microsoft Graph API requests.
 
    .DESCRIPTION
    Configures the base URI used to build absolute request URIs when a relative path is supplied
    to commands such as Invoke-GraphWithRetry and Get-GraphData.
 
    .PARAMETER BaseUri
    The base URI to use for Graph requests. Defaults to https://graph.microsoft.com/v1.0 when
    the module is imported.
 
    .INPUTS
    None
    This command does not accept pipeline input.
 
    .OUTPUTS
    None
    This command updates module configuration and does not return an object.
 
    .EXAMPLE
    Set-GraphBaseUri -BaseUri 'https://graph.microsoft.com/v1.0'
 
    Uses the global Microsoft Graph endpoint.
 
    .EXAMPLE
    Set-GraphBaseUri -BaseUri 'https://graph.microsoft.us/v1.0'
 
    Uses the Microsoft Graph US Government endpoint.
 
    .NOTES
    This value is used when commands receive relative Uri values, for example in New-GraphUri and Invoke-GraphWithRetry.
    #>

    param
    (
        [Parameter(Mandatory)]
        [string]$BaseUri
    )

    process
    {
        $uri = New-Object System.Uri($BaseUri.Trim().TrimEnd('/'))
        if($uri.Segments.Length -lt 2)
        {
            throw "Invalid BaseUri. Please provide a valid URI with at least one segment."
        }
        if($uri.Segments[1].TrimEnd('/') -notin @('v1.0', 'beta'))
        {
            throw "BaseUri must include a version segment (e.g. 'v1.0' or 'beta')."
        }
        $script:graphConnection.BaseUri = $uri
    }
}
function Set-GraphScopes
{
    <#
    .SYNOPSIS
    Sets the scopes for Graph API authentication
     
    .DESCRIPTION
    Configures the scope to be used when requesting access tokens for Graph API calls.
    The default scope is 'https://graph.microsoft.com/.default' which uses the permissions assigned to the application in Azure AD.
     
    .PARAMETER Scopes
    The scopes to use when requesting access tokens. The default is 'https://graph.microsoft.com/.default'.
 
    .INPUTS
    None
    This command does not accept pipeline input.
 
    .OUTPUTS
    None
    This command updates module configuration and does not return an object.
     
    .EXAMPLE
    Set-GraphScopes -Scopes 'https://graph.microsoft.com/.default'
     
    Configures the module to use the default Graph API scope for authentication.
     
    .EXAMPLE
    Set-GraphScopes -Scopes 'https://graph.microsoft.com/User.Read'
     
    Configures the module to request a token with only User.Read permissions.
 
    .EXAMPLE
    Set-GraphScopes -Scopes @('https://graph.microsoft.com/User.Read', 'https://graph.microsoft.com/Mail.Read')
 
    Configures multiple delegated scopes for token acquisition.
 
    .NOTES
    The configured scopes are used by Get-GraphAuthorizationHeader when requesting tokens.
    #>

    param
    (
        [Parameter()]
        [string[]]$Scopes = @('https://graph.microsoft.com/.default')
    )

    process
    {
        $script:graphConnection.GraphScope = $Scopes
    }
}
#endregion Public commands
#region Internal commands
<#
.SYNOPSIS
Represents module-level connection settings for Microsoft Graph.
 
.DESCRIPTION
Stores shared configuration used by GraphApiHelper commands, including
the authentication factory name, Graph base URI, scopes, and optional
Application Insights logger instance.
 
.NOTES
This is an internal type used by module commands and is not exported.
#>

class GraphConnection {

    #name of AadAuthenticationFactry factory to use for obtaining tokens
    [string]$FactoryName
    #base URI for Microsoft Graph API calls, typically https://graph.microsoft.com/v1.0 or https://graph.microsoft.us/beta
    [Uri]$BaseUri
    #scopes required for Microsoft Graph API access
    [string[]]$GraphScope
    #optional Application Insights logger instance
    [object]$AiLogger

    GraphConnection()
    {
        #set defaults
        $this.FactoryName = 'graph'
        $this.BaseUri = [Uri]::new('https://graph.microsoft.com/v1.0')
        $this.GraphScope = @('https://graph.microsoft.com/.default')
        $this.AiLogger = $null
    }
    
    GraphConnection([string]$BaseUri, [string[]]$GraphScope, $AiLogger)
    {
        $this.FactoryName = 'graph'
        $this.BaseUri = new-object System.Uri($BaseUri)
        $this.GraphScope = $GraphScope
        $this.AiLogger = $AiLogger
    }

    <#
    .SYNOPSIS
    Builds a directory object reference URI for Microsoft Graph.
 
    .PARAMETER id
    The Azure AD object identifier to convert into a directoryObjects
    reference URI.
 
    .OUTPUTS
    System.String
    The fully-qualified reference URI for the provided object id.
    #>

    [string] GetReference([string]$id)
    {
        $ref = "$($this.BaseUri.Scheme)://$($this.BaseUri.Host)/v1.0/directoryObjects/$id"
        Write-Verbose "Constructed reference URI: $ref"
        return $ref
    }
}
#endregion Internal commands
#region Module initialization
$script:graphConnection = new-object GraphConnection('https://graph.microsoft.com/v1.0', @('https://graph.microsoft.com/.default'), $null)
#endregion Module initialization

# SIG # Begin signature block
# MIIuMwYJKoZIhvcNAQcCoIIuJDCCLiACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBAJP6jOtW2Zvsx
# 8ZxD9tLdf7c8AH4BJfkuw5kNtiql5KCCE2AwggWQMIIDeKADAgECAhAFmxtXno4h
# MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV
# BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z
# ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
# AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z
# G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ
# anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s
# Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL
# 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb
# BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3
# JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c
# AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx
# YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0
# viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL
# T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud
# EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf
# Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk
# aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS
# PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK
# 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB
# cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp
# 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg
# dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri
# RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7
# 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5
# nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3
# i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H
# EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G
# CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C
# 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce
# 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da
# E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T
# SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA
# FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh
# D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM
# 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z
# 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05
# huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY
# mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP
# /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN
# BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry
# sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL
# IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf
# Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh
# OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh
# dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV
# 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j
# wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH
# Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC
# XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l
# /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW
# eE4wggcUMIIE/KADAgECAhAP9xCe9qf4ax3LBs7uih/sMA0GCSqGSIb3DQEBCwUA
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwHhcNMjMxMTA4MDAwMDAwWhcNMjYxMDAxMjM1OTU5WjCBnDET
# MBEGCysGAQQBgjc8AgEDEwJDWjEdMBsGA1UEDwwUUHJpdmF0ZSBPcmdhbml6YXRp
# b24xETAPBgNVBAUTCDA0OTIzNjkzMQswCQYDVQQGEwJDWjEOMAwGA1UEBxMFUHJh
# aGExGjAYBgNVBAoTEUdyZXlDb3JiZWwgcy5yLm8uMRowGAYDVQQDExFHcmV5Q29y
# YmVsIHMuci5vLjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJ8t/Qga
# dJKtGC7EqH4pmIU73fInH+j1scmVnrJtXL8tGlKzWZ7qlWDWOJBR3owF9CVqL4IX
# BGImH8Miowj6RKKqhEe9UtxiH5ipV6msnzAjTFkwqR9vjfEm9vrU1JuXWvAWAfYx
# qYg92oyCEBDQxpURpZmqAVSBy9U/ScDwE4NykZGzb0oYSPtzStd8RJvtUkc4126w
# YKMbVe/kdY1mDbKO9DLfpbSIj3vghrH6XeHwEb7/jAVYI7Vl+jUyyqfmYHD7FldQ
# X2fZfwvoGSibY1uWvvP0/vm0yd6uDbDjCDOTQW8Lxl5wvlXEf5ewn2oaPSoa6ov3
# 1XmnxL5iT8c1LM06JFCwfHS9e0NSyNr86IiKaxQO9/MANrYciTicObtD3cBcSRDO
# pEUfhc4TvA5DQZaakSduVJWPdMhxQs9iWeYMOzh5NDTB3xAx8eLBn7Uj++hjI3FQ
# WGEPw4Ew6WoDsJShU0HemlDJGTPW9EZSWHGdNFr1BxXEPb4F7DbjJZn33QIDAQAB
# o4ICAjCCAf4wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0O
# BBYEFP2yViJvcgO05qXIH6aJSXB/QcEhMD0GA1UdIAQ2MDQwMgYFZ4EMAQMwKTAn
# BggrBgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMA4GA1UdDwEB
# /wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBP
# hk1odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2Rl
# U2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2Ny
# bDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0
# MDk2U0hBMzg0MjAyMUNBMS5jcmwwgZQGCCsGAQUFBwEBBIGHMIGEMCQGCCsGAQUF
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXAYIKwYBBQUHMAKGUGh0dHA6
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWdu
# aW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3J0MAkGA1UdEwQCMAAwDQYJKoZIhvcN
# AQELBQADggIBADCe9Fh40HN9RneCehz5MrBy4O9WYsYCMJ7qJ9DsBT+Hed98UOKB
# k/XjgSLfsj5eZRHRmz3HzhGDK1PaRI+yIUVQx96a4qL7adktmrHex3fW39Iq+tPB
# rHtiEIp9rwunATeZpk+876u0AXYD1VDRWCtkL8zwZU0oqL6U/mWEIXzkryCB5N3x
# xtE54jMmW7MKi1+To4yQcrK3zQ394e2dr50L+aF2fgJ5mo1/YJvzyLLhigbqpoYG
# U/gjZonhNJXUaYogpHSTgUaBRlIKZ5xCnrFfJlOsbkhex4QAcdkU6XC+XyYfEQka
# 7ERwgxmEoRT3NlZ8/EbrQxJP4S1H8Z29M4D3L6rXNXXmv0IbfA9FQcqEco3Y3tRW
# dgdcFEwJmYTo0mCZrYTJHgkKW8xDvQ5BJISAp/ydOX5tSa71ojx1/Kp7qizqjBN/
# W77jdqJ89N1y+N/SOiHOCH9NO5pDLsHpTWW/arvjZT0I8dVYkqK0V39rh95XELI+
# NwBZvV4AsKLirjrkZU3pwCz6O99VmPkBqp9TA5wl13NdTpDHuQ6QyVT7hbC8LF5p
# z6x/xO/+tEGxG+1A31UTJPmkxhhUlR+NE3ZXiXhcG72CFHYUUvqwlThPkFYe4Ygf
# j9ADmss08k0JhVU5rkbrC2h+549HPlFu/XOSIrps4SXzInjHPEYuBETzMYIaKTCC
# GiUCAQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4x
# QTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQw
# OTYgU0hBMzg0IDIwMjEgQ0ExAhAP9xCe9qf4ax3LBs7uih/sMA0GCWCGSAFlAwQC
# AQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwG
# CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZI
# hvcNAQkEMSIEICKnQeMxfu4B6Mkmx3znNSlpCg0s+/PjqXG84xoJzA0gMA0GCSqG
# SIb3DQEBAQUABIIBgFMfkBhVSCGdg+lRuHq0SELHC+2bsp8R2OVXqEcWn5MjlKdj
# prmvNBjWS9g/S6y3fZjifs9C0qCmwy6mucUQY+gIw6+4DnGYpvVWEjkKZA8FFRJM
# RiT/MjyGh3hYuH0rYvU/Qz1w4k4eh6tH7soSJFPRcoFxNLwuR8/emDtzEwug/UYJ
# +IB5X83tFbo6NjS4E1zhzX9DesAHOk/e6RBL+te7CDCX/JAOMYshvXFHL5Cz1ZQs
# C2LvzDE9cAEhmZPpjAXIauE0NTJK0wMTNjEg66ZG3dZqBa1Ruf9vWEPvMEV7Tp+z
# AqkQ7NGxRmL6LyImC4wFC5FLM8Pltb/OBFsqOzUDq2RyMPZKGfrCo6GtBApCgG5d
# s6YLUZfnN+EzhH6i+NZiTUTkbTTyigKVkq/RIOvjTJj/O1sXt3vbz0ZV/QPzm9/B
# BNYcY/Dr61c+LWgC0RgRfyTlhfR78BUVPjmO+jJgJ1ZEdkJ6KX0+dEE/3VWojspB
# 6tz6VSFN393mOM6TCqGCF3YwghdyBgorBgEEAYI3AwMBMYIXYjCCF14GCSqGSIb3
# DQEHAqCCF08wghdLAgEDMQ8wDQYJYIZIAWUDBAIBBQAwdwYLKoZIhvcNAQkQAQSg
# aARmMGQCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCBpW8PvxlqPjomI
# EZBeQ+Oi0LNBRqipZOk56PxfC1NSIAIQDZ9orX9JAuysT3tqlPltehgPMjAyNjA2
# MTMxMDM2MjlaoIITOjCCBu0wggTVoAMCAQICEAqA7xhLjfEFgtHEdqeVdGgwDQYJ
# KoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBS
# U0E0MDk2IFNIQTI1NiAyMDI1IENBMTAeFw0yNTA2MDQwMDAwMDBaFw0zNjA5MDMy
# MzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7
# MDkGA1UEAxMyRGlnaUNlcnQgU0hBMjU2IFJTQTQwOTYgVGltZXN0YW1wIFJlc3Bv
# bmRlciAyMDI1IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDQRqwt
# Esae0OquYFazK1e6b1H/hnAKAd/KN8wZQjBjMqiZ3xTWcfsLwOvRxUwXcGx8AUjn
# i6bz52fGTfr6PHRNv6T7zsf1Y/E3IU8kgNkeECqVQ+3bzWYesFtkepErvUSbf+EI
# YLkrLKd6qJnuzK8Vcn0DvbDMemQFoxQ2Dsw4vEjoT1FpS54dNApZfKY61HAldytx
# NM89PZXUP/5wWWURK+IfxiOg8W9lKMqzdIo7VA1R0V3Zp3DjjANwqAf4lEkTlCDQ
# 0/fKJLKLkzGBTpx6EYevvOi7XOc4zyh1uSqgr6UnbksIcFJqLbkIXIPbcNmA98Os
# kkkrvt6lPAw/p4oDSRZreiwB7x9ykrjS6GS3NR39iTTFS+ENTqW8m6THuOmHHjQN
# C3zbJ6nJ6SXiLSvw4Smz8U07hqF+8CTXaETkVWz0dVVZw7knh1WZXOLHgDvundrA
# tuvz0D3T+dYaNcwafsVCGZKUhQPL1naFKBy1p6llN3QgshRta6Eq4B40h5avMcpi
# 54wm0i2ePZD5pPIssoszQyF4//3DoK2O65Uck5Wggn8O2klETsJ7u8xEehGifgJY
# i+6I03UuT1j7FnrqVrOzaQoVJOeeStPeldYRNMmSF3voIgMFtNGh86w3ISHNm0Ia
# adCKCkUe2LnwJKa8TIlwCUNVwppwn4D3/Pt5pwIDAQABo4IBlTCCAZEwDAYDVR0T
# AQH/BAIwADAdBgNVHQ4EFgQU5Dv88jHt/f3X85FxYxlQQ89hjOgwHwYDVR0jBBgw
# FoAU729TSunkBnx6yuKQVvYv1Ensy04wDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB
# /wQMMAoGCCsGAQUFBwMIMIGVBggrBgEFBQcBAQSBiDCBhTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMF0GCCsGAQUFBzAChlFodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdS
# U0E0MDk2U0hBMjU2MjAyNUNBMS5jcnQwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDov
# L2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0YW1waW5n
# UlNBNDA5NlNIQTI1NjIwMjVDQTEuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsG
# CWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAZSqt8RwnBLmuYEHs0QhEnmNA
# ciH45PYiT9s1i6UKtW+FERp8FgXRGQ/YAavXzWjZhY+hIfP2JkQ38U+wtJPBVBaj
# YfrbIYG+Dui4I4PCvHpQuPqFgqp1PzC/ZRX4pvP/ciZmUnthfAEP1HShTrY+2DE5
# qjzvZs7JIIgt0GCFD9ktx0LxxtRQ7vllKluHWiKk6FxRPyUPxAAYH2Vy1lNM4kze
# kd8oEARzFAWgeW3az2xejEWLNN4eKGxDJ8WDl/FQUSntbjZ80FU3i54tpx5F/0Kr
# 15zW/mJAxZMVBrTE2oi0fcI8VMbtoRAmaaslNXdCG1+lqvP4FbrQ6IwSBXkZagHL
# hFU9HCrG/syTRLLhAezu/3Lr00GrJzPQFnCEH1Y58678IgmfORBPC1JKkYaEt2Od
# Dh4GmO0/5cHelAK2/gTlQJINqDr6JfwyYHXSd+V08X1JUPvB4ILfJdmL+66Gp3CS
# BXG6IwXMZUXBhtCyIaehr0XkBoDIGMUG1dUtwq1qmcwbdUfcSYCn+OwncVUXf53V
# JUNOaMWMts0VlRYxe5nK+At+DI96HAlXHAL5SlfYxJ7La54i71McVWRP66bW+yER
# NpbJCjyCYG2j+bdpxo/1Cy4uPcU3AWVPGrbn5PhDBf3Froguzzhk++ami+r3Qrx5
# bIbY3TVzgiFI7Gq3zWcwgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0G
# CSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTla
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEy
# NTYgMjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHT
# CphBcr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPh
# of6pvF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mA
# xAHeHYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBv
# MgEdgkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps
# 0wjUjsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF
# 83bRVFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXi
# UOeSLsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOM
# CZIVNSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydP
# pOjL6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrU
# G2ZdSoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+
# sdFUeEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s
# BwEwDQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WI
# GjB/T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+
# IQhQE7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8M
# yb9rEVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2
# th9y1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjaj
# V/gxdEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2
# Lr3ty9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFze
# GxcytL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG
# 7uEBYTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+N
# Jpud/v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckT
# etiSuEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszW
# kPZPubdcMIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0B
# AQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk
# IElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw
# ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz
# 7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS
# 5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7
# bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfI
# SKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jH
# trHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14
# Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2
# h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt
# 6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPR
# iQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ER
# ElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4K
# Jpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAd
# BgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SS
# y4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAk
# BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAC
# hjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURS
# b290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRV
# HSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyh
# hyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO
# 0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo
# 8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++h
# UD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5x
# aiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMYIDfDCCA3gC
# AQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/
# BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg
# U0hBMjU2IDIwMjUgQ0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUA
# oIHRMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcN
# MjYwNjEzMTAzNjI5WjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBTdYjCshgotMGva
# OLFoeVIwB/tBfjAvBgkqhkiG9w0BCQQxIgQgrR7L5NAan70yPFMJ6FeAa/QAPLSI
# RnINSm9wE5bM5CkwNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgSqA/oizXXITFXJOP
# go5na5yuyrM/420mmqM08UYRCjMwDQYJKoZIhvcNAQEBBQAEggIAsh0hrf8P9C8L
# ejrEbMiFfAhW6iqapBegY36RwEMv7tm+WeoHCclQLfmuZgiqm9Ev3YDh7O4F5/Ku
# pPsB3kFdRp2/OHz2/Vt/WDDffLvWP2uoSaHFKAN4vM0DJ084Yr+vE66Vk2xxOVbh
# +H0jrbtwc06bUtjSxL7vyUmvkfF4Oet0ehmjFAodpenlrZe/ShTAmeznQn4E4bHx
# sn4mfbbbWXZyMpyo5Y2KV7Lx4f8kbXm8Gb0F5F8CLdJIx03cUD7bHmPiXZMdrkES
# oYqO7ndKP19jS98TxbDq+D15XRUdw5adgAQoxImq2WziSYNVsj8ulVOV8F9J1/W0
# cZ0vkFv6w4imPH0RRqUQFwY5PDE5GfPhNyz/OL+z+/bnehtUYn/qlEuruRDh6Qib
# rN8lVJ9nmJM0n5GFrZeTUn3kgquFLIRcUITwEHBsPiw4N1PsiM/D2FqXR6v2m9WN
# v+zPd7RLoIS+FIxzVrY+ienDU95U8ICR76xrmkAbyK4aMyTmH3L+gx6y8gwDUpDu
# 33oNNjvEKHLAUNzVV3NvWxF/S5Vzsq1O7DOhsIfIdqYYmkczq15IWZ39g6yLW0cm
# oEjph6dTs/jpze5z7YWbXhWpCGAgcw6OpIugafyKdPcKYEt1KupGZFsJjeBkW7Tq
# peCaDbMPDdOs+Aw19Ca2vEQcP9kCEIc=
# SIG # End signature block