NsfFolderCommands.ps1

#requires -Version 5.1

. "$PSScriptRoot/NsfBatchSampleData.ps1"

function New-KeeperNSFFolder {
    <#
    .Synopsis
    Creates a new Keeper NSF folder.

    .Description
    Creates a new folder in Keeper NSF using the v3 API.

    .Parameter Name
    Name of the folder to create.

    .Parameter ParentFolderUid
    UID of the parent folder. If omitted, the folder is created at root level.

    .Parameter Color
    Optional color for the folder.

    .Parameter NoInheritPermissions
    If specified, the folder will not inherit permissions from its parent.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $Name,

        [Parameter()]
        [string] $ParentFolderUid,

        [Parameter()]
        [string] $Color,

        [Parameter()]
        [switch] $NoInheritPermissions
    )

    try{
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    } catch {
        Write-Host "Error getting vault: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    $inheritPermissions = -not $NoInheritPermissions.IsPresent

    try {
        $folderUid = $vault.CreateKeeperNSFFolder($Name, $ParentFolderUid, $Color, $inheritPermissions).GetAwaiter().GetResult()
        Write-Host "Folder '$Name' created successfully (UID: $folderUid)." -ForegroundColor Green
        return $folderUid
    }
    catch {
        Write-Host "Error creating folder: $($_.Exception.Message)" -ForegroundColor Red
    }
}

New-Alias -Name nsf-mkdir -Value New-KeeperNSFFolder

function New-KeeperNSFFolders {
    <#
    .Synopsis
    Creates multiple Keeper NSF folders in a single batch API call (up to 100 per request).

    .Description
    Uses batching. Creating under an existing parent requires add/create
    permission on that parent. Folder keys are AES-GCM encrypted; the folder name is stored in
    encrypted FolderData.Data. Larger sets are chunked automatically (100 folders per request).

    JSON schema: a "folders" array. Each item: name (required), optional parent / parent_uid,
    optional color, optional inherit_permissions (default true).

    .Parameter FilePath
    Path to a UTF-8 JSON folder batch file.

    .Parameter Json
    Inline JSON string (same schema as -FilePath).

    .Parameter DownloadSampleFolders
    Writes a sample batch create JSON file and exits without creating folders.

    .EXAMPLE
    PS> New-KeeperNSFFolders -DownloadSampleFolders

    .EXAMPLE
    PS> New-KeeperNSFFolders -FilePath .\nsf-folders-batch.sample.json
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'File')]
    Param(
        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [ValidateNotNullOrEmpty()]
        [string] $FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'Json')]
        [ValidateNotNullOrEmpty()]
        [string] $Json,

        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [switch] $DownloadSampleFolders
    )

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }
    catch {
        Write-Error "Not connected to Keeper. Please login first."
        return
    }

    if ($DownloadSampleFolders) {
        if (-not $FilePath) {
            $FilePath = 'nsf-folders-batch.sample.json'
        }

        $fullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath)
        $parentDir = Split-Path -Parent $fullPath
        if ($parentDir -and -not (Test-Path -LiteralPath $parentDir)) {
            New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
        }

        $sampleJson = Get-KeeperNSFFolderBatchSampleJson
        $utf8NoBom = New-Object System.Text.UTF8Encoding $false
        [System.IO.File]::WriteAllText($fullPath, $sampleJson, $utf8NoBom)

        Write-Host "Sample NSF folder batch file written to: $fullPath" -ForegroundColor Green
        Write-Host "Edit folder names / parent UIDs, then run:"
        Write-Host " New-KeeperNSFFolders -FilePath `"$FilePath`""
        return
    }

    if (-not $FilePath -and -not $Json) {
        Write-Error "FilePath or Json is required when -DownloadSampleFolders is not specified."
        return
    }

    try {
        $jsonText = if ($PSCmdlet.ParameterSetName -eq 'File') {
            if (-not (Test-Path -LiteralPath $FilePath)) {
                throw "File not found: $FilePath"
            }
            Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8
        }
        else {
            $Json
        }

        $folderRequests = ConvertTo-KeeperNSFFolderCreateRequests -JsonText $jsonText
        if (-not $folderRequests -or $folderRequests.Count -eq 0) {
            throw "Folder file contains no folders."
        }

        if ($folderRequests -isnot [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderCreateRequest]]) {
            $typed = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderCreateRequest]'
            foreach ($item in @($folderRequests)) {
                if ($item -is [KeeperSecurity.Vault.KeeperNSFFolderCreateRequest]) {
                    $typed.Add($item) | Out-Null
                }
            }
            $folderRequests = $typed
        }
    }
    catch {
        Write-Host "Error parsing folder payload: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    if (-not $PSCmdlet.ShouldProcess("$($folderRequests.Count) Keeper NSF folder(s)", "Create Keeper NSF folders")) {
        return
    }

    try {
        Write-Host "Creating $($folderRequests.Count) Keeper NSF folder(s) in batch..."
        $folderList = [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderCreateRequest]]$folderRequests
        $results = $vault.CreateKeeperNSFFolders($folderList).GetAwaiter().GetResult()

        $ok = @($results | Where-Object { $_.Success })
        $fail = @($results | Where-Object { -not $_.Success })
        Write-Host "Batch complete: $($ok.Count) succeeded, $($fail.Count) failed."
        Write-Host ""

        foreach ($result in $results) {
            if ($result.Success) {
                Write-Host " [OK] $($result.Name) UID: $($result.FolderUid)" -ForegroundColor Green
            }
            else {
                $msg = if ($result.Message) { $result.Message } else { '(no message)' }
                Write-Host " [FAIL] $($result.Name) status=$($result.Status) $msg" -ForegroundColor Red
            }
        }

        return ,@($results)
    }
    catch {
        Write-Host "Error creating folders in batch: $($_.Exception.Message)" -ForegroundColor Red
    }
}

New-Alias -Name nsf-mkdirs -Value New-KeeperNSFFolders

# Parse batch folder-create JSON into KeeperNSFFolderCreateRequest objects.
function Script:ConvertTo-KeeperNSFFolderCreateRequests {
    Param(
        [Parameter(Mandatory = $true)]
        [string] $JsonText
    )

    $parsed = $JsonText | ConvertFrom-Json -ErrorAction Stop
    $items = @()
    if ($null -ne $parsed.folders) {
        $items = @($parsed.folders)
    }
    elseif ($parsed -is [System.Array]) {
        $items = @($parsed)
    }
    else {
        throw "JSON must contain a 'folders' array (or be a root array of folder objects)."
    }

    $list = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderCreateRequest]'
    $index = 0
    foreach ($item in $items) {
        $name = $null
        if ($item.name) { $name = [string]$item.name }
        elseif ($item.Name) { $name = [string]$item.Name }

        if ([string]::IsNullOrWhiteSpace($name)) {
            throw "Folder item at index $index is missing required 'name'."
        }

        $parent = $null
        if ($item.parent) { $parent = [string]$item.parent }
        elseif ($item.parent_uid) { $parent = [string]$item.parent_uid }
        elseif ($item.ParentFolderUid) { $parent = [string]$item.ParentFolderUid }

        $color = $null
        if ($item.color) { $color = [string]$item.color }
        elseif ($item.Color) { $color = [string]$item.Color }

        $inherit = $true
        if ($null -ne $item.inherit_permissions) {
            $inherit = [bool]$item.inherit_permissions
        }
        elseif ($null -ne $item.InheritPermissions) {
            $inherit = [bool]$item.InheritPermissions
        }

        $req = New-Object KeeperSecurity.Vault.KeeperNSFFolderCreateRequest
        $req.Name = $name.Trim()
        if (-not [string]::IsNullOrWhiteSpace($parent)) {
            $req.ParentFolderUid = $parent.Trim()
        }
        $req.Color = $color
        $req.InheritPermissions = $inherit
        $list.Add($req) | Out-Null
        $index++
    }

    return ,$list
}

function Set-KeeperNSFFolderAccess {
    <#
    .Synopsis
    Grant or revoke user or team access to a Keeper NSF folder.

    .Description
    Changes the sharing permissions of a Keeper NSF folder using the v3 API.
    Supports granting access with a specified role, or revoking access entirely.
    Recipients may be user emails, team names, or team UIDs.
    For bulk grant/update/revoke from JSON, use Share-KeeperNSFFolderAccesses,
    Update-KeeperNSFFolderAccesses, or Unshare-KeeperNSFFolderAccesses.

    .Parameter FolderUid
    UID of the folder to share.

    .Parameter Action
    Action to perform: 'grant' (default) or 'remove'.

    .Parameter Email
    One or more user email addresses, team names, or team UIDs to grant/revoke access.

    .Parameter Role
    Access role for grant action: viewer (default), share-manager, content-manager,
    content-share-manager, full-manager.

    .Parameter ExpireIn
    Optional. Share expiration period from now (e.g. 30d, 6mo, 1y, 24h, 30mi), integer minutes,
    or a TimeSpan. Same as Grant-KeeperRecordAccess.

    .Parameter ExpireAt
    Optional. Absolute share expiration as ISO datetime (e.g. 2027-01-01T00:00:00Z).
#>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $FolderUid,

        [Parameter()]
        [ValidateSet('grant', 'remove')]
        [string] $Action = 'grant',

        [Parameter(Mandatory = $true)]
        [string[]] $Email,

        [Parameter()]
        [ValidateSet('viewer', 'share-manager', 'content-manager', 'content-share-manager', 'full-manager')]
        [string] $Role = 'viewer',

        [Alias('expire-in')]
        [Parameter()]
        [System.Object] $ExpireIn,

        [Alias('expire-at')]
        [Parameter()]
        [string] $ExpireAt
    )

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    } catch {
        Write-Host "Error getting vault: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    [KeeperSecurity.Vault.FolderNode]$tmpFolder = $null
    if (-not $vault.TryGetKeeperNSFFolder($FolderUid, [ref]$tmpFolder)) {
        Write-Host "Error: NSF folder '$FolderUid' not found." -ForegroundColor Red
        return
    }

    $shareOptions = $null
    if ($Action -eq 'grant' -and ($ExpireIn -or $ExpireAt)) {
        try {
            $expirationDto = Get-ExpirationDate -ExpireIn $ExpireIn -ExpireAt $ExpireAt
        }
        catch {
            Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
            return
        }
        $shareOptions = New-Object KeeperSecurity.Vault.SharedFolderUserOptions
        $shareOptions.Expiration = $expirationDto
    }

    foreach ($user in $Email) {
        try {
            if ($Action -eq 'grant') {
                [void]$vault.GrantKeeperNSFFolderAccess($FolderUid, $user, $Role, $shareOptions).GetAwaiter().GetResult()
                $expireMsg = if ($shareOptions -and $shareOptions.Expiration) {
                    " (expires $($shareOptions.Expiration.LocalDateTime.ToString('g')))"
                } else { '' }
                Write-Host "Granted '$Role' access to '$user' on folder '$FolderUid'$expireMsg." -ForegroundColor Green
            }
            else {
                [void]$vault.RevokeKeeperNSFFolderAccess($FolderUid, $user).GetAwaiter().GetResult()
                Write-Host "Revoked access for '$user' from folder '$FolderUid'." -ForegroundColor Green
            }
        }
        catch {
            Write-Host "Error ${Action}ing access for '$user': $($_.Exception.Message)" -ForegroundColor Red
        }
    }
}

New-Alias -Name nsf-share-folder -Value Set-KeeperNSFFolderAccess

function Share-KeeperNSFFolderAccesses {
    <#
    .Synopsis
    Batch-grant Keeper NSF folder access (user or team) — up to 500 entries per API request.

    .Description
    Uses vault/folders/v3/access_update (FolderAccessAdds). Independent of Set-KeeperNSFFolderAccess.
    Caller must have share/update-access permission on each folder.
    JSON schema: an "accesses" array. Each item: folder_uid, accessor (email/team name/UID),
    optional role (default viewer), optional expire_in / expire_at.
    Omit as_team for team names/UIDs so resolution matches nsf-share-folder auto-detect.

    .Parameter FilePath
    Path to a UTF-8 JSON folder access grant batch file.

    .Parameter Json
    Inline JSON string (same schema as -FilePath).

    .Parameter DownloadSampleAccesses
    Writes a sample grant batch JSON file and exits without granting access.

    .EXAMPLE
    PS> Share-KeeperNSFFolderAccesses -DownloadSampleAccesses

    .EXAMPLE
    PS> Share-KeeperNSFFolderAccesses -FilePath .\nsf-folders-access-grant-batch.sample.json
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'File')]
    Param(
        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [ValidateNotNullOrEmpty()]
        [string] $FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'Json')]
        [ValidateNotNullOrEmpty()]
        [string] $Json,

        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [switch] $DownloadSampleAccesses
    )

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }
    catch {
        Write-Error "Not connected to Keeper. Please login first."
        return
    }

    if ($DownloadSampleAccesses) {
        if (-not $FilePath) {
            $FilePath = 'nsf-folders-access-grant-batch.sample.json'
        }

        $fullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath)
        $parentDir = Split-Path -Parent $fullPath
        if ($parentDir -and -not (Test-Path -LiteralPath $parentDir)) {
            New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
        }

        $sampleJson = Get-KeeperNSFFolderAccessGrantBatchSampleJson
        $utf8NoBom = New-Object System.Text.UTF8Encoding $false
        [System.IO.File]::WriteAllText($fullPath, $sampleJson, $utf8NoBom)

        Write-Host "Sample NSF folder access grant batch file written to: $fullPath" -ForegroundColor Green
        Write-Host "Replace placeholders, then run:"
        Write-Host " Share-KeeperNSFFolderAccesses -FilePath `"$FilePath`""
        return
    }

    if (-not $FilePath -and -not $Json) {
        Write-Error "FilePath or Json is required when -DownloadSampleAccesses is not specified."
        return
    }

    try {
        $jsonText = if ($PSCmdlet.ParameterSetName -eq 'File') {
            if (-not (Test-Path -LiteralPath $FilePath)) {
                throw "File not found: $FilePath"
            }
            Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8
        }
        else {
            $Json
        }

        $requests = ConvertTo-KeeperNSFFolderAccessGrantRequests -JsonText $jsonText
        if (-not $requests -or $requests.Count -eq 0) {
            throw "Access grant file contains no entries."
        }

        if ($requests -isnot [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessGrantRequest]]) {
            $typed = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessGrantRequest]'
            foreach ($item in @($requests)) {
                if ($item -is [KeeperSecurity.Vault.KeeperNSFFolderAccessGrantRequest]) {
                    $typed.Add($item) | Out-Null
                }
            }
            $requests = $typed
        }
    }
    catch {
        Write-Host "Error parsing folder access grant payload: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    if (-not $PSCmdlet.ShouldProcess("$($requests.Count) Keeper NSF folder access grant(s)", "Grant Keeper NSF folder access")) {
        return
    }

    try {
        Write-Host "Granting $($requests.Count) Keeper NSF folder access(es) in batch..."
        $list = [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessGrantRequest]]$requests
        $results = $vault.GrantKeeperNSFFolderAccesses($list).GetAwaiter().GetResult()
        Write-KeeperNSFFolderAccessBatchResults -Results $results -ActionLabel 'grant'
        return ,@($results)
    }
    catch {
        Write-Host "Error granting folder access in batch: $($_.Exception.Message)" -ForegroundColor Red
    }
}

