TrashCommands.ps1

#requires -Version 5.1

$script:MAX_TIMESTAMP = 4102444800  
$script:STRING_LENGTH_LIMIT = 100
$script:MAX_RECORDS_LIMIT = 990
$script:FIELD_LABEL_WIDTH = 21
$script:STATUS_SUCCESS = "success"

$script:RECORD_VERSION_LEGACY_MIN = 0
$script:RECORD_VERSION_LEGACY_MAX = 2
$script:RECORD_VERSION_V3 = 3
$script:RECORD_VERSION_V4 = 4
$script:RECORD_VERSION_V5 = 5
$script:RECORD_VERSION_V6 = 6

function Get-VaultOrThrow {
    # Internal: Gets vault instance or throws error
    $vault = getVault
    if ($null -eq $vault) {
        throw "Failed to get vault instance"
    }
    return $vault
}

function Invoke-TaskAndWait {
    # Internal: Waits for task completion and properly unwraps exceptions
    param([System.Threading.Tasks.Task]$Task)
    
    $Task.Wait()
    if ($Task.IsFaulted) {
        if ($Task.Exception.InnerExceptions.Count -eq 1) {
            throw $Task.Exception.InnerException
        }
        throw $Task.Exception
    }
}

function Get-RecordMetadata {
    # Internal: Parses record title and type from decrypted data
    param([byte[]]$DataUnencrypted)
    
    if ($null -eq $DataUnencrypted -or $DataUnencrypted.Length -eq 0) {
        return @{ Title = ""; Type = "" }
    }
    
    try {
        $jsonString = [System.Text.Encoding]::UTF8.GetString($DataUnencrypted)
        $json = $jsonString | ConvertFrom-Json
        return @{
            Title = if ($json.title) { $json.title } else { "" }
            Type = if ($json.type) { $json.type } else { "" }
        }
    }
    catch {
        Write-Verbose "Failed to parse record metadata: $($_.Exception.Message)"
        return @{ Title = "Parse Error"; Type = "Unknown" }
    }
}

function New-WildcardRegex {
    # Internal: Creates regex from wildcard pattern with timeout protection
    param(
        [Parameter(Mandatory = $true)]
        [string]$Pattern,
        
        [int]$MaxLength = $script:STRING_LENGTH_LIMIT,
        
        [int]$TimeoutSeconds = 1
    )
    
    if ($Pattern.Length -gt $MaxLength) {
        Write-Warning "Pattern too long, truncated to $MaxLength characters"
        $Pattern = $Pattern.Substring(0, $MaxLength)
    }
    
    try {
        $escaped = [regex]::Escape($Pattern) -replace "\\\*", ".*" -replace "\\\?", "."
        $regexPattern = "^$escaped$"
        return [regex]::new(
            $regexPattern, 
            [System.Text.RegularExpressions.RegexOptions]::IgnoreCase,
            [TimeSpan]::FromSeconds($TimeoutSeconds)
        )
    }
    catch {
        Write-Warning "Invalid pattern '$Pattern': $($_.Exception.Message)"
        return $null
    }
}

function Get-KeeperTrashList {
    <#
    .SYNOPSIS
    Lists deleted records in trash

    .DESCRIPTION
    Lists all deleted records, orphaned records, and shared folders in the trash.
    Shows record details including UID, name, type, deletion date, and status.

    .PARAMETER Pattern
    Filter records by pattern (supports wildcards * and ?)

    .PARAMETER Verbose
    Show detailed information including folder details

    .OUTPUTS
    None. Displays formatted table output to console.

    .EXAMPLE
    Get-KeeperTrashList
    Lists all items in trash

    .EXAMPLE
    Get-KeeperTrashList -Pattern "test*"
    Lists only records matching "test*" pattern

    .EXAMPLE
    Get-KeeperTrashList -Verbose
    Shows detailed information including folder details
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Position = 0)]
        [ValidateLength(0, 100)]
        [string]$Pattern        
    )

    Write-Verbose "Starting trash list operation with pattern: '$Pattern'"
    
    try {
        $vault = Get-VaultOrThrow
        $loadTask = [KeeperSecurity.Vault.TrashManagement]::EnsureDeletedRecordsLoaded($vault)
        Invoke-TaskAndWait -Task $loadTask
    }
    catch {
        Write-Error "Failed to load deleted records: $($_.Exception.Message)"
        return
    }

    $deletedRecords = [KeeperSecurity.Vault.TrashManagement]::GetDeletedRecords()
    $orphanedRecords = [KeeperSecurity.Vault.TrashManagement]::GetOrphanedRecords()
    $sharedFolders = [KeeperSecurity.Vault.TrashManagement]::GetSharedFolders()

    if ([KeeperSecurity.Vault.TrashManagement]::IsTrashEmpty()) {
        Write-Host "Trash is empty"
        return
    }

    $normalizedPattern = if ($Pattern -eq "*") { $null } elseif ($Pattern) { $Pattern.ToLower() } else { $null }
    $titlePattern = if ($normalizedPattern) { New-WildcardRegex -Pattern $normalizedPattern } else { $null }

    $recordResults = @(
        Get-RecordTableData -Records $deletedRecords -IsShared $false -Pattern $normalizedPattern -TitlePattern $titlePattern
        Get-RecordTableData -Records $orphanedRecords -IsShared $true -Pattern $normalizedPattern -TitlePattern $titlePattern
    )
    $folderResults = @(Get-FolderTableData -SharedFolders $sharedFolders -Pattern $normalizedPattern -TitlePattern $titlePattern)
    
    $recordResults = @($recordResults | Sort-Object @{Expression={if ($_.'Deleted At') { $_.'Deleted At' } else { [DateTime]::MinValue }}; Descending=$true}, Name)
    $folderResults = @($folderResults | Sort-Object @{Expression={if ($_.'Deleted At') { $_.'Deleted At' } else { [DateTime]::MinValue }}; Descending=$true}, Name)
    
    $results = @($recordResults) + @($folderResults)

    if ($results.Count -gt 0) {
        $results | Format-Table -AutoSize -Wrap
    }
    else {
        Write-Host "No records found matching the specified criteria"
    }
}