New-Alias -Name nsf-share-folders -Value Share-KeeperNSFFolderAccesses

function Update-KeeperNSFFolderAccesses {
    <#
    .Synopsis
    Batch-update existing Keeper NSF folder access (user or team) — up to 500 entries per API request.

    .Description
    Uses vault/folders/v3/access_update (FolderAccessUpdates). Independent of Set-KeeperNSFFolderAccess.
    JSON schema: an "accesses" array. Each item: folder_uid, accessor, role and/or expire_in/expire_at.
    Omit as_team for team names/UIDs so resolution matches nsf-share-folder auto-detect.

    .Parameter FilePath
    Path to a UTF-8 JSON folder access update batch file.

    .Parameter Json
    Inline JSON string (same schema as -FilePath).

    .Parameter DownloadSampleAccesses
    Writes a sample update batch JSON file and exits without updating access.

    .EXAMPLE
    PS> Update-KeeperNSFFolderAccesses -DownloadSampleAccesses

    .EXAMPLE
    PS> Update-KeeperNSFFolderAccesses -FilePath .\nsf-folders-access-update-batch.sample.json
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'File')]
    Param(
        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [ValidateNotNullOrEmpty()]
        [string] $FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'Json')]
        [ValidateNotNullOrEmpty()]
        [string] $Json,

        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [switch] $DownloadSampleAccesses
    )

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }
    catch {
        Write-Error "Not connected to Keeper. Please login first."
        return
    }

    if ($DownloadSampleAccesses) {
        if (-not $FilePath) {
            $FilePath = 'nsf-folders-access-update-batch.sample.json'
        }

        $fullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath)
        $parentDir = Split-Path -Parent $fullPath
        if ($parentDir -and -not (Test-Path -LiteralPath $parentDir)) {
            New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
        }

        $sampleJson = Get-KeeperNSFFolderAccessUpdateBatchSampleJson
        $utf8NoBom = New-Object System.Text.UTF8Encoding $false
        [System.IO.File]::WriteAllText($fullPath, $sampleJson, $utf8NoBom)

        Write-Host "Sample NSF folder access update batch file written to: $fullPath" -ForegroundColor Green
        Write-Host "Replace placeholders, then run:"
        Write-Host " Update-KeeperNSFFolderAccesses -FilePath `"$FilePath`""
        return
    }

    if (-not $FilePath -and -not $Json) {
        Write-Error "FilePath or Json is required when -DownloadSampleAccesses is not specified."
        return
    }

    try {
        $jsonText = if ($PSCmdlet.ParameterSetName -eq 'File') {
            if (-not (Test-Path -LiteralPath $FilePath)) {
                throw "File not found: $FilePath"
            }
            Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8
        }
        else {
            $Json
        }

        $requests = ConvertTo-KeeperNSFFolderAccessUpdateRequests -JsonText $jsonText
        if (-not $requests -or $requests.Count -eq 0) {
            throw "Access update file contains no entries."
        }

        if ($requests -isnot [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessUpdateRequest]]) {
            $typed = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessUpdateRequest]'
            foreach ($item in @($requests)) {
                if ($item -is [KeeperSecurity.Vault.KeeperNSFFolderAccessUpdateRequest]) {
                    $typed.Add($item) | Out-Null
                }
            }
            $requests = $typed
        }
    }
    catch {
        Write-Host "Error parsing folder access update payload: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    if (-not $PSCmdlet.ShouldProcess("$($requests.Count) Keeper NSF folder access update(s)", "Update Keeper NSF folder access")) {
        return
    }

    try {
        Write-Host "Updating $($requests.Count) Keeper NSF folder access(es) in batch..."
        $list = [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessUpdateRequest]]$requests
        $results = $vault.UpdateKeeperNSFFolderAccesses($list).GetAwaiter().GetResult()
        Write-KeeperNSFFolderAccessBatchResults -Results $results -ActionLabel 'update'
        return ,@($results)
    }
    catch {
        Write-Host "Error updating folder access in batch: $($_.Exception.Message)" -ForegroundColor Red
    }
}

New-Alias -Name nsf-update-folder-access -Value Update-KeeperNSFFolderAccesses

function Unshare-KeeperNSFFolderAccesses {
    <#
    .Synopsis
    Batch-revoke Keeper NSF folder access (user or team) — up to 500 entries per API request.

    .Description
    Uses vault/folders/v3/access_update (FolderAccessRemoves). Independent of Set-KeeperNSFFolderAccess.
    JSON schema: an "accesses" array. Each item: folder_uid, accessor.
    Omit as_team for team names/UIDs so resolution matches nsf-share-folder auto-detect.

    .Parameter FilePath
    Path to a UTF-8 JSON folder access revoke batch file.

    .Parameter Json
    Inline JSON string (same schema as -FilePath).

    .Parameter DownloadSampleAccesses
    Writes a sample revoke batch JSON file and exits without revoking access.

    .EXAMPLE
    PS> Unshare-KeeperNSFFolderAccesses -DownloadSampleAccesses

    .EXAMPLE
    PS> Unshare-KeeperNSFFolderAccesses -FilePath .\nsf-folders-access-revoke-batch.sample.json
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'File')]
    Param(
        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [ValidateNotNullOrEmpty()]
        [string] $FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'Json')]
        [ValidateNotNullOrEmpty()]
        [string] $Json,

        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [switch] $DownloadSampleAccesses
    )

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }
    catch {
        Write-Error "Not connected to Keeper. Please login first."
        return
    }

    if ($DownloadSampleAccesses) {
        if (-not $FilePath) {
            $FilePath = 'nsf-folders-access-revoke-batch.sample.json'
        }

        $fullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath)
        $parentDir = Split-Path -Parent $fullPath
        if ($parentDir -and -not (Test-Path -LiteralPath $parentDir)) {
            New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
        }

        $sampleJson = Get-KeeperNSFFolderAccessRevokeBatchSampleJson
        $utf8NoBom = New-Object System.Text.UTF8Encoding $false
        [System.IO.File]::WriteAllText($fullPath, $sampleJson, $utf8NoBom)

        Write-Host "Sample NSF folder access revoke batch file written to: $fullPath" -ForegroundColor Green
        Write-Host "Replace placeholders, then run:"
        Write-Host " Unshare-KeeperNSFFolderAccesses -FilePath `"$FilePath`""
        return
    }

    if (-not $FilePath -and -not $Json) {
        Write-Error "FilePath or Json is required when -DownloadSampleAccesses is not specified."
        return
    }

    try {
        $jsonText = if ($PSCmdlet.ParameterSetName -eq 'File') {
            if (-not (Test-Path -LiteralPath $FilePath)) {
                throw "File not found: $FilePath"
            }
            Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8
        }
        else {
            $Json
        }

        $requests = ConvertTo-KeeperNSFFolderAccessRevokeRequests -JsonText $jsonText
        if (-not $requests -or $requests.Count -eq 0) {
            throw "Access revoke file contains no entries."
        }

        if ($requests -isnot [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessRevokeRequest]]) {
            $typed = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessRevokeRequest]'
            foreach ($item in @($requests)) {
                if ($item -is [KeeperSecurity.Vault.KeeperNSFFolderAccessRevokeRequest]) {
                    $typed.Add($item) | Out-Null
                }
            }
            $requests = $typed
        }
    }
    catch {
        Write-Host "Error parsing folder access revoke payload: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    if (-not $PSCmdlet.ShouldProcess("$($requests.Count) Keeper NSF folder access revoke(s)", "Revoke Keeper NSF folder access")) {
        return
    }

    try {
        Write-Host "Revoking $($requests.Count) Keeper NSF folder access(es) in batch..."
        $list = [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessRevokeRequest]]$requests
        $results = $vault.RevokeKeeperNSFFolderAccesses($list).GetAwaiter().GetResult()
        Write-KeeperNSFFolderAccessBatchResults -Results $results -ActionLabel 'revoke'
        return ,@($results)
    }
    catch {
        Write-Host "Error revoking folder access in batch: $($_.Exception.Message)" -ForegroundColor Red
    }
}