function Get-RecordTableData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Records,
        [Parameter(Mandatory = $true)]
        [bool]$IsShared,
        [string]$Pattern,
        [regex]$TitlePattern
    )

    if ($null -eq $Records -or $null -eq $Records.Values) {
        Write-Warning "No records provided or records collection is empty"
        return @()
    }

    $results = @()

    foreach ($record in $Records.Values) {
        if ($null -eq $record) {
            Write-Warning "Null record found, skipping"
            continue
        }

        $metadata = Get-RecordMetadata -DataUnencrypted $record.DataUnencrypted
        
        if ($null -ne $Pattern -and $null -ne $TitlePattern -and $metadata.Title -notmatch $TitlePattern) {
            continue
        }

        $status = if ($IsShared) { "Share" } else { "Record" }
        $dateDeleted = if (-not $IsShared) { Get-DeletedDate -Timestamp $record.DateDeleted } else { $null }

        $results += [PSCustomObject]@{
            'Folder UID' = ""
            'Record UID' = $record.RecordUid
            'Name' = $metadata.Title
            'Record Type' = $metadata.Type
            'Deleted At' = $dateDeleted
            'Status' = $status
        }
    }

    return $results
}

function Get-FolderTableData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $SharedFolders,
        [string]$Pattern,
        [regex]$TitlePattern
    )

    $results = @()

    if (-not $SharedFolders -or -not $SharedFolders.Folders -or -not $SharedFolders.Records) {
        return $results
    }

    foreach ($folder in $SharedFolders.Folders.Values) {
        $folderName = Get-FolderName -Folder $folder -FolderUid $folder.FolderUidString
        $folderRecords = $SharedFolders.Records.Values | Where-Object { $_.FolderUid -eq $folder.FolderUidString }
        
        $folderMatches = Test-FolderMatchesPattern -FolderName $folderName -FolderRecords $folderRecords -Pattern $Pattern -TitlePattern $TitlePattern
        
        if ($folderMatches) {
            if ($VerbosePreference -eq 'Continue') {
                $results += [PSCustomObject]@{
                    'Folder UID' = $folder.FolderUidString
                    'Record UID' = ""
                    'Name' = $folderName
                    'Record Type' = "Shared Folder"
                    'Deleted At' = $null
                    'Status' = "Folder"
                }
                
                foreach ($record in $folderRecords) {
                    $metadata = Get-RecordMetadata -DataUnencrypted $record.DataUnencrypted
                    $recordMatches = $null -eq $Pattern -or $metadata.Title -match $TitlePattern
                    
                    if ($recordMatches) {
                        $results += [PSCustomObject]@{
                            'Folder UID' = $folder.FolderUidString
                            'Record UID' = $record.RecordUid
                            'Name' = $metadata.Title
                            'Record Type' = $metadata.Type
                            'Deleted At' = Get-DeletedDate -Timestamp $record.DateDeleted
                            'Status' = "Share"
                        }
                    }
                }
            }
            else {
                $recordCount = $folderRecords.Count
                $results += [PSCustomObject]@{
                    'Folder UID' = $folder.FolderUidString
                    'Record UID' = ""
                    'Name' = "$folderName ($recordCount records)"
                    'Record Type' = "Shared Folder"
                    'Deleted At' = $null
                    'Status' = "Folder"
                }
            }
        }
    }

    return $results
}

function Test-FolderMatchesPattern {
    # Internal: Tests if folder name or any record matches the search pattern
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$FolderName,
        
        [Parameter(Mandatory = $false)]
        [AllowNull()]
        $FolderRecords,
        
        [string]$Pattern,
        [regex]$TitlePattern
    )
    
    if ([string]::IsNullOrEmpty($Pattern)) {
        return $true
    }
    
    if ($FolderName -match $TitlePattern) {
        return $true
    }
    
    if ($null -ne $FolderRecords) {
        foreach ($record in $FolderRecords) {
            $metadata = Get-RecordMetadata -DataUnencrypted $record.DataUnencrypted
            if ($metadata.Title -match $TitlePattern) {
                return $true
            }
        }
    }
    
    return $false
}

function Get-FolderName {
    param(
        $Folder,
        [string]$FolderUid
    )
    
    try {
        if ($Folder.DataUnEncrypted -and $Folder.DataUnEncrypted.Length -gt 0) {
            $jsonString = [System.Text.Encoding]::UTF8.GetString($Folder.DataUnEncrypted)
            $jsonObject = $jsonString | ConvertFrom-Json
            if ($jsonObject.name) { 
                return $jsonObject.name 
            } else { 
                return $FolderUid 
            }
        }
        return $FolderUid
    }
    catch [System.Text.DecoderFallbackException] {
        Write-Verbose "Invalid encoding in folder $($FolderUid): $($_.Exception.Message)"
        return $FolderUid
    }
    catch [System.ArgumentException] {
        Write-Verbose "Invalid JSON data in folder $($FolderUid): $($_.Exception.Message)"
        return $FolderUid
    }
    catch {
        Write-Verbose "Unexpected error parsing folder data for $($FolderUid): $($_.Exception.Message)"
        return $FolderUid
    }
}

function ConvertFrom-UnixTimestamp {
    # Internal: Converts Unix timestamp (seconds or milliseconds) to DateTime
    [CmdletBinding()]
    [OutputType([DateTime])]
    param(
        [Parameter(Mandatory = $true)]
        [long]$Timestamp,
        
        [switch]$AsMilliseconds
    )
    
    if ($Timestamp -le 0) {
        return $null
    }
    
    try {
        $isMilliseconds = $AsMilliseconds -or $Timestamp -gt 9999999999
        
        if ($isMilliseconds) {
            $seconds = $Timestamp / 1000
            if ($seconds -lt 0 -or $seconds -gt $script:MAX_TIMESTAMP) {
                return $null
            }
            return [DateTimeOffset]::FromUnixTimeMilliseconds($Timestamp).DateTime
        }
        else {
            if ($Timestamp -lt 0 -or $Timestamp -gt $script:MAX_TIMESTAMP) {
                return $null
            }
            return [DateTimeOffset]::FromUnixTimeSeconds($Timestamp).DateTime
        }
    }
    catch {
        Write-Verbose "Failed to convert timestamp $Timestamp`: $($_.Exception.Message)"
        return $null
    }
}

function Get-DeletedDate {
    param([long]$Timestamp)
    
    if ($Timestamp -le 0) {
        return $null
    }
    
    return ConvertFrom-UnixTimestamp -Timestamp $Timestamp -AsMilliseconds
}