New-Alias -Name nsf-unshare-folders -Value Unshare-KeeperNSFFolderAccesses

# Print per-item OK/FAIL lines after a folder-access batch call.
function Script:Write-KeeperNSFFolderAccessBatchResults {
    Param(
        [Parameter(Mandatory = $true)]
        $Results,
        [string] $ActionLabel = 'access'
    )

    $ok = @($Results | Where-Object { $_.Success })
    $fail = @($Results | Where-Object { -not $_.Success })
    Write-Host "Batch complete: $($ok.Count) succeeded, $($fail.Count) failed."
    Write-Host ""

    foreach ($result in $Results) {
        $typeHint = if ($result.AccessType) { " ($($result.AccessType))" } else { '' }
        $roleHint = if ($result.Role) { " [$($result.Role)]" } else { '' }
        if ($result.Success) {
            Write-Host " [OK] $($result.FolderUid) -> $($result.Accessor)$typeHint$roleHint" -ForegroundColor Green
        }
        else {
            $msg = if ($result.Message) { $result.Message } else { '(no message)' }
            Write-Host " [FAIL] $($result.FolderUid) -> $($result.Accessor)$typeHint status=$($result.Status) $msg" -ForegroundColor Red
        }
    }
}

# Pull the access array out of grant/update/revoke batch JSON (accesses, shares, or root array).
function Script:Get-KeeperNSFFolderAccessJsonItems {
    Param(
        [Parameter(Mandatory = $true)]
        [string] $JsonText
    )

    $parsed = $JsonText | ConvertFrom-Json -ErrorAction Stop
    if ($null -ne $parsed.accesses) {
        return @($parsed.accesses)
    }
    if ($null -ne $parsed.shares) {
        return @($parsed.shares)
    }
    if ($parsed -is [System.Array]) {
        return @($parsed)
    }
    throw "JSON must contain an 'accesses' array (or be a root array of access objects)."
}

# Normalize one JSON access object to folder_uid, accessor, role, team flag, and expiration.
function Script:Get-KeeperNSFFolderAccessItemFields {
    Param($Item, [int] $Index, [switch] $RequireRole)

    $folderUid = $null
    if ($Item.folder_uid) { $folderUid = [string]$Item.folder_uid }
    elseif ($Item.uid) { $folderUid = [string]$Item.uid }
    elseif ($Item.FolderUid) { $folderUid = [string]$Item.FolderUid }

    $accessor = $null
    if ($Item.accessor) { $accessor = [string]$Item.accessor }
    elseif ($Item.email) { $accessor = [string]$Item.email }
    elseif ($Item.user_email) { $accessor = [string]$Item.user_email }
    elseif ($Item.Accessor) { $accessor = [string]$Item.Accessor }

    if ([string]::IsNullOrWhiteSpace($folderUid)) {
        throw "Access item at index $Index is missing required 'folder_uid' (or uid)."
    }
    if ([string]::IsNullOrWhiteSpace($accessor)) {
        throw "Access item at index $Index is missing required 'accessor' (or email)."
    }

    $role = $null
    if ($Item.role) { $role = [string]$Item.role }
    elseif ($Item.Role) { $role = [string]$Item.Role }

    $validRoles = @('viewer', 'share-manager', 'content-manager', 'content-share-manager', 'full-manager')
    if ($RequireRole) {
        if ([string]::IsNullOrWhiteSpace($role)) { $role = 'viewer' }
        $roleNormalized = $role.Trim().ToLowerInvariant()
        if ($validRoles -notcontains $roleNormalized) {
            throw "Invalid role '$role' at access index $Index. Valid roles: $($validRoles -join ', ')."
        }
        $role = $roleNormalized
    }
    elseif (-not [string]::IsNullOrWhiteSpace($role)) {
        $roleNormalized = $role.Trim().ToLowerInvariant()
        if ($validRoles -notcontains $roleNormalized) {
            throw "Invalid role '$role' at access index $Index. Valid roles: $($validRoles -join ', ')."
        }
        $role = $roleNormalized
    }

    $asTeam = $null
    if ($null -ne $Item.as_team) { $asTeam = [bool]$Item.as_team }
    elseif ($null -ne $Item.AsTeam) { $asTeam = [bool]$Item.AsTeam }

    $expireIn = $null
    if ($Item.expire_in) { $expireIn = $Item.expire_in }
    elseif ($Item.ExpireIn) { $expireIn = $Item.ExpireIn }

    $expireAt = $null
    if ($Item.expire_at) { $expireAt = [string]$Item.expire_at }
    elseif ($Item.ExpireAt) { $expireAt = [string]$Item.ExpireAt }

    $options = $null
    if ($expireIn -or $expireAt) {
        $expirationDto = Get-ExpirationDate -ExpireIn $expireIn -ExpireAt $expireAt
        $options = New-Object KeeperSecurity.Vault.SharedFolderUserOptions
        $options.Expiration = $expirationDto
    }

    return @{
        FolderUid = $folderUid.Trim()
        Accessor  = $accessor.Trim()
        Role      = $role
        AsTeam    = $asTeam
        Options   = $options
    }
}

# Build grant requests from folder-access batch JSON.
function Script:ConvertTo-KeeperNSFFolderAccessGrantRequests {
    Param(
        [Parameter(Mandatory = $true)]
        [string] $JsonText
    )

    $items = Get-KeeperNSFFolderAccessJsonItems -JsonText $JsonText
    $list = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessGrantRequest]'
    $index = 0
    foreach ($item in $items) {
        $fields = Get-KeeperNSFFolderAccessItemFields -Item $item -Index $index -RequireRole
        $req = New-Object KeeperSecurity.Vault.KeeperNSFFolderAccessGrantRequest
        $req.FolderUid = $fields.FolderUid
        $req.Accessor = $fields.Accessor
        $req.Role = $fields.Role
        if ($null -ne $fields.AsTeam) { $req.AsTeam = $fields.AsTeam }
        if ($null -ne $fields.Options) { $req.Options = $fields.Options }
        $list.Add($req) | Out-Null
        $index++
    }
    return ,$list
}

# Build role/expiration update requests from folder-access batch JSON.
function Script:ConvertTo-KeeperNSFFolderAccessUpdateRequests {
    Param(
        [Parameter(Mandatory = $true)]
        [string] $JsonText
    )

    $items = Get-KeeperNSFFolderAccessJsonItems -JsonText $JsonText
    $list = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessUpdateRequest]'
    $index = 0
    foreach ($item in $items) {
        $fields = Get-KeeperNSFFolderAccessItemFields -Item $item -Index $index
        if ([string]::IsNullOrWhiteSpace($fields.Role) -and $null -eq $fields.Options) {
            throw "Access update at index $index requires 'role' and/or expire_in/expire_at."
        }
        $req = New-Object KeeperSecurity.Vault.KeeperNSFFolderAccessUpdateRequest
        $req.FolderUid = $fields.FolderUid
        $req.Accessor = $fields.Accessor
        $req.Role = $fields.Role
        if ($null -ne $fields.AsTeam) { $req.AsTeam = $fields.AsTeam }
        if ($null -ne $fields.Options) { $req.Options = $fields.Options }
        $list.Add($req) | Out-Null
        $index++
    }
    return ,$list
}

# Build revoke requests from folder-access batch JSON.
function Script:ConvertTo-KeeperNSFFolderAccessRevokeRequests {
    Param(
        [Parameter(Mandatory = $true)]
        [string] $JsonText
    )

    $items = Get-KeeperNSFFolderAccessJsonItems -JsonText $JsonText
    $list = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderAccessRevokeRequest]'
    $index = 0
    foreach ($item in $items) {
        $fields = Get-KeeperNSFFolderAccessItemFields -Item $item -Index $index
        $req = New-Object KeeperSecurity.Vault.KeeperNSFFolderAccessRevokeRequest
        $req.FolderUid = $fields.FolderUid
        $req.Accessor = $fields.Accessor
        if ($null -ne $fields.AsTeam) { $req.AsTeam = $fields.AsTeam }
        $list.Add($req) | Out-Null
        $index++
    }
    return ,$list
}