function Restore-KeeperTrashRecords {
    <#
    .SYNOPSIS
    Restores deleted records from trash

    .DESCRIPTION
    Restores deleted records, orphaned records, and shared folders from the trash.
    Supports restoring by record UID or pattern matching.

    .PARAMETER Records
    Array of record UIDs or patterns to restore. Supports wildcards (* and ?).

    .PARAMETER Force
    Skip confirmation prompts and restore immediately.

    .OUTPUTS
    None. Displays status messages to console.

    .EXAMPLE
    Restore-KeeperTrashRecords -Records "NyTgDxKnMRhcgpR_BGkFkw"
    Restores a specific record by UID

    .EXAMPLE
    Restore-KeeperTrashRecords -Records "test*", "MyRecord"
    Restores records matching "test*" pattern and a specific record

    .EXAMPLE
    Restore-KeeperTrashRecords -Records "test*" -Force
    Restores records matching "test*" pattern without confirmation
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateCount(1, 10000)]
        [ValidateLength(1, 100)]
        [string[]]$Records,
        
        [Parameter()]
        [switch]$Force
    )

    Write-Verbose "Starting trash restore operation with $($Records.Count) record(s)"
    
    $validationResult = Test-RecordParameters -Records $Records
    if (-not $validationResult.IsValid) {
        Write-Error $validationResult.ErrorMessage
        return
    }

    if ($validationResult.ValidRecords.Count -eq 0) {
        Write-Host "No valid records specified for restoration"
        return
    }

    try {
        $vault = Get-VaultOrThrow
        $restoreTask = [KeeperSecurity.Vault.TrashManagement]::RestoreTrashRecords($vault, $validationResult.ValidRecords)
        Invoke-TaskAndWait -Task $restoreTask
        Write-Host "Successfully initiated restoration of $($validationResult.ValidRecords.Count) record(s)"
        Write-Host "Use 'Get-KeeperTrashList' to verify the restoration"
    }
    catch {
        Write-Error "Failed to restore records: $($_.Exception.Message)"
    }
}

function Test-RecordParameters {
    <#
    .SYNOPSIS
    Validates record parameters for trash operations
    
    .PARAMETER Records
    Array of record identifiers to validate
    
    .OUTPUTS
    PSCustomObject with IsValid, ErrorMessage, and ValidRecords properties
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$Records
    )
    
    $validatedRecords = @()
    $errors = @()
    
    if ($Records.Count -gt $script:MAX_RECORDS_LIMIT) {
        return [PSCustomObject]@{
            IsValid = $false
            ErrorMessage = "Too many records specified (max: $script:MAX_RECORDS_LIMIT)"
            ValidRecords = @()
        }
    }
    
    for ($i = 0; $i -lt $Records.Count; $i++) {
        $record = $Records[$i]
        
        if ([string]::IsNullOrWhiteSpace($record)) {
            $errors += "Record $($record) at index $($i + 1) must not be empty or whitespace"
            continue
        }
        
        if ($record.Length -gt $script:STRING_LENGTH_LIMIT) {
            $errors += "Record $($record) at index $($i + 1) exceeds maximum length ($script:STRING_LENGTH_LIMIT characters)"
            continue
        }
        
        $validatedRecords += $record.Trim()
    }
    
    return [PSCustomObject]@{
        IsValid = $validatedRecords.Count -gt 0
        ErrorMessage = if ($errors.Count -gt 0) { $errors -join "; " } else { $null }
        ValidRecords = $validatedRecords
    }
}
function Remove-TrashedKeeperRecordShares {
    <#
    .SYNOPSIS
    Removes shares from deleted records in trash

    .DESCRIPTION
    Removes all non-owner shares from orphaned records in the trash.
    This is useful for cleaning up shared records before permanently deleting them.

    .PARAMETER Records
    Array of record UIDs or patterns to unshare. Supports wildcards (* and ?).
    Use "*" to process all orphaned records.

    .PARAMETER Force
    Skip confirmation prompts and remove shares immediately.

    .OUTPUTS
    None. Displays status messages to console.

    .EXAMPLE
    Remove-TrashedKeeperRecordShares -Records "NyTgDxKnMRhcgpR_BGkFkw"
    Removes shares from a specific orphaned record by UID

    .EXAMPLE
    Remove-TrashedKeeperRecordShares -Records "test*", "MyRecord"
    Removes shares from records matching "test*" pattern and a specific record

    .EXAMPLE
    Remove-TrashedKeeperRecordShares -Records "*" -Force
    Removes shares from all orphaned records without confirmation
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateCount(1, 10000)]
        [ValidateLength(1, 100)]
        [string[]]$Records,
        
        [Parameter()]
        [switch]$Force
    )

    Write-Verbose "Starting trash unshare operation with $($Records.Count) record(s)"
    
    $validationResult = Test-RecordParameters -Records $Records
    if (-not $validationResult.IsValid) {
        Write-Error $validationResult.ErrorMessage
        return
    }

    if ($validationResult.ValidRecords.Count -eq 0) {
        Write-Host "No valid records specified"
        return
    }

    try {
        $vault = Get-VaultOrThrow
        $loadTask = [KeeperSecurity.Vault.TrashManagement]::EnsureDeletedRecordsLoaded($vault)
        Invoke-TaskAndWait -Task $loadTask
    }
    catch {
        Write-Error "Failed to load deleted records: $($_.Exception.Message)"
        return
    }

    $orphanedRecords = [KeeperSecurity.Vault.TrashManagement]::GetOrphanedRecords()
    
    if ($null -eq $orphanedRecords -or $orphanedRecords.Count -eq 0) {
        Write-Host "Trash is empty"
        return
    }

    $recordsToUnshare = Find-RecordsToUnshare -RecordPatterns $validationResult.ValidRecords -OrphanedRecords $orphanedRecords
    
    if ($recordsToUnshare.Count -eq 0) {
        Write-Host "There are no records to unshare"
        return
    }

    if (-not (Confirm-UnshareOperation -Force:$Force -RecordCount $recordsToUnshare.Count)) {
        Write-Host "Operation cancelled by user"
        return
    }

    Remove-SharesFromRecords -RecordsToUnshare $recordsToUnshare
}
function Find-RecordsToUnshare {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$RecordPatterns,
        
        [Parameter(Mandatory = $true)]
        $OrphanedRecords
    )

    $recordsToUnshare = New-Object System.Collections.Generic.HashSet[string]

    foreach ($pattern in $RecordPatterns) {
        if ($OrphanedRecords.ContainsKey($pattern)) {
            [void]$recordsToUnshare.Add($pattern)
        }
        else {
            Add-MatchingRecords -Pattern $pattern -OrphanedRecords $OrphanedRecords -RecordsToUnshare $recordsToUnshare
        }
    }

    return @($recordsToUnshare)
}
function Add-MatchingRecords {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Pattern,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $OrphanedRecords,
        
        [Parameter(Mandatory = $false)]
        [AllowEmptyCollection()]
        [System.Collections.Generic.HashSet[string]]$RecordsToUnshare
    )

    if ($null -eq $RecordsToUnshare) {
        Write-Warning "RecordsToUnshare collection is null"
        return
    }

    $titlePattern = New-WildcardRegex -Pattern $Pattern
    if ($null -eq $titlePattern) {
        return
    }

    foreach ($kvp in $OrphanedRecords.GetEnumerator()) {
        $recordUid = $kvp.Key
        $record = $kvp.Value

        if ($RecordsToUnshare.Contains($recordUid)) {
            continue
        }

        $metadata = Get-RecordMetadata -DataUnencrypted $record.DataUnencrypted
        if (-not [string]::IsNullOrEmpty($metadata.Title) -and $titlePattern.IsMatch($metadata.Title)) {
            [void]$RecordsToUnshare.Add($recordUid)
        }
    }
}

function Confirm-UnshareOperation {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [switch]$Force,
        
        [Parameter(Mandatory = $true)]
        [int]$RecordCount
    )

    if ($Force) {
        return $true
    }

    $confirmation = Read-Host "Do you want to remove shares from $RecordCount record(s)? (yes/No)"
    return ($confirmation -match '^(y|yes)$')
}

function Remove-SharesFromRecords {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$RecordsToUnshare
    )

    try {
        $vault = Get-VaultOrThrow
        $recordSharesTask = $vault.GetSharesForRecords($RecordsToUnshare)
        Invoke-TaskAndWait -Task $recordSharesTask
        
        $recordShares = $recordSharesTask.Result
        if ($null -eq $recordShares) {
            Write-Verbose "No shares found for the specified records"
            return
        }

        $removeShareRequests = Build-RemoveShareRequests -RecordShares $recordShares
        if ($removeShareRequests.Count -eq 0) {
            Write-Verbose "No share removal requests to process"
            return
        }

        Invoke-ShareRemovalRequests -RemoveRequests $removeShareRequests
    }
    catch {
        Write-Error "Error getting record shares: $($_.Exception.Message)"
    }
}

function Build-RemoveShareRequests {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        $RecordShares
    )

    $removeRequests = New-Object System.Collections.Generic.List[Records.SharedRecord]

    foreach ($recordShare in $recordShares) {
        if (-not $recordShare.UserPermissions) {
            continue
        }

        foreach ($userPermission in $recordShare.UserPermissions) {
            if (-not $userPermission.Owner) {
                $shareRequest = New-Object Records.SharedRecord
                $shareRequest.ToUsername = $userPermission.Username
                $shareRequest.RecordUid = [Google.Protobuf.ByteString]::CopyFrom(
                    [KeeperSecurity.Utils.CryptoUtils]::Base64UrlDecode($recordShare.RecordUid)
                )
                $removeRequests.Add($shareRequest)
            }
        }
    }

    return $removeRequests
}

function Invoke-ShareRemovalRequests {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.Generic.List[Records.SharedRecord]]$RemoveRequests
    )

    $chunkSize = 100
    $processedCount = 0
    
    for ($i = 0; $i -lt $RemoveRequests.Count; $i += $chunkSize) {
        $remainingCount = $RemoveRequests.Count - $i
        $currentChunkSize = [Math]::Min($chunkSize, $remainingCount)
        $chunk = $RemoveRequests.GetRange($i, $currentChunkSize)
        
        Invoke-ShareRemovalChunk -Chunk $chunk
        $processedCount += $chunk.Count
        Write-Verbose "Processed $processedCount of $($RemoveRequests.Count) share removal requests"
    }
    
    Write-Host "Successfully removed shares from $processedCount record(s)"
}

function Invoke-ShareRemovalChunk {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        $Chunk
    )

    try {
        $vault = Get-VaultOrThrow
        $updateRequest = New-Object Records.RecordShareUpdateRequest
        $updateRequest.RemoveSharedRecord.AddRange($Chunk)

        $responseTask = $vault.Auth.ExecuteAuthRest(
            "vault/records_share_update", 
            $updateRequest, 
            [Records.RecordShareUpdateResponse]
        )
        $response = $responseTask.GetAwaiter().GetResult() -as [Records.RecordShareUpdateResponse]
        
        Write-ShareRemovalErrors -Response $response
    }
    catch {
        Write-Error "Error removing shares: $($_.Exception.Message)"
        throw
    }
}

function Write-ShareRemovalErrors {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        $Response
    )

    foreach ($status in $Response.RemoveSharedRecordStatus) {
        if ($status.Status -ne $script:STATUS_SUCCESS) {
            $recordUid = [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($status.RecordUid.ToByteArray())
            Write-Warning "Remove share '$($status.Username)' from record UID '$recordUid' error: $($status.Message)"
        }
    }
}

function Get-KeeperTrashedRecordDetails {
    <#
    .SYNOPSIS
    Gets detailed information about a deleted record in trash

    .DESCRIPTION
    Displays detailed information about a specific deleted record including all fields,
    custom fields, and share information if the record is shared.

    .PARAMETER RecordUid
    The unique identifier (UID) of the deleted record to retrieve

    .OUTPUTS
    None. Displays formatted record details to console.

    .EXAMPLE
    Get-KeeperTrashedRecordDetails -RecordUid "QGMaKCr9ksOOkhIMSvIWtg"
    Displays detailed information about the specified deleted record

    .EXAMPLE
    Get-KeeperTrashedRecordDetails "hlPKPNt9rsIqC_mCwwfP5A"
    Displays details using positional parameter
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [ValidateLength(16, 64)]
        [ValidatePattern('^[A-Za-z0-9_-]+$')]
        [string]$RecordUid
    )

    Write-Verbose "Retrieving details for record: $RecordUid"
    
    [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    if ($null -eq $vault) {
        Write-Error "Failed to get vault instance"
        return
    }

    try {
        $loadTask = [KeeperSecurity.Vault.TrashManagement]::EnsureDeletedRecordsLoaded($vault)
        Invoke-TaskAndWait -Task $loadTask
    }
    catch {
        Write-Error "Failed to load deleted records: $($_.Exception.Message)"
        return
    }

    $deletedRecords = [KeeperSecurity.Vault.TrashManagement]::GetDeletedRecords()
    $orphanedRecords = [KeeperSecurity.Vault.TrashManagement]::GetOrphanedRecords()

    $record = $null
    $isShared = $false

    if ($deletedRecords.ContainsKey($RecordUid)) {
        $record = $deletedRecords[$RecordUid]
        $isShared = $false
    }
    elseif ($orphanedRecords.ContainsKey($RecordUid)) {
        $record = $orphanedRecords[$RecordUid]
        $isShared = $true
    }
    else {
        Write-Error "$RecordUid is not a valid deleted record UID"
        return
    }

    if ($null -eq $record.RecordKeyUnencrypted) {
        Write-Error "Cannot retrieve record $RecordUid`: no decryption key available"
        return
    }

    try {
        $recordData = ConvertTo-ParsedRecord -DeletedRecord $record
        if (-not $recordData) {
            Write-Error "Cannot parse record $RecordUid"
            return
        }

        Show-RecordDetails -RecordData $recordData

        if ($isShared) {
            Show-RecordShares -Vault $vault -RecordUid $RecordUid
        }
    }
    catch {
        Write-Error "Error displaying record details: $($_.Exception.Message)"
        Write-Verbose $_.Exception.ToString()
    }
}