function Write-KeeperNSFRemoveImpact {
    param(
        [Folder.V3.Remove.RemoveResponse]$Response,
        [string] $ItemLabel = 'Record'
    )

    if ($Response.ErrorMessage) {
        Write-Host "Error: $($Response.ErrorMessage)" -ForegroundColor Red
    }

    foreach ($result in $Response.Results) {
        $recordUid = if ($result.ItemUid.Length -gt 0) {
            [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($result.ItemUid.ToByteArray())
        } else { '(unknown)' }

        $folderUid = if ($result.FolderUid.Length -gt 0) {
            [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($result.FolderUid.ToByteArray())
        } else { '' }

        Write-Host ""
        Write-Host "${ItemLabel}: $recordUid" -ForegroundColor Cyan
        if ($folderUid) {
            Write-Host " Folder context: $folderUid"
        }
        Write-Host " Status: $($result.Status)"

        if ($result.Error -and $result.Error.Message) {
            Write-Host " Error: $($result.Error.Message)" -ForegroundColor Red
        }

        if ($result.Impact) {
            $impact = $result.Impact
            Write-Host " Impact:"
            Write-Host " Folders: $($impact.FoldersCount)"
            Write-Host " Records: $($impact.RecordsCount)"
            Write-Host " Affected users: $($impact.AffectedUsersCount)"
            Write-Host " Affected teams: $($impact.AffectedTeamsCount)"
            if ($impact.RecordInfo) {
                Write-Host " Other locations: $($impact.RecordInfo.LocationsCount)"
            }
            foreach ($warning in $impact.Warnings) {
                Write-Host " Warning: $warning" -ForegroundColor Yellow
            }
        }
    }
}
function Set-KeeperNSFFolder {
    <#
    .Synopsis
    Renames, recolors, or updates permission inheritance for a single Keeper NSF folder.

    .Description
    Updates one folder via the Keeper NSF v3 API.
    For updating many folders from JSON, use Set-KeeperNSFFolders (nsf-folders-update).

    .Parameter Folder
    Folder UID or name.

    .Parameter Name
    New folder name.

    .Parameter Color
    Optional folder color, or "none" to clear.

    .Parameter NoInheritPermissions
    Do not inherit parent folder permissions.

    .EXAMPLE
    PS> Set-KeeperNSFFolder <folderUid> -Name "Renamed" -Color blue

    .EXAMPLE
    PS> nsf-rndir <folderUid> -Name "Renamed"
#>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Default')]
    Param(
        [Parameter(Position = 0, Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Folder,

        [Alias('n')]
        [Parameter(ParameterSetName = 'Default')]
        [string] $Name,

        [ValidateSet('none', 'red', 'orange', 'yellow', 'green', 'blue', 'gray', 'grey')]
        [string] $Color,

        [Parameter()]
        [switch] $NoInheritPermissions
    )

    if (-not $Name -and -not $PSBoundParameters.ContainsKey('Color') -and -not $NoInheritPermissions.IsPresent) {
        Write-Error -Message "Specify -Name, -Color, and/or -NoInheritPermissions to update the folder."
        return
    }

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }
    catch {
        Write-Error -Message "Error getting vault: $($_.Exception.Message)"
        return
    }

    [KeeperSecurity.Vault.FolderNode]$folderNode = $null
    if (-not $vault.TryResolveKeeperNSFFolder($Folder, [ref]$folderNode)) {
        Write-Error -Message "Keeper NSF folder `"$Folder`" was not found. Run Sync-Keeper or nsf-list first."
        return
    }

    $newNameArg = if ($PSBoundParameters.ContainsKey('Name')) { $Name } else { [NullString]::Value }
    $colorArg = if ($PSBoundParameters.ContainsKey('Color')) { $Color } else { [NullString]::Value }
    $inheritVal = if ($NoInheritPermissions.IsPresent) { $false } else { $null }

    $target = if ([string]::IsNullOrEmpty($folderNode.FolderUid)) { $Folder } else { "$($folderNode.Name) ($($folderNode.FolderUid))" }
    if (-not $PSCmdlet.ShouldProcess($target, "Update Keeper NSF folder")) {
        return
    }

    try {
        $result = $vault.UpdateKeeperNSFFolder($folderNode.FolderUid, $newNameArg, $colorArg, $inheritVal).GetAwaiter().GetResult()
        [KeeperSecurity.Vault.VaultOnline]::ValidateFolderModifyResult($result)
        Write-Host "Folder '$($folderNode.FolderUid)' updated." -ForegroundColor Green
    }
    catch {
        Write-Error -Message $_.Exception.Message
        return
    }

    $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null
}
New-Alias -Name nsf-rndir -Value Set-KeeperNSFFolder

function Set-KeeperNSFFolders {
    <#
    .Synopsis
    Batch-updates Keeper NSF folders from JSON (up to 100 per API request).

    .Description
    Uses vault/folders/v3/update batching. Independent of Set-KeeperNSFFolder / nsf-rndir (single folder).
    Folder name/color are AES-GCM encrypted into FolderData.Data.
    Optional inherit_permissions may only be false (disable inheritance); true is rejected — same as
    Set-KeeperNSFFolder -NoInheritPermissions. Requires share/update-access permission on each folder.
    Larger sets are chunked automatically (100 folders per request).

    JSON schema: a "folders" array. Each item: uid (required), optional name, optional color,
    optional inherit_permissions (false only).

    .Parameter FilePath
    Path to a UTF-8 JSON folder update batch file.

    .Parameter Json
    Inline JSON string (same schema as -FilePath).

    .Parameter DownloadSampleFolders
    Writes a sample batch update JSON file and exits without updating folders.

    .EXAMPLE
    PS> Set-KeeperNSFFolders -DownloadSampleFolders

    .EXAMPLE
    PS> Set-KeeperNSFFolders -FilePath .\nsf-folders-update-batch.sample.json
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'File')]
    Param(
        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [ValidateNotNullOrEmpty()]
        [string] $FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'Json')]
        [ValidateNotNullOrEmpty()]
        [string] $Json,

        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [switch] $DownloadSampleFolders
    )

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }
    catch {
        Write-Error "Not connected to Keeper. Please login first."
        return
    }

    if ($DownloadSampleFolders) {
        if (-not $FilePath) {
            $FilePath = 'nsf-folders-update-batch.sample.json'
        }

        $fullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath)
        $parentDir = Split-Path -Parent $fullPath
        if ($parentDir -and -not (Test-Path -LiteralPath $parentDir)) {
            New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
        }

        $sampleJson = Get-KeeperNSFFolderUpdateBatchSampleJson
        $utf8NoBom = New-Object System.Text.UTF8Encoding $false
        [System.IO.File]::WriteAllText($fullPath, $sampleJson, $utf8NoBom)

        Write-Host "Sample NSF folder update batch file written to: $fullPath" -ForegroundColor Green
        Write-Host "Replace REPLACE_WITH_FOLDER_UID_* values, then run:"
        Write-Host " Set-KeeperNSFFolders -FilePath `"$FilePath`""
        return
    }

    if (-not $FilePath -and -not $Json) {
        Write-Error "FilePath or Json is required when -DownloadSampleFolders is not specified."
        return
    }

    try {
        $jsonText = if ($PSCmdlet.ParameterSetName -eq 'File') {
            if (-not (Test-Path -LiteralPath $FilePath)) {
                throw "File not found: $FilePath"
            }
            Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8
        }
        else {
            $Json
        }

        $folderRequests = ConvertTo-KeeperNSFFolderUpdateRequests -JsonText $jsonText
        if (-not $folderRequests -or $folderRequests.Count -eq 0) {
            throw "Folder update file contains no folders."
        }

        if ($folderRequests -isnot [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderUpdateRequest]]) {
            $typed = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderUpdateRequest]'
            foreach ($item in @($folderRequests)) {
                if ($item -is [KeeperSecurity.Vault.KeeperNSFFolderUpdateRequest]) {
                    $typed.Add($item) | Out-Null
                }
            }
            $folderRequests = $typed
        }
    }
    catch {
        Write-Host "Error parsing folder update payload: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    if (-not $PSCmdlet.ShouldProcess("$($folderRequests.Count) Keeper NSF folder(s)", "Update Keeper NSF folders")) {
        return
    }

    try {
        Write-Host "Updating $($folderRequests.Count) Keeper NSF folder(s) in batch..."
        $folderList = [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderUpdateRequest]]$folderRequests
        $results = $vault.UpdateKeeperNSFFolders($folderList).GetAwaiter().GetResult()

        $ok = @($results | Where-Object { $_.Success })
        $fail = @($results | Where-Object { -not $_.Success })
        Write-Host "Batch complete: $($ok.Count) succeeded, $($fail.Count) failed."
        Write-Host ""

        foreach ($result in $results) {
            if ($result.Success) {
                Write-Host " [OK] $($result.Name) UID: $($result.FolderUid)" -ForegroundColor Green
            }
            else {
                $msg = if ($result.Message) { $result.Message } else { '(no message)' }
                Write-Host " [FAIL] $($result.Name) UID: $($result.FolderUid) status=$($result.Status) $msg" -ForegroundColor Red
            }
        }

        return ,@($results)
    }
    catch {
        Write-Host "Error updating folders in batch: $($_.Exception.Message)" -ForegroundColor Red
    }
}

New-Alias -Name nsf-folders-update -Value Set-KeeperNSFFolders

# Parse batch folder-update JSON into KeeperNSFFolderUpdateRequest objects.
function Script:ConvertTo-KeeperNSFFolderUpdateRequests {
    Param(
        [Parameter(Mandatory = $true)]
        [string] $JsonText
    )

    $parsed = $JsonText | ConvertFrom-Json -ErrorAction Stop
    $items = @()
    if ($null -ne $parsed.folders) {
        $items = @($parsed.folders)
    }
    elseif ($parsed -is [System.Array]) {
        $items = @($parsed)
    }
    else {
        throw "JSON must contain a 'folders' array (or be a root array of folder update objects)."
    }

    $list = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderUpdateRequest]'
    $index = 0
    foreach ($item in $items) {
        $uid = $null
        if ($item.uid) { $uid = [string]$item.uid }
        elseif ($item.folder_uid) { $uid = [string]$item.folder_uid }
        elseif ($item.FolderUid) { $uid = [string]$item.FolderUid }

        if ([string]::IsNullOrWhiteSpace($uid)) {
            throw "Folder update item at index $index is missing required 'uid' (or folder_uid)."
        }

        $hasName = $false
        $name = $null
        if ($null -ne $item.PSObject.Properties['name']) {
            $hasName = $true
            if ($null -ne $item.name) { $name = [string]$item.name }
        }
        elseif ($null -ne $item.PSObject.Properties['Name']) {
            $hasName = $true
            if ($null -ne $item.Name) { $name = [string]$item.Name }
        }

        $hasColor = $false
        $color = $null
        if ($null -ne $item.PSObject.Properties['color']) {
            $hasColor = $true
            if ($null -ne $item.color) { $color = [string]$item.color }
        }
        elseif ($null -ne $item.PSObject.Properties['Color']) {
            $hasColor = $true
            if ($null -ne $item.Color) { $color = [string]$item.Color }
        }

        $hasInherit = $false
        $inherit = $null
        if ($null -ne $item.PSObject.Properties['inherit_permissions']) {
            $hasInherit = $true
            if ($null -ne $item.inherit_permissions) { $inherit = [bool]$item.inherit_permissions }
        }
        elseif ($null -ne $item.PSObject.Properties['InheritPermissions']) {
            $hasInherit = $true
            if ($null -ne $item.InheritPermissions) { $inherit = [bool]$item.InheritPermissions }
        }

        if (-not $hasName -and -not $hasColor -and -not $hasInherit) {
            throw "Folder update item at index $index must include name, color, and/or inherit_permissions."
        }

        if ($hasInherit -and $null -ne $inherit -and $inherit -eq $true) {
            throw "Folder update item at index ${index}: inherit_permissions can only be false on update (same as Set-KeeperNSFFolder -NoInheritPermissions)."
        }

        $req = New-Object KeeperSecurity.Vault.KeeperNSFFolderUpdateRequest
        $req.FolderUid = $uid.Trim()
        if ($hasName) { $req.Name = $name }
        if ($hasColor) { $req.Color = $color }
        if ($hasInherit -and $null -ne $inherit) { $req.InheritPermissions = $inherit }
        $list.Add($req) | Out-Null
        $index++
    }

    return ,$list
}