function Get-DecryptedRecordData {
    # Internal: Decrypts record data using appropriate AES version
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        $DeletedRecord
    )
    
    if ($null -ne $DeletedRecord.DataUnencrypted) {
        return $DeletedRecord.DataUnencrypted
    }
    
    $encryptedData = [KeeperSecurity.Utils.CryptoUtils]::Base64UrlDecode($DeletedRecord.Data)
    
    if ($DeletedRecord.Version -ge $script:RECORD_VERSION_V3) {
        return [KeeperSecurity.Utils.CryptoUtils]::DecryptAesV2($encryptedData, $DeletedRecord.RecordKeyUnencrypted)
    }
    else {
        return [KeeperSecurity.Utils.CryptoUtils]::DecryptAesV1($encryptedData, $DeletedRecord.RecordKeyUnencrypted)
    }
}

function ConvertTo-LegacyPasswordRecord {
    # Internal: Converts decrypted data (v0-2) to PasswordRecord
    [CmdletBinding()]
    [OutputType([KeeperSecurity.Vault.PasswordRecord])]
    param(
        [Parameter(Mandatory = $true)]
        [byte[]]$DecryptedData,
        
        [Parameter(Mandatory = $true)]
        $DeletedRecord
    )
    
    $recordData = [KeeperSecurity.Utils.JsonUtils]::ParseJson([KeeperSecurity.Commands.RecordData], $DecryptedData)
    
    $passwordRecord = New-Object KeeperSecurity.Vault.PasswordRecord
    $passwordRecord.Uid = $DeletedRecord.RecordUid
    $passwordRecord.Version = $DeletedRecord.Version
    $passwordRecord.Title = $recordData.Title
    $passwordRecord.Login = $recordData.Secret1
    $passwordRecord.Password = $recordData.Secret2
    $passwordRecord.Link = $recordData.Link
    $passwordRecord.Notes = $recordData.Notes
    
    if ($recordData.Custom) {
        foreach ($cr in $recordData.Custom) {
            if ($cr) {
                $customField = New-Object KeeperSecurity.Vault.CustomField
                $customField.Name = $cr.Name
                $customField.Value = $cr.Value
                $customField.Type = $cr.Type
                $passwordRecord.Custom.Add($customField)
            }
        }
    }
    
    return $passwordRecord
}

function ConvertTo-ModernTypedRecord {
    # Internal: Converts decrypted JSON data (v3-6) to TypedRecord
    [CmdletBinding()]
    [OutputType([KeeperSecurity.Vault.TypedRecord])]
    param(
        [Parameter(Mandatory = $true)]
        [byte[]]$DecryptedData,
        
        [Parameter(Mandatory = $true)]
        $DeletedRecord
    )
    
    $jsonString = [System.Text.Encoding]::UTF8.GetString($DecryptedData)
    $jsonData = $jsonString | ConvertFrom-Json
    
    $recordType = if ($jsonData.type) { $jsonData.type } else { "login" }
    $typedRecord = New-Object KeeperSecurity.Vault.TypedRecord($recordType)
    $typedRecord.Uid = $DeletedRecord.RecordUid
    $typedRecord.Version = $DeletedRecord.Version
    $typedRecord.Title = if ($jsonData.title) { $jsonData.title } else { "" }
    $typedRecord.Notes = if ($jsonData.notes) { $jsonData.notes } else { "" }
    
    if ($jsonData.fields) {
        foreach ($fieldData in $jsonData.fields) {
            $field = ConvertTo-TypedField -FieldData $fieldData
            if ($field) {
                $typedRecord.Fields.Add($field)
            }
        }
    }
    
    if ($jsonData.custom) {
        foreach ($fieldData in $jsonData.custom) {
            $field = ConvertTo-TypedField -FieldData $fieldData
            if ($field) {
                $typedRecord.Custom.Add($field)
            }
        }
    }
    
    return $typedRecord
}

function ConvertTo-ParsedRecord {
    # Internal: Decrypts and parses DeletedRecord to TypedRecord (v3+) or PasswordRecord (v0-2)
    [CmdletBinding()]
    [OutputType([KeeperSecurity.Vault.TypedRecord], [KeeperSecurity.Vault.PasswordRecord])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $DeletedRecord
    )

    try {
        Write-Verbose "Parsing record $($DeletedRecord.RecordUid) (Version $($DeletedRecord.Version))"
        
        $decryptedData = Get-DecryptedRecordData -DeletedRecord $DeletedRecord
        
        if ($null -eq $decryptedData) {
            Write-Error "Failed to decrypt record $($DeletedRecord.RecordUid). The record key may be invalid."
            return $null
        }

        if ($DeletedRecord.Version -ge $script:RECORD_VERSION_LEGACY_MIN -and $DeletedRecord.Version -le $script:RECORD_VERSION_LEGACY_MAX) {
            return ConvertTo-LegacyPasswordRecord -DecryptedData $decryptedData -DeletedRecord $DeletedRecord
        }
        elseif ($DeletedRecord.Version -in $script:RECORD_VERSION_V3, $script:RECORD_VERSION_V4, $script:RECORD_VERSION_V5, $script:RECORD_VERSION_V6) {
            return ConvertTo-ModernTypedRecord -DecryptedData $decryptedData -DeletedRecord $DeletedRecord
        }
        else {
            Write-Error "Unsupported record version $($DeletedRecord.Version) for record $($DeletedRecord.RecordUid)"
            return $null
        }
    }
    catch {
        Write-Error "Error parsing record $($DeletedRecord.RecordUid) (Version $($DeletedRecord.Version)): $($_.Exception.Message)"
        Write-Verbose $_.Exception.ToString()
        return $null
    }
}

function ConvertTo-TypedField {
    # Internal: Converts JSON field data to TypedField with proper value formatting
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $FieldData
    )

    try {
        $fieldType = if ($FieldData.type) { $FieldData.type } else { "text" }
        $label = if ($FieldData.label) { $FieldData.label } else { $null }
        
        Write-Verbose "Converting field type='$fieldType' label='$label'"
        
        $field = New-Object "KeeperSecurity.Vault.TypedField[object]"($fieldType, $label)
        
        if ($null -ne $FieldData.value) {
            if ($FieldData.value -is [array]) {
                foreach ($val in $FieldData.value) {
                    if ($null -ne $val) {
                        $convertedValue = ConvertTo-FieldValue -Value $val -FieldType $fieldType
                        $field.Values.Add($convertedValue)
                    }
                }
            }
            else {
                $convertedValue = ConvertTo-FieldValue -Value $FieldData.value -FieldType $fieldType
                $field.Values.Add($convertedValue)
            }
        }
        
        return $field
    }
    catch {
        Write-Verbose "Error converting field '$fieldType': $($_.Exception.Message)"
        Write-Verbose $_.Exception.ToString()
        return $null
    }
}

function ConvertTo-NameFieldValue {
    # Internal: Formats name field with first, middle, last components
    param([PSCustomObject]$Value)
    
    $parts = @()
    if ($Value.first) { $parts += $Value.first }
    if ($Value.middle) { $parts += $Value.middle }
    if ($Value.last) { $parts += $Value.last }
    
    if ($parts.Count -gt 0) {
        return ($parts -join ' ')
    }
    return $null
}

function ConvertTo-AddressFieldValue {
    # Internal: Formats address field with street, city, state, zip, country components
    param([PSCustomObject]$Value)
    
    $parts = @()
    if ($Value.street1) { $parts += $Value.street1 }
    if ($Value.street2) { $parts += $Value.street2 }
    if ($Value.city) { $parts += $Value.city }
    if ($Value.state) { $parts += $Value.state }
    if ($Value.zip) { $parts += $Value.zip }
    if ($Value.country) { $parts += $Value.country }
    
    if ($parts.Count -gt 0) {
        return ($parts -join ', ')
    }
    return $null
}

function ConvertTo-PhoneFieldValue {
    # Internal: Formats phone field with number, extension, and type
    param([PSCustomObject]$Value)
    
    if ($Value.number) {
        $result = $Value.number
        if ($Value.ext) {
            $result += " ext. $($Value.ext)"
        }
        if ($Value.type) {
            $result += " ($($Value.type))"
        }
        return $result
    }
    return $null
}

function ConvertTo-DateFieldValue {
    # Internal: Formats date field from Unix timestamp
    param($Value)
    
    if ($Value -is [long] -or ($Value -match '^\d+$')) {
        try {
            $timestamp = [long]$Value
            $dateTime = ConvertFrom-UnixTimestamp -Timestamp $timestamp
            if ($null -ne $dateTime) {
                return $dateTime.ToString('yyyy-MM-dd')
            }
        }
        catch {
            Write-Warning "Failed to convert date value '$Value': $($_.Exception.Message)"
        }
    }
    return $Value
}

function ConvertTo-FieldValue {
    # Internal: Formats field values (dates, names, addresses, phones) to readable strings
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        $Value,
        
        [Parameter(Mandatory = $true)]
        [string]$FieldType
    )

    if ($Value -is [PSCustomObject]) {
        $formatted = switch ($FieldType) {
            'name'    { ConvertTo-NameFieldValue -Value $Value }
            'address' { ConvertTo-AddressFieldValue -Value $Value }
            'phone'   { ConvertTo-PhoneFieldValue -Value $Value }
            default   { $null }
        }
        
        if ($null -ne $formatted) {
            return $formatted
        }
        
        return ($Value | ConvertTo-Json -Compress -Depth 10)
    }
    
    if ($FieldType -eq 'date') {
        return ConvertTo-DateFieldValue -Value $Value
    }
    
    return $Value
}

function Show-RecordDetails {
    # Internal: Formats and displays TypedRecord or PasswordRecord fields
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $RecordData
    )

    try {
        if ($RecordData -is [KeeperSecurity.Vault.TypedRecord]) {
            Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1}" -f "Title", $RecordData.Title)
            Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1}" -f "Type", $RecordData.TypeName)

            if ($RecordData.Fields.Count -gt 0) {
                foreach ($field in $RecordData.Fields) {
                    Show-RecordField -Field $field
                }
            }

            if ($RecordData.Custom.Count -gt 0) {
                foreach ($field in $RecordData.Custom) {
                    Show-RecordField -Field $field
                }
            }
        }
        elseif ($RecordData -is [KeeperSecurity.Vault.PasswordRecord]) {
            Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1}" -f "Title", $RecordData.Title)
            Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1}" -f "Type", [KeeperSecurity.Vault.VaultExtensions]::KeeperRecordType($RecordData))

            if (-not [string]::IsNullOrEmpty($RecordData.Login)) {
                Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1}" -f "Login", $RecordData.Login)
            }
            if (-not [string]::IsNullOrEmpty($RecordData.Password)) {
                Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1}" -f "Password", $RecordData.Password)
            }
            if (-not [string]::IsNullOrEmpty($RecordData.Link)) {
                Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1}" -f "URL", $RecordData.Link)
            }
            if (-not [string]::IsNullOrEmpty($RecordData.Notes)) {
                Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1}" -f "Notes", $RecordData.Notes)
            }
            if ($RecordData.Custom.Count -gt 0) {
                foreach ($custom in $RecordData.Custom) {
                    if (-not [string]::IsNullOrEmpty($custom.Value)) {
                        $name = if (-not [string]::IsNullOrEmpty($custom.Name)) { $custom.Name } else { "Custom" }
                        Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1}" -f $name, $custom.Value)
                    }
                }
            }
        }
    }
    catch {
        Write-Error "Error displaying record details: $($_.Exception.Message)"
        Write-Verbose $_.Exception.ToString()
    }
}

function Show-RecordField {
    # Internal: Displays field name and value(s), handling multi-value fields
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Field
    )

    try {
        $fieldName = if (-not [string]::IsNullOrEmpty($Field.FieldLabel)) { 
            $Field.FieldLabel 
        } 
        elseif (-not [string]::IsNullOrEmpty($Field.FieldName)) { 
            $Field.FieldName 
        }
        else {
            try {
                $name = [KeeperSecurity.Utils.RecordTypesUtils]::GetTypedFieldName($Field)
                if (-not [string]::IsNullOrEmpty($name)) {
                    $name
                }
                else {
                    Write-Verbose "Could not determine field name for field type $($Field.FieldName)"
                    "Unknown"
                }
            }
            catch {
                Write-Verbose "Could not get field name: $($_.Exception.Message)"
                "Unknown"
            }
        }

        $valueArray = @()
        
        if ($Field.Values.Count -gt 0) {
            $valueArray = @($Field.Values)
        }
        else {
            try {
                $values = [KeeperSecurity.Utils.RecordTypesUtils]::GetTypedFieldValues($Field)
                if ($null -ne $values) {
                    $valueArray = @($values)
                }
            }
            catch {
                Write-Verbose "Could not get typed field values for '$fieldName': $($_.Exception.Message)"
            }
        }

        for ($i = 0; $i -lt [Math]::Max($valueArray.Count, 1); $i++) {
            $value = if ($i -lt $valueArray.Count) { $valueArray[$i] } else { "" }
            if (-not [string]::IsNullOrEmpty($value)) {
                if ($i -eq 0) {
                    Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1}" -f $fieldName, $value)
                }
                else {
                    Write-Host ("{0,$script:FIELD_LABEL_WIDTH} {1}" -f "", $value)
                }
            }
        }
    }
    catch {
        Write-Verbose "Error displaying field: $($_.Exception.Message)"
    }
}