function Remove-KeeperNSFFolder {
    <#
    .Synopsis
    Removes one or more Keeper NSF folders by UID/name (Keeper NSF v3 API).

    .Description
    Uses vault/folders/v3/remove_folder (preview/confirm). Max 100 folders per API request;
    larger sets are chunked automatically. For JSON batch remove, use Remove-KeeperNSFFolders (nsf-rmdirs).

    .Parameter Folder
    One or more folder UIDs or names.

    .Parameter Operation
    folder-trash (default, recoverable) or delete-permanent (irreversible).
    owner-trash is not supported for folders.

    .Parameter Force
    Skip confirmation after preview.

    .Parameter DryRun
    Preview only; do not remove folders.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Default')]
    Param(
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [string[]] $Folder,

        [Alias('o')]
        [ValidateSet('folder-trash', 'delete-permanent')]
        [string] $Operation = 'folder-trash',

        [Alias('f')]
        [switch] $Force,

        [switch] $DryRun
    )

    begin {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
        $removals = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderRemoval]'

        $op = switch ($Operation) {
            'folder-trash' { [KeeperSecurity.Vault.KeeperNSFFolderRemoveOperation]::FolderTrash }
            'delete-permanent' { [KeeperSecurity.Vault.KeeperNSFFolderRemoveOperation]::DeletePermanent }
        }
    }

    process {
        foreach ($name in $Folder) {
            [KeeperSecurity.Vault.FolderNode]$folderNode = $null
            if (-not $vault.TryResolveKeeperNSFFolder($name, [ref]$folderNode)) {
                Write-Error -Message "Keeper NSF folder `"$name`" was not found. Run Sync-Keeper or nsf-list first."
                continue
            }

            $removal = New-Object KeeperSecurity.Vault.KeeperNSFFolderRemoval
            $removal.FolderUid = $folderNode.FolderUid
            $removal.Operation = $op
            $removals.Add($removal)
        }
    }

    end {
        if ($removals.Count -eq 0) {
            return
        }

        if ($Operation -eq 'delete-permanent' -and -not $Force -and -not $DryRun) {
            Write-Host ""
            Write-Host "*** WARNING ***" -ForegroundColor Red
            Write-Host " delete-permanent is IRREVERSIBLE."
            Write-Host " All sub-folders and records inside will be permanently destroyed."
        }

        Write-Host ""
        Write-Host "=== Keeper NSF Folder Remove Preview ===" -ForegroundColor Cyan
        try {
            $previewResult = $vault.RemoveKeeperNSFFolders($removals, $true).GetAwaiter().GetResult()
        }
        catch {
            Write-Error -Message $_.Exception.Message
            return
        }
        Write-KeeperNSFRemoveImpact -Response $previewResult.PreviewResponse -ItemLabel 'Folder'

        if ($previewResult.FailedChunkCount -gt 0) {
            Write-Error -Message "Folder remove preview failed for $($previewResult.FailedChunkCount) chunk(s)."
            foreach ($err in @($previewResult.ChunkErrors)) {
                Write-Warning " $err"
            }
            return
        }

        try {
            [KeeperSecurity.Vault.VaultOnline]::ValidateRemoveResponse($previewResult.PreviewResponse, $false)
        }
        catch {
            Write-Error -Message $_.Exception.Message
            return
        }

        if ($DryRun) {
            Write-Host ""
            Write-Host "Dry run: no folders were removed." -ForegroundColor DarkYellow
            return
        }

        if (-not $Force) {
            $prompt = if ($Operation -eq 'delete-permanent') {
                "Are you sure you want to permanently delete the folder(s) above? This action cannot be undone. (yes/No)"
            } else {
                "Are you sure you want to remove the folder(s) above? (yes/No)"
            }
            $confirmation = Read-Host $prompt
            if ($confirmation -notmatch '^(y|yes)$') {
                Write-Host "Remove operation cancelled"
                return
            }
        }

        if (-not $previewResult.ChunkConfirmationTokens -or $previewResult.ChunkConfirmationTokens.Count -eq 0) {
            Write-Error -Message "Preview did not return confirmation token(s)."
            return
        }

        Write-Host ""
        Write-Host "Removing folders..." -ForegroundColor Cyan
        try {
            $confirmResult = $vault.ConfirmKeeperNSFFolders($removals, $previewResult).GetAwaiter().GetResult()
        }
        catch {
            Write-Error -Message $_.Exception.Message
            return
        }
        if ($confirmResult.PartialSuccess) {
            Write-Warning "Partial folder removal: $($confirmResult.ConfirmedChunkCount) chunk(s) succeeded, $($confirmResult.FailedChunkCount) failed."
            foreach ($err in @($confirmResult.ChunkErrors)) {
                Write-Warning " $err"
            }
            $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null
            return
        }
        if (-not $confirmResult.Confirmed) {
            $detail = if ($confirmResult.ChunkErrors -and $confirmResult.ChunkErrors.Count -gt 0) {
                ($confirmResult.ChunkErrors -join '; ')
            } else {
                'Folder removal was not confirmed by the server.'
            }
            Write-Error -Message $detail
            return
        }

        $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null
        Write-Host ""
        Write-Host "Keeper NSF folder removal completed." -ForegroundColor Green
    }
}
New-Alias -Name nsf-rmdir -Value Remove-KeeperNSFFolder

function Remove-KeeperNSFFolders {
    <#
    .Synopsis
    Batch-remove Keeper NSF folders from JSON (up to 100 per API request).

    .Description
    Uses vault/folders/v3/remove_folder (preview/confirm). Independent of Remove-KeeperNSFFolder / nsf-rmdir.
    Larger sets are chunked automatically (100 folders per request).
    Duplicate folder_uid values in the same batch are rejected.
    owner-trash is not supported for folders.

    JSON schema: a "folders" (or "removals") array. Each item: uid / folder_uid (required),
    optional operation (folder-trash default, or delete-permanent).

    .Parameter FilePath
    Path to a UTF-8 JSON folder remove batch file.

    .Parameter Json
    Inline JSON string (same schema as -FilePath).

    .Parameter DownloadSampleFolders
    Writes a sample batch remove JSON file and exits without removing folders.

    .Parameter Force
    Skip confirmation after preview.

    .Parameter DryRun
    Preview only; do not remove folders.

    .EXAMPLE
    PS> Remove-KeeperNSFFolders -DownloadSampleFolders

    .EXAMPLE
    PS> Remove-KeeperNSFFolders -FilePath .\nsf-folders-remove-batch.sample.json

    .EXAMPLE
    PS> Remove-KeeperNSFFolders -FilePath .\nsf-folders-remove-batch.sample.json -Force
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'File')]
    Param(
        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [ValidateNotNullOrEmpty()]
        [string] $FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'Json')]
        [ValidateNotNullOrEmpty()]
        [string] $Json,

        [Parameter(Mandatory = $false, ParameterSetName = 'DownloadSample')]
        [switch] $DownloadSampleFolders,

        [Alias('f')]
        [switch] $Force,

        [switch] $DryRun
    )

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }
    catch {
        Write-Error "Not connected to Keeper. Please login first."
        return
    }

    if ($DownloadSampleFolders) {
        if (-not $FilePath) {
            $FilePath = 'nsf-folders-remove-batch.sample.json'
        }

        $fullPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath)
        $parentDir = Split-Path -Parent $fullPath
        if ($parentDir -and -not (Test-Path -LiteralPath $parentDir)) {
            New-Item -ItemType Directory -Path $parentDir -Force | Out-Null
        }

        $sampleJson = Get-KeeperNSFFolderRemoveBatchSampleJson
        $utf8NoBom = New-Object System.Text.UTF8Encoding $false
        [System.IO.File]::WriteAllText($fullPath, $sampleJson, $utf8NoBom)

        Write-Host "Sample NSF folder remove batch file written to: $fullPath" -ForegroundColor Green
        Write-Host "Replace REPLACE_WITH_FOLDER_UID_* values, then run:"
        Write-Host " Remove-KeeperNSFFolders -FilePath `"$FilePath`""
        return
    }

    if (-not $FilePath -and -not $Json) {
        Write-Error "FilePath or Json is required when -DownloadSampleFolders is not specified."
        return
    }

    try {
        $jsonText = if ($PSCmdlet.ParameterSetName -eq 'File') {
            if (-not (Test-Path -LiteralPath $FilePath)) {
                throw "File not found: $FilePath"
            }
            Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8
        }
        else {
            $Json
        }

        $specs = ConvertTo-KeeperNSFFolderRemovalSpecs -JsonText $jsonText
        if (-not $specs -or $specs.Count -eq 0) {
            throw "Folder remove file contains no folders."
        }
    }
    catch {
        Write-Host "Error parsing folder remove payload: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    $removals = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFFolderRemoval]'
    $seenUids = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
    $resolveErrors = New-Object System.Collections.Generic.List[string]
    $index = 0
    foreach ($spec in $specs) {
        [KeeperSecurity.Vault.FolderNode]$folderNode = $null
        if (-not $vault.TryResolveKeeperNSFFolder($spec.FolderUid, [ref]$folderNode)) {
            $resolveErrors.Add("Keeper NSF folder `"$($spec.FolderUid)`" was not found (index $index). Run Sync-Keeper or nsf-list first.")
            $index++
            continue
        }

        if (-not $seenUids.Add($folderNode.FolderUid)) {
            $resolveErrors.Add("Duplicate folder_uid '$($folderNode.FolderUid)' in the same request (index $index).")
            $index++
            continue
        }

        if ($spec.Operation -notin @('folder-trash', 'delete-permanent')) {
            $resolveErrors.Add("Invalid operation '$($spec.Operation)' at index $index. Use folder-trash or delete-permanent.")
            $index++
            continue
        }

        $op = switch ($spec.Operation) {
            'folder-trash' { [KeeperSecurity.Vault.KeeperNSFFolderRemoveOperation]::FolderTrash }
            'delete-permanent' { [KeeperSecurity.Vault.KeeperNSFFolderRemoveOperation]::DeletePermanent }
        }

        $removal = New-Object KeeperSecurity.Vault.KeeperNSFFolderRemoval
        $removal.FolderUid = $folderNode.FolderUid
        $removal.Operation = $op
        $removals.Add($removal)
        $index++
    }

    if ($resolveErrors.Count -gt 0) {
        foreach ($err in $resolveErrors) {
            Write-Error -Message $err
        }
        Write-Error -Message "Aborting folder remove batch: $($resolveErrors.Count) item(s) failed validation. Fix the JSON and retry."
        return
    }

    if ($removals.Count -eq 0) {
        return
    }

    $hasPermanent = @($removals | Where-Object {
            $_.Operation -eq [KeeperSecurity.Vault.KeeperNSFFolderRemoveOperation]::DeletePermanent
        }).Count -gt 0
    if ($hasPermanent -and -not $Force -and -not $DryRun) {
        Write-Host ""
        Write-Host "*** WARNING ***" -ForegroundColor Red
        Write-Host " delete-permanent is IRREVERSIBLE."
        Write-Host " All sub-folders and records inside will be permanently destroyed."
    }

    Write-Host ""
    Write-Host "=== Keeper NSF Folder Remove Preview ===" -ForegroundColor Cyan
    try {
        $previewResult = $vault.RemoveKeeperNSFFolders($removals, $true).GetAwaiter().GetResult()
    }
    catch {
        Write-Error -Message $_.Exception.Message
        return
    }
    Write-KeeperNSFRemoveImpact -Response $previewResult.PreviewResponse -ItemLabel 'Folder'

    if ($previewResult.FailedChunkCount -gt 0) {
        Write-Error -Message "Folder remove preview failed for $($previewResult.FailedChunkCount) chunk(s)."
        foreach ($err in @($previewResult.ChunkErrors)) {
            Write-Warning " $err"
        }
        return
    }

    $previewErrors = @($previewResult.PreviewResponse.Results | Where-Object {
            $_.Error -and -not [string]::IsNullOrWhiteSpace($_.Error.Message)
        })
    if ($previewErrors.Count -gt 0) {
        Write-Host ""
        Write-Host "One or more folders could not be previewed. Aborting." -ForegroundColor Yellow
        return
    }

    try {
        [KeeperSecurity.Vault.VaultOnline]::ValidateRemoveResponse($previewResult.PreviewResponse, $false)
    }
    catch {
        Write-Error -Message $_.Exception.Message
        return
    }

    if ($DryRun) {
        Write-Host ""
        Write-Host "Dry run: no folders were removed." -ForegroundColor DarkYellow
        return
    }

    if (-not $PSCmdlet.ShouldProcess("$($removals.Count) Keeper NSF folder(s)", "Remove Keeper NSF folders")) {
        return
    }

    if (-not $Force) {
        $prompt = if ($hasPermanent) {
            "Are you sure you want to permanently delete the folder(s) above? This action cannot be undone. (yes/No)"
        } else {
            "Are you sure you want to remove the folder(s) above? (yes/No)"
        }
        $confirmation = Read-Host $prompt
        if ($confirmation -notmatch '^(y|yes)$') {
            Write-Host "Remove operation cancelled"
            return
        }
    }

    if (-not $previewResult.ChunkConfirmationTokens -or $previewResult.ChunkConfirmationTokens.Count -eq 0) {
        Write-Error -Message "Preview did not return confirmation token(s)."
        return
    }

    Write-Host ""
    Write-Host "Removing $($removals.Count) Keeper NSF folder(s) in batch..." -ForegroundColor Cyan
    try {
        $confirmResult = $vault.ConfirmKeeperNSFFolders($removals, $previewResult).GetAwaiter().GetResult()
    }
    catch {
        Write-Error -Message $_.Exception.Message
        return
    }

    if ($confirmResult.PartialSuccess) {
        Write-Warning "Partial folder removal: $($confirmResult.ConfirmedChunkCount) chunk(s) succeeded, $($confirmResult.FailedChunkCount) failed."
        foreach ($err in @($confirmResult.ChunkErrors)) {
            Write-Warning " $err"
        }
        $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null
        return
    }
    if (-not $confirmResult.Confirmed) {
        $detail = if ($confirmResult.ChunkErrors -and $confirmResult.ChunkErrors.Count -gt 0) {
            ($confirmResult.ChunkErrors -join '; ')
        } else {
            'Folder removal was not confirmed by the server.'
        }
        Write-Error -Message $detail
        return
    }

    $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null
    Write-Host ""
    Write-Host "Keeper NSF folder removal completed." -ForegroundColor Green
}

New-Alias -Name nsf-rmdirs -Value Remove-KeeperNSFFolders

# Parse batch folder-remove JSON into removal specs (uid + operation, deduped).
function Script:ConvertTo-KeeperNSFFolderRemovalSpecs {
    Param(
        [Parameter(Mandatory = $true)]
        [string] $JsonText
    )

    $parsed = $JsonText | ConvertFrom-Json -ErrorAction Stop
    $items = @()
    if ($null -ne $parsed.folders) {
        $items = @($parsed.folders)
    }
    elseif ($null -ne $parsed.removals) {
        $items = @($parsed.removals)
    }
    elseif ($parsed -is [System.Array]) {
        $items = @($parsed)
    }
    else {
        throw "JSON must contain a 'folders' (or 'removals') array (or be a root array of folder remove objects)."
    }

    if ($items.Count -eq 0) {
        throw "folders must not be empty."
    }

    $list = New-Object System.Collections.Generic.List[object]
    $seen = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
    $index = 0
    foreach ($item in $items) {
        $uid = $null
        if ($item.uid) { $uid = [string]$item.uid }
        elseif ($item.folder_uid) { $uid = [string]$item.folder_uid }
        elseif ($item.FolderUid) { $uid = [string]$item.FolderUid }

        if ([string]::IsNullOrWhiteSpace($uid)) {
            throw "Folder remove item at index $index is missing required 'uid' (or folder_uid)."
        }

        $uid = $uid.Trim()
        if (-not $seen.Add($uid)) {
            throw "Duplicate folder_uid '$uid' in the same request (index $index)."
        }

        $operation = 'folder-trash'
        if ($null -ne $item.PSObject.Properties['operation'] -and $null -ne $item.operation) {
            $operation = [string]$item.operation
        }
        elseif ($null -ne $item.PSObject.Properties['Operation'] -and $null -ne $item.Operation) {
            $operation = [string]$item.Operation
        }

        $operation = $operation.Trim().ToLowerInvariant()
        if ($operation -eq 'owner-trash') {
            throw "Folder remove item at index ${index}: owner-trash (FOLDER_MOVE_TO_OWNER_TRASH) is not supported yet."
        }
        if ($operation -notin @('folder-trash', 'delete-permanent')) {
            throw "Folder remove item at index $index has invalid operation '$operation'. Use folder-trash or delete-permanent."
        }

        $list.Add([pscustomobject]@{
                FolderUid = $uid
                Operation = $operation
            }) | Out-Null
        $index++
    }

    return ,$list
}

# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB7Zr5fwHp4jfhm
# hjdUPLyZY37Ja/Lk3kzCUomdpOs7FKCCITswggWNMIIEdaADAgECAhAOmxiO+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
# oAMCAQICEAHdzU+FVN9jCMv0HhHagNUwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE
# BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy
# dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB
# MTAeFw0yNjA2MDUwMDAwMDBaFw0yNzA2MDQyMzU5NTlaMIHRMRMwEQYLKwYBBAGC
# NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ
# cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC
# VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK
# ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5
# IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCb4DRTV0sNQsa1
# 0YRh+bliabmLOVYr6S0+BSVvRJAN3SHP6x52i1Dkpki5xVDIH06ZnnsToVrgvTv+
# QxGwsn9SAPHEZ/PIJRFxbMR4ShDaptYyL4f0u4k/3HwRzIleWE4mTUonYH8BdgLw
# /F53B7wa7VTDHtxXltYTibEOwJxYCOi4Zr2FYQhjw14/CHcqS3FSMs6YYU2T56+g
# w819hQM3K0YlwTNOFoIm1v7/ZZZiJGH8uGDsvy1makh1Xyyo/wN8EbQ1nbslmePT
# roPm9w7WqiP/yiq+CZHiuTk9JK5bEgkWG3ns+v25cI251WidJx3SU7IZnX0OTd6/
# ZdKhprD5Gcfy5GBbJdcYw2WycQRW0PT5BEt55xRE0heufkpDaTUN6RdOuJdXbkl0
# hV91IZIuhueEMCk3h5mDTlU5gImxqj0R/TbAxjSSGTKCeuYFkQIRqytSabdrZZ48
# kW5hOIZMVDY1f4kpPJa8UeEvDZXT3vrtj36aSJrwez2uh4FMNlkCAwEAAaOCAgIw
# ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBT1
# SmCYU/7Yrz1fX66Ur5nSzlSYOzA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB
# BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC
# B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p
# bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI
# QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT
# QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA
# A4ICAQBcavcUHNFEg872HDRq2+hRlnvaghCXv7X/6h9HSzjAQP3rt95BZty3ASqi
# 2MYyGQLGdDl4DToe/WhajtEOBOYa83agW6tBvrfcKRrDrwJOMPTbwNYvn+GuiL4T
# CKzXaytWiJJbrc5odc7Ecat2ZvJylpPmNainr4Q0LzzH23Gea/Mm/hIJTN4IGgrH
# hrXiTIIW/ZUzrY6g8b3RZB4BA497n43wNdSqP+C3ntFw6NiGB4Z25SW4YntIxYPv
# Kf37OVhF0xqxLC1sK/XxgK0EGQ6iaj8Ncpr2C5vSNZqfW2MndxOA1W67pgDpg83k
# UWG+/YJeGhqOTF82/0kIzQXeI/lIqbnL/IJAJqSm/ROSpsGUKVbzk03cpTD55ZQX
# WjM0fLirypBqY05T8gnh1L0fSwxr/SwJZ8OddivgyK1YOMn02nnsEG5kxBt9cMX4
# JCYABhypmAVDRvyYifEVdoFWv2gAXXW+PPRvlNa6E4aMCZrVcoKHiyeMAXOi1IC9
# mHvC2+foTSMFueq3AdnYfeKnZnAiKXKRhXcdHbQYcR2A7AIzIcqahPYr4FNEgb/E
# /y/kypAkf0rMHlYl1kNqLs2Nv1UnMEHYT5YmDVLO63+1Trcw4zTZ70zuqIqeID/d
# nbOlgtyG6DSRCL7f0E7kP18f4RoX5i1PkfeO4VJHsAuCeNG1qjGCBdkwggXVAgEB
# MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD
# VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI
# QTM4NCAyMDIxIENBMQIQAd3NT4VU32MIy/QeEdqA1TANBglghkgBZQMEAgEFAKCB
# hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE
# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ
# BDEiBCA9gfgIZLt/CaQy28q8ipWYjSRc3QH5paTZVJ2ymvff7jANBgkqhkiG9w0B
# AQEFAASCAYBZJEjS7Fr36ACvoaRSHkXd0fjelXduxNXVVoKrbdfeBdje2lIYfrgq
# WRMDSIY6/wNl6i3GKKZyVke4QRTuBOnJqbZ0bnA+v8XS8FzTLankZr/+mrfO6Vh1
# jnLuwsE08TVafZyYY++zcbLjQWS4AXWIrv1NJ0J0wnqrF1DiZzGa3cAYCEkf5zdH
# uVlypcgItzJctjGnmjc3PX66y91K4ZnC5+SpNRe8VSkPC7eV3CDh06Tgyr27Ozm5
# iFDN6LJ9kq7ID7c0oyvLHAgNGubPCKBPTozWbLWaj4OmBu3fvg1P9XbyG9YsH0Ms
# Mj2eFXB3kMUcMWdUSlcfVjpsQyf5dtr8dod4pnhuEC+FxVl/vZ8FS5Evmi3sueCe
# UDhTRGBU9iPXS4ktdSPK4ew6T5JzsXnZ9cnSeaqLGq6UI2PR8U+DVJIJ/Zeh7JPi
# zCE2GuZOXNUkCfwYZIJBM2seheLH+pjWIMgMC1Txb9N0oj46wpJrMCZEY7Xx9uHZ
# /lykruUQ1m2hggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwODE0MjExOTE2WjAv
# BgkqhkiG9w0BCQQxIgQgXCg7ZMy71agQ31SjqK0EYHTeRB0lLiNy2wUVWPDyvLgw
# DQYJKoZIhvcNAQEBBQAEggIAvENgg4DOFs91095m97ybYTDIAF6VRBg5GRTTczkS
# DVwkAqksbNwb560CKsMtlN/aQAsBTE7zoCKf9vs+oBKKx6d5RBqCxILoYfisNEx1
# pt9KsISg28PxmuA6N0IAa3zF+Jsx0/i+2EbhYMq21ebqI1/t1FvV+VOGayqo8ZWi
# uwCbFhfD9aoTVIIfGSbWXEjE7FK6EvR+3wwLhcjtayJJjeqMT/wyx+IzcyefPB5j
# 5eqpFeKy09gNPGJFZiTiXg3Hv47Gjctzp+5J9OE5sJM34etvBwzicSYC4LsBUxh6
# nmbwSaEWRqk3mrqjj0kXQqYA7BEptB7YXWfiHlIZaAE1XnyGfieS1J1bvOekXFJ+
# nHwTERSH7NNmXsPq5iRnuqae/PTjoPFgH5Ff3jHnUzNtELsAAtsHVw2lz9HAe4MA
# Jy6SRxvCrtBsY/gbRQDSG4EUlwufHckFJGDXh37CQFXEvSLUy5Sqhfm1Viw1gXGF
# KOOLAw2eWSviyUOJYwwLoJ0abDMN2cHn0t1niWyElj6ZPpqr3zLE4ZyCCyDlBEmU
# cMaEhWtxHnGhDtPUoOB6NP/pWIU1WHQhi1i4Xht6HDXsKkxPIfhZ+2fIO4FOJwl4
# p8kisGWHfYGrE15vnvKIGZT5GSfwJeYHkL1+txcZQRou3d6I/9zA/1wlD2CUnm4e
# n9Q=
# SIG # End signature block