function Show-RecordShares {
    # Internal: Retrieves and displays user permissions for a shared record
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [KeeperSecurity.Vault.VaultOnline]$Vault,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$RecordUid
    )

    try {
        Write-Verbose "Retrieving share information for record $RecordUid"
        
        $sharesTask = $Vault.GetSharesForRecords(@($RecordUid))
        Invoke-TaskAndWait -Task $sharesTask
        
        $shares = $sharesTask.Result
        $recordShares = $shares | Select-Object -First 1

        if ($null -ne $recordShares -and $null -ne $recordShares.UserPermissions -and $recordShares.UserPermissions.Length -gt 0) {
            $sortedPermissions = $recordShares.UserPermissions | 
                Sort-Object @{Expression={if ($_.Owner) { " 1" } elseif ($_.CanEdit) { " 2" } elseif ($_.CanShare) { " 3" } else { "" }}}, Username

            $isFirst = $true
            foreach ($permission in $sortedPermissions) {
                if ($permission.Owner) {
                    continue
                }

                $flags = [System.Collections.Generic.List[string]]::new()
                if ($permission.CanEdit) {
                    $flags.Add("Can Edit")
                }
                if ($permission.CanShare) {
                    $flags.Add($(if ($flags.Count -gt 0) { "& Can Share" } else { "Can Share" }))
                }
                $flagsText = if ($flags.Count -gt 0) { $flags -join " " } else { "Read Only" }

                $selfFlag = if ($null -ne $Vault.Auth -and $permission.Username -eq $Vault.Auth.Username) { "self" } else { "" }
                $header = if ($isFirst) { "Direct User Shares" } else { "" }

                Write-Host ("{0,$script:FIELD_LABEL_WIDTH}: {1,-26} ({2}) {3}" -f $header, $permission.Username, $flagsText, $selfFlag)
                $isFirst = $false
            }
        }
    }
    catch {
        Write-Verbose "Error loading share information for record $RecordUid`: $($_.Exception.Message)"
    }
}

function Clear-KeeperTrash {
    <#
    .SYNOPSIS
    Permanently deletes all records in trash
    
    .DESCRIPTION
    Permanently deletes all records in the trash. This action cannot be undone.
    
    .PARAMETER Force
    Skip confirmation prompts and purge immediately.
    
    .OUTPUTS
    None. Displays status messages to console.
    
    .EXAMPLE
    Clear-KeeperTrash
    Purges all trash records with confirmation prompt
    
    .EXAMPLE
    Clear-KeeperTrash -Force
    Purges all trash records without confirmation
    #>

    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter()]
        [switch]$Force
    )
    
    if (-not $Force) {
        $confirmation = Read-Host "Are you sure you want to permanently delete all records in trash? This action cannot be undone. (yes/No)"
        if ($confirmation -notmatch '^(y|yes)$') {
            Write-Host "Purge operation cancelled"
            return
        }
    }
    
    try {
        $vault = Get-VaultOrThrow
        $request = New-Object KeeperSecurity.Commands.PurgeDeletedRecordsCommand
        $task = [KeeperSecurity.Authentication.AuthExtensions]::ExecuteAuthCommand($vault.Auth, $request)
        Invoke-TaskAndWait -Task $task
        Write-Host "Successfully purged all records from trash"
    }
    catch {
        Write-Error "Failed to purge trash: $($_.Exception.Message)"
    }
}

Set-Alias -Name ktrash -Value Get-KeeperTrashList
Set-Alias -Name ktrash-restore -Value Restore-KeeperTrashRecords
Set-Alias -Name ktrash-unshare -Value Remove-TrashedKeeperRecordShares
Set-Alias -Name ktrash-get -Value Get-KeeperTrashedRecordDetails
Set-Alias -Name ktrash-purge -Value Clear-KeeperTrash

# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCNd9rakskC3fq/
# TeiDwN0ROvywtxLuKOXAG90tyvC7t6CCITswggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg
# MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit
# eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS
# 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM
# swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC
# Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3
# /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j
# q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5
# OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo
# 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU
# tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm
# KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP
# TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq
# hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK
# r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda
# qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+
# lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a
# brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS
# y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK
# iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb
# KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q
# xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm
# zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn
# HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w
# gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1
# c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo
# dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi
# 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg
# xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF
# cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ
# m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS
# GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1
# ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9
# MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7
# Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG
# RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6
# X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd
# BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx
# XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF
# BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
# aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo
# dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy
# bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL
# BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj
# aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0
# hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0
# F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT
# mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf
# ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE
# wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh
# OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX
# gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO
# LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG
# WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg
# AwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG
# EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0
# IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex
# MB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMx
# FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy
# NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZI
# hvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3
# zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8Tch
# TySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWj
# FDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2Uo
# yrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjP
# KHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KS
# uNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7w
# JNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vW
# doUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOg
# rY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K
# 096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCf
# gPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zy
# Me39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL
# TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG
# AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
# b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp
# Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy
# dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j
# cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB
# CwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZ
# D9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/
# ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu
# +WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4o
# bEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2h
# ECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasn
# M9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol
# /DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgY
# xQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3oc
# CVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcB
# ZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB0kwggUx
# oAMCAQICEAe0P3SLJmcoVNrErUyxTt0wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE
# BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy
# dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB
# MTAeFw0yNTEyMzEwMDAwMDBaFw0yOTAxMDIyMzU5NTlaMIHRMRMwEQYLKwYBBAGC
# NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ
# cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC
# VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK
# ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5
# IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUcNMoSVmxAi0a
# vG+StFJMNFFTUIOo3HdBZ+0gqA1XpNgUx11vB1vCZrvFsD9m5oA58tdp4gZN3LmQ
# aMvCl2ANUT7MilI02Hf1RWlygBzon6iE0GpU3lgRrwrk1dhtLpGsR6dbMKUUHprc
# vKpXk90/VN+vhzY1uik1tCTxkDCPu/AYJg7m9+tR2KqvMuYMaMLhii66eWUAGsBC
# h/uZxjkGoJF6qZ0DgFd7rW7VYljbfYSNPeZNGTDgB0J/wOsKl0mn612DTseIvAKt
# 4vra/FLFukyEyStnfQ8lWYDcLLCMCjNVrzGipmT5E2iyx7Y1RZCIpNwVogp3Ixbk
# Gbq5A/41YNOLLd4cFewyB2F037RevBCRsUODZEt1qBf7Jbu3DiYo1G+zTj9E0R1s
# FzyijcfdsTm6X5ble+yCJeGkX5XgsyPnZpyz/FX9Fr0N9pMPGWwW2PKyHEnSytXm
# 0Dxdq2P4mA4CBUxq7YoV26L2PF6QEh9BQdXTPcnLysUv7SI/a0ECAwEAAaOCAgIw
# ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRG
# 4H6CH8pvNX632bsdnrda4MtJLDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB
# BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC
# B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p
# bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI
# QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT
# QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA
# A4ICAQA1Wlq0WzJa3N6DgjgBU7nagIJBab1prPARXZreX1MOv9VjnS5o0CrfQLr6
# z3bmWHw7xT8dt6bcSwRixqvPJtv4q8Rvo80O3eUMvMxQzqmi7z1zf+HG+/3G4F+2
# IYegvPc8Ui151XCV9rjA8tvFWRLRMX0ZRxY1zfT027HMw0iYL20z44+Cky//FAnL
# iRwoNDGiRkZiHbB9YOftPAYNMG3gm1z3zOW5RdfKPrqvMuijE+dfyLIAA6Immpzu
# FMH+Wgn8NnSlot9b4YKycaqqdjd7wXDjPub/oQ7VShuCSBWj+UNOTVh0vcZGackc
# H1DLVgwp2dcKlxJiQKtkHT/T6LloY6LTe6+8wkVkr8EAv1W+q/+M1a4Ao+ykFbIA
# 2LBEmA9qdgoLtenAYIiEg+48SjMPgyBbVPE3bhL1vIqjEIxYCfdmi6wx33oYX7HB
# +bJ7zitHw4GgtpfPV8y8QRZImKmeDOKyXjQPDmQM/Eglm/Ns0GzBkVXM8h6UI34b
# WZrHz9sbLSE20m5Svmxftvw5zju+I3WsmS/stNfWlOkwU0niUgwPHaz21kjXEA5A
# g+aqv26wodqZcnGOlChoWDvSJ8KKgdOFbeAYKAMp1NY7iWV315zpGH19RipCR1NH
# 0ND8iIubk3WGNf2rzEfqlOi3h2ywqVkU6AKXHdO5JV4otSKKEDGCBdkwggXVAgEB
# MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD
# VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI
# QTM4NCAyMDIxIENBMQIQB7Q/dIsmZyhU2sStTLFO3TANBglghkgBZQMEAgEFAKCB
# hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE
# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ
# BDEiBCBUIFkamSm4c01tDWNakSxBhei1Izyna1Vw/6ff3AjytTANBgkqhkiG9w0B
# AQEFAASCAYCIAdqUmYeccEgzroQUif7z5K5UOJ4iFz0CtZZGWeFNFIzxqj/y/hx5
# E+Ax09vZtqOBLhh9XcgUvWMTN7vjDly0uxaa/Ow4W9b6W6C60EdGQIUcJnUfJ/iA
# SJxN17n56uNHHrQutKV/RqxXvVKOxQnyx5MDGCbJATULFjsJpnVoborZdkCgFpJV
# EHXg0ulsqxigFns+saOhoKHY8ObT+WXOX5GchdVD0bpsrQgKDzdw1pTzh/XE3Z6a
# Elo0WTEjBaEGXw0DX1oiolfpVxZgcLecjLfm0zfARB6mEpzw+J2/NP2hhOvYOAVQ
# GWzWO7sV4OtYYPrXMHV+pmlKtc3GgRC4WiG4/zwyS3Z5u5Om4kw3qk2RRvc9GkWg
# dtiAXj2RmeZ3A8KHnY/mi6cDdCNsihjC4lN0ySlhFfeRPSrxyA/VIEgzyLMANeUV
# WcVXO1IN/knjfzgSIsqXT8nMdb0qn5dmrX4IkOebVvSyRmAx3pkuA6AJCL91znxE
# w4aYB84q16yhggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwMjI1MjIzODA1WjAv
# BgkqhkiG9w0BCQQxIgQgrMnmAPcbe8y4D5v2LzZlyBBylwBrwkDlJ4UhF1icLGgw
# DQYJKoZIhvcNAQEBBQAEggIAkJYsY+qRuktROOF++YX3DgAzZLcKZa20nswa4aRG
# cU58cb5kEkCHgURd2/ZXMn82LLZxWqH2r20Mgz8tgXjTbqJxkmsYJ7qPp8hvMZ1F
# irYrlEBMEnLDw647/rg3N8g6QOYrHXRbnsTP67KAEwSz8omdB0Gbb5cIfDKsUG5G
# Wgw/STA4rc9+/QYU7XcnwsZVGoBCTjhlV7WexX+lHOgMlkSLc+Qp8kCJ3keBTKCJ
# cEz6/eENTGEBu1sjwvsrUf8hbQmr/3Tdwkxr+UjjvBMi4Vt6R/hgFxevgnWhyk6C
# cpb/AAj3kDR3wMFrFZolsuN37eRlu+a0UCjHW1Cf5gQANX8GFcAEvN8erVPnrARC
# v8oFnoM5CkcJN4QEhQdjVVjTQ2jI8Ez1jL46Qc5snw14Rh8VdFwpNUfeMxZw0pOT
# FeqXKXa29zaIf3+3xa9W/KdqAR6RbDYPKdaAyqx7QRv+Zl7N4YY3qPI6/qUFhz4o
# M9wobk0U8LhSxgtccssFV/vJklpMZmpBqr67a8BmYsWeQt63M6jYNAmpkQdtnaau
# 3XQ1TI52Cuz0oYfIc13LZ9KuVIqd9NQ8pwy1cAVEUXbRXkliLOgqGT8BXq+moXGr
# J7tJVc4qT8iv1hTjXMGU2NABAU/7FGKRJGBeo6ikajGkGb3ONYXCa5sigyt/IT0y
# SkU=
# SIG # End signature block