NsfRecordCommands.ps1

#requires -Version 5.1

function Script:ConvertTo-NSFJsonValue {
    Param([Parameter(Mandatory = $true)][AllowNull()] $InputObject)

    if ($null -eq $InputObject) { return $null }

    $base = $InputObject
    if ($InputObject -is [System.Management.Automation.PSObject] -and $null -ne $InputObject.PSObject) {
        $bo = $InputObject.PSObject.BaseObject
        if ($null -ne $bo) { $base = $bo }
    }

    if ($base -is [string] -or $base -is [System.ValueType]) {
        return $base
    }

    if ($base -is [System.Collections.IDictionary]) {
        $ht = New-Object 'System.Collections.Hashtable'
        foreach ($key in $base.Keys) {
            $ht[$key] = ConvertTo-NSFJsonValue -InputObject $base[$key]
        }
        return $ht
    }

    if ($base -is [System.Collections.IEnumerable]) {
        $list = New-Object 'System.Collections.Generic.List[object]'
        foreach ($item in $base) {
            $list.Add((ConvertTo-NSFJsonValue -InputObject $item)) | Out-Null
        }
        return $list.ToArray()
    }

    if ($null -ne $InputObject.PSObject -and $InputObject.PSObject.Properties.Count -gt 0) {
        $ht = New-Object 'System.Collections.Hashtable'
        foreach ($prop in $InputObject.PSObject.Properties) {
            $ht[$prop.Name] = ConvertTo-NSFJsonValue -InputObject $prop.Value
        }
        return $ht
    }

    return $base
}

function Script:Resolve-KeeperNSFFieldValue {
    Param(
        [Parameter(Mandatory = $true)][AllowEmptyString()][string] $RawValue
    )

    if ([string]::IsNullOrEmpty($RawValue)) { return $RawValue }

    if ($RawValue -clike '$JSON:*') {
        $jsonStr = $RawValue.Substring(6)
        if ([string]::IsNullOrEmpty($jsonStr)) {
            Write-Warning "JSON value cannot be empty. Format: `$JSON:<json_object>"
            return $RawValue
        }
        try {
            $parsed = $jsonStr | ConvertFrom-Json -ErrorAction Stop
        } catch {
            Write-Warning "Invalid JSON value: $($_.Exception.Message)"
            return $RawValue
        }
        return (ConvertTo-NSFJsonValue -InputObject $parsed)
    }

    return $RawValue
}

function Script:Parse-KeeperNSFFieldSpecs {
    Param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [string[]] $FieldSpecs
    )

    $fieldDict = New-Object 'System.Collections.Generic.Dictionary[string,object]'
    $skipped = New-Object 'System.Collections.Generic.List[string]'

    foreach ($f in $FieldSpecs) {
        if ([string]::IsNullOrWhiteSpace($f)) { continue }

        $key = $null
        $val = $null
        $eqIdx = $f.IndexOf('=')
        if ($eqIdx -gt 0) {
            $key = $f.Substring(0, $eqIdx).Trim()
            $val = $f.Substring($eqIdx + 1).Trim()
        }
        else {
            $colonIdx = $f.IndexOf(':')
            if ($colonIdx -gt 0) {
                $key = $f.Substring(0, $colonIdx).Trim()
                $val = $f.Substring($colonIdx + 1).Trim()
                if ($val.Length -ge 2 -and $val[0] -eq '"' -and $val[$val.Length - 1] -eq '"') {
                    $val = $val.Substring(1, $val.Length - 2)
                }
                Write-Host "Warning: Field '$f' uses ':' as delimiter; prefer key=value (e.g. ${key}=${val})." -ForegroundColor Yellow
            }
        }

        if ([string]::IsNullOrEmpty($key)) {
            $skipped.Add($f) | Out-Null
            Write-Host "Warning: Skipping invalid field '$f'. Expected format: key=value" -ForegroundColor Yellow
            continue
        }

        $resolved = Resolve-KeeperNSFFieldValue -RawValue $val
        if ($resolved -is [System.Management.Automation.PSObject] -and $null -ne $resolved.PSObject.BaseObject) {
            $resolved = $resolved.PSObject.BaseObject
        }
        $fieldDict[$key] = $resolved
    }

    return [PSCustomObject]@{
        Dictionary = $fieldDict
        Skipped    = $skipped
        ParsedCount = $fieldDict.Count
    }
}

function Add-KeeperNSFRecord {
    <#
    .Synopsis
    Creates a new Keeper NSF record.

    .Description
    Creates a new record in Keeper NSF using the v3 API.
    Supports setting title, type, notes, folder, and field values.

    .Parameter Title
    Title for the new record.

    .Parameter RecordType
    Record type (e.g. login, general). Defaults to 'login'.

    .Parameter FolderUid
    Optional folder UID to place the record in.
 
    .Parameter Notes
    Optional notes for the record.

    .Parameter Fields
    Optional field values as key=value pairs (e.g. login=admin password=secret url=https://example.com).

    Complex (object-typed) fields can be supplied with the $JSON:<json> indirection token,
    e.g. for a `databaseCredentials` record's `host` field:
      'host=$JSON:{"hostName":"1.2.3.4","port":"1234"}'
    NOTE: single-quote the spec so PowerShell does not parse $JSON: as a drive-qualified variable.

    .Parameter GeneratePassword
    When present, generates a random password via CryptoUtils.GeneratePassword and stores it on the
    'password' field, overriding any explicit password=... value supplied in -Fields.
#>

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

        [Parameter()]
        [string] $RecordType = 'login',

        [Parameter()]
        [string] $FolderUid,

        [Parameter()]
        [string] $Notes,

        [Parameter()]
        [switch] $GeneratePassword,

        [Parameter(ValueFromRemainingArguments = $true)]
        [string[]] $Fields
    )

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

    $fieldDict = $null
    if (($Fields -and $Fields.Count -gt 0) -or $GeneratePassword.IsPresent) {
        $fieldDict = New-Object 'System.Collections.Generic.Dictionary[string,object]'
        if ($Fields) {
            $parsed = Parse-KeeperNSFFieldSpecs -FieldSpecs $Fields
            foreach ($key in $parsed.Dictionary.Keys) {
                $fieldDict[$key] = $parsed.Dictionary[$key]
            }
        }
        if ($GeneratePassword.IsPresent) {
            $fieldDict['password'] = [KeeperSecurity.Utils.CryptoUtils]::GeneratePassword($null)
        }
    }

    try {
        $recordUid = $vault.CreateKeeperNSFRecord($Title, $RecordType, $FolderUid, $Notes, $fieldDict).GetAwaiter().GetResult()
        Write-Host "Record '$Title' created successfully (UID: $recordUid)." -ForegroundColor Green
        return $recordUid
    }
    catch {
        Write-Host "Error creating record: $($_.Exception.Message)" -ForegroundColor Red
    }
}

New-Alias -Name nsf-record-add -Value Add-KeeperNSFRecord

function Edit-KeeperNSFRecord {
    <#
    .Synopsis
    Updates an existing Keeper NSF record.

    .Description
    Updates the title, type, notes, and/or fields of a Keeper NSF record using the v3 API.
    Only specified parameters are changed; others are preserved.

    .Parameter RecordUid
    UID of the record to update.

    .Parameter Title
    New title for the record.

    .Parameter RecordType
    New record type.

    .Parameter Notes
    New notes for the record.

    .Parameter Fields
    Field values to add or update as key=value pairs (e.g. login=newuser password=newpass).

    Complex (object-typed) fields can be supplied with the $JSON:<json> indirection token,
    e.g. updating a `databaseCredentials` record's `host` field:
      nsf-record-update <UID> -RecordType databaseCredentials 'host=$JSON:{"hostName":"1.2.3.4","port":"1234"}'
    NOTE: single-quote the spec so PowerShell does not parse $JSON: as a drive-qualified variable.

    .Parameter GeneratePassword
    When present, generates a random password via CryptoUtils.GeneratePassword and stores it on the
    'password' field, overriding any explicit password=... value supplied in -Fields.
#>

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

        [Parameter()]
        [string] $Title,

        [Parameter()]
        [string] $RecordType,

        [Parameter()]
        [string] $Notes,

        [Parameter()]
        [switch] $GeneratePassword,

        [Parameter(ValueFromRemainingArguments = $true)]
        [string[]] $Fields
    )

    $hasTitle = $PSBoundParameters.ContainsKey('Title')
    $hasType  = $PSBoundParameters.ContainsKey('RecordType')
    $hasNotes = $PSBoundParameters.ContainsKey('Notes')
    $hasFields = $Fields -and $Fields.Count -gt 0

    if (-not $hasTitle -and -not $hasType -and -not $hasNotes -and -not $hasFields -and -not $GeneratePassword.IsPresent) {
        Write-Host "Error: At least one of -Title, -RecordType, -Notes, -GeneratePassword, or field values must be specified." -ForegroundColor Red
        return
    }

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

    $titleParam = if ($hasTitle) { $Title }      else { [NullString]::Value }
    $typeParam  = if ($hasType)  { $RecordType } else { [NullString]::Value }
    $notesParam = if ($hasNotes) { $Notes }      else { [NullString]::Value }

    $fieldDict = $null
    if ($hasFields -or $GeneratePassword.IsPresent) {
        $fieldDict = New-Object 'System.Collections.Generic.Dictionary[string,object]'
        if ($hasFields) {
            $parsed = Parse-KeeperNSFFieldSpecs -FieldSpecs $Fields
            foreach ($key in $parsed.Dictionary.Keys) {
                $fieldDict[$key] = $parsed.Dictionary[$key]
            }
            if ($parsed.ParsedCount -eq 0 -and -not $GeneratePassword.IsPresent) {
                Write-Host "Error: No valid field values were parsed. Use key=value (e.g. login=a12)." -ForegroundColor Red
                return
            }
        }
        if ($GeneratePassword.IsPresent) {
            $fieldDict['password'] = [KeeperSecurity.Utils.CryptoUtils]::GeneratePassword($null)
        }
    }

    try {
        [void]$vault.UpdateKeeperNSFRecord($RecordUid, $titleParam, $typeParam, $notesParam, $fieldDict).GetAwaiter().GetResult()
        Write-Host "Record '$RecordUid' updated successfully." -ForegroundColor Green
    }
    catch {
        Write-Host "Error updating record: $($_.Exception.Message)" -ForegroundColor Red
    }
}

New-Alias -Name nsf-record-update -Value Edit-KeeperNSFRecord

function Set-KeeperNSFRecordAccess {
    <#
    .Synopsis
    Grant or revoke user access to a Keeper NSF record.

    .Description
    Shares or unshares a Keeper NSF record with a user using the v3 API.
    When granting, encrypts and sends the record key to the recipient.

    .Parameter RecordUid
    UID of the record to share/unshare.

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

    .Parameter Email
    One or more user email addresses 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] $RecordUid,

        [Parameter()]
        [ValidateSet('grant', 'revoke')]
        [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.KeeperNSFRecord]$tmpRecord = $null
    if (-not $vault.TryGetKeeperNSFRecord($RecordUid, [ref]$tmpRecord)) {
        Write-Host "Error: NSF record '$RecordUid' 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.SharedFolderRecordOptions
        $shareOptions.Expiration = $expirationDto
    }

    foreach ($user in $Email) {
        try {
            if ($Action -eq 'grant') {
                [void]$vault.ShareKeeperNSFRecord($RecordUid, $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 record '$RecordUid'$expireMsg." -ForegroundColor Green
            }
            else {
                [void]$vault.UnshareKeeperNSFRecord($RecordUid, $user).GetAwaiter().GetResult()
                Write-Host "Revoked access for '$user' from record '$RecordUid'." -ForegroundColor Green
            }
        }
        catch {
            Write-Host "Error ${Action}ing access for '$user': $($_.Exception.Message)" -ForegroundColor Red
        }
    }
}

New-Alias -Name nsf-share-record -Value Set-KeeperNSFRecordAccess

function Set-KeeperNSFRecordPermission {
    <#
    .Synopsis
    Bulk grant or revoke record-level sharing permissions for records in a Keeper NSF folder.

    .Description
    Modifies sharing permissions on all records within a Keeper NSF folder using the v3 API.
    Fetches current access permissions, computes required changes, displays a plan, and
    executes the changes after confirmation.

    .Parameter FolderUid
    Folder UID or name containing the records. If omitted, operates on root-level records.

    .Parameter Action
    Action to perform: 'grant' or 'revoke'.

    .Parameter Role
    Access role: viewer, share-manager, content-manager, content-share-manager, full-manager.
    Required for grant action. For revoke, if specified, only revokes users with that role.

    .Parameter Recursive
    If specified, includes records in subfolders.

    .Parameter Force
    Skip confirmation prompt.

    .Parameter DryRun
    Show what would change without making modifications.
#>

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

        [Parameter(Mandatory = $true)]
        [ValidateSet('grant', 'revoke')]
        [string] $Action,

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

        [Parameter()]
        [switch] $Recursive,

        [Parameter()]
        [switch] $Force,

        [Parameter()]
        [switch] $DryRun
    )

    if ($Action -eq 'grant' -and -not $Role) {
        Write-Host "Error: -Role is required for grant action." -ForegroundColor Red
        return
    }

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

    if ($FolderUid) {
        [KeeperSecurity.Vault.FolderNode]$tmpFolder = $null
        if (-not $vault.TryGetKeeperNSFFolder($FolderUid, [ref]$tmpFolder)) {
            foreach ($f in $vault.KeeperNSFFolderNodes) {
                if ($f.Name -and $f.Name -ieq $FolderUid) { $FolderUid = $f.FolderUid; break }
            }
        }
    }

    $folderDisplay = if ($FolderUid) { $FolderUid } else { 'root' }
    $roleLabel = if ($Role) { "'$Role'" } else { 'all' }
    $scopeLabel = if ($Recursive.IsPresent) { 'recursively' } else { 'only' }
    Write-Host ""
    Write-Host "Request to $($Action.ToUpper()) $roleLabel permission(s) in '$folderDisplay' folder $scopeLabel" -ForegroundColor Cyan

    try {
        $permResult = $vault.UpdateKeeperNSFRecordPermissions($FolderUid, $Action, $Role, $Recursive.IsPresent, $true).GetAwaiter().GetResult()
    }
    catch {
        Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    $hasChanges = ($permResult.Grants.Count -gt 0) -or ($permResult.Revokes.Count -gt 0)

    if ($permResult.Skipped.Count -gt 0) {
        Write-Host ""
        Write-Host " SKIPPED ($($permResult.Skipped.Count)):" -ForegroundColor Yellow
        foreach ($s in $permResult.Skipped) {
            $email = if ($s.Email) { $s.Email } else { '-' }
            $curRole = if ($s.CurrentRole) { $s.CurrentRole } else { '-' }
            Write-Host " $($s.RecordUid) $email [$curRole] $($s.Message)"
        }
    }

    if ($permResult.Grants.Count -gt 0) {
        Write-Host ""
        Write-Host " PLANNED GRANTS ($($permResult.Grants.Count)):" -ForegroundColor Green
        foreach ($g in $permResult.Grants) {
            $inherited = if ($g.ChangeType -eq 'create') { ' (inherited override)' } else { '' }
            Write-Host " $($g.RecordUid) $($g.Email) $($g.CurrentRole) -> $($g.NewRole)$inherited"
        }
    }

    if ($permResult.Revokes.Count -gt 0) {
        Write-Host ""
        Write-Host " PLANNED REVOKES ($($permResult.Revokes.Count)):" -ForegroundColor Red
        foreach ($r in $permResult.Revokes) {
            Write-Host " $($r.RecordUid) $($r.Email) [$($r.CurrentRole)]"
        }
    }

    if (-not $hasChanges -and $permResult.Skipped.Count -eq 0) {
        Write-Host "No permission changes are needed." -ForegroundColor DarkYellow
        return
    }

    if ($DryRun.IsPresent) {
        Write-Host ""
        Write-Host "[Dry-run mode - no changes were made]" -ForegroundColor Yellow
        $planTotal = $permResult.Grants.Count + $permResult.Revokes.Count
        Write-Host "Summary: $planTotal planned, $($permResult.Skipped.Count) skipped" -ForegroundColor Cyan
        return
    }

    if (-not $hasChanges) {
        Write-Host ""
        Write-Host "Summary: 0 changes, $($permResult.Skipped.Count) skipped" -ForegroundColor Cyan
        return
    }

    if (-not $Force) {
        $confirmation = Read-Host "Are you sure you want to apply the above changes? (yes/No)"
        if ($confirmation -notmatch '^(y|yes)$') {
            Write-Host "Update operation cancelled"
            return
        }
    }

    try {
        $permResult = $vault.UpdateKeeperNSFRecordPermissions($FolderUid, $Action, $Role, $Recursive.IsPresent, $false).GetAwaiter().GetResult()
    }
    catch {
        Write-Host "Error executing changes: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    if ($permResult.Grants.Count -gt 0) {
        Write-Host ""
        Write-Host " GRANT ($($permResult.Grants.Count)):" -ForegroundColor Green
        foreach ($g in $permResult.Grants) {
            $statusIcon = if ($g.Success) { '[OK]' } else { '[FAIL]' }
            $statusColor = if ($g.Success) { 'Green' } else { 'Red' }
            $inherited = if ($g.ChangeType -eq 'create') { ' (inherited override)' } else { '' }
            Write-Host " $statusIcon $($g.RecordUid) $($g.Email) $($g.CurrentRole) -> $($g.NewRole)$inherited" -ForegroundColor $statusColor
            if (-not $g.Success -and $g.Message) {
                Write-Host " Error: $($g.Message)" -ForegroundColor Red
            }
        }
    }

    if ($permResult.Revokes.Count -gt 0) {
        Write-Host ""
        Write-Host " REVOKE ($($permResult.Revokes.Count)):" -ForegroundColor Red
        foreach ($r in $permResult.Revokes) {
            $statusIcon = if ($r.Success) { '[OK]' } else { '[FAIL]' }
            $statusColor = if ($r.Success) { 'Green' } else { 'Red' }
            Write-Host " $statusIcon $($r.RecordUid) $($r.Email) [$($r.CurrentRole)]" -ForegroundColor $statusColor
            if (-not $r.Success -and $r.Message) {
                Write-Host " Error: $($r.Message)" -ForegroundColor Red
            }
        }
    }

    $successCount = ($permResult.Grants | Where-Object { $_.Success }).Count + ($permResult.Revokes | Where-Object { $_.Success }).Count
    $failCount = ($permResult.Grants | Where-Object { -not $_.Success }).Count + ($permResult.Revokes | Where-Object { -not $_.Success }).Count
    Write-Host ""
    Write-Host "Summary: $successCount succeeded, $failCount failed, $($permResult.Skipped.Count) skipped" -ForegroundColor Cyan
}

New-Alias -Name nsf-record-permission -Value Set-KeeperNSFRecordPermission

function Get-KeeperNSFShortcut {
    <#
    .Synopsis
    Lists Keeper NSF records that appear in more than one folder (shortcuts).

    .Description
    Scans all Keeper NSF folder-record links and reports records that exist
    in two or more folders. Optionally filters by a specific record UID/title
    or folder UID/name.

    .Parameter Target
    Optional record UID, record title, folder UID, or folder name to filter results.

    .Parameter Format
    Output format: table (default), csv, or json.

    .Parameter Output
    Path to output file. Ignored for table format.
#>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0)]
        [string] $Target,

        [Parameter()]
        [ValidateSet('table', 'csv', 'json')]
        [string] $Format = 'table',

        [Parameter()]
        [string] $Output
    )

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

    $recordUid = $null
    $folderUid = $null

    if ($Target) {
        [KeeperSecurity.Vault.KeeperNSFRecord]$tmpRecord = $null
        if ($vault.TryGetKeeperNSFRecord($Target, [ref]$tmpRecord)) {
            $recordUid = $Target
        } else {
            [KeeperSecurity.Vault.FolderNode]$tmpFolder = $null
            if ($vault.TryGetKeeperNSFFolder($Target, [ref]$tmpFolder)) {
                $folderUid = $Target
            } else {
                $foundByTitle = $false
                foreach ($r in $vault.KeeperNSFRecordEntries) {
                    if ($r.Title -and $r.Title -ieq $Target) {
                        $recordUid = $r.RecordUid
                        $foundByTitle = $true
                        break
                    }
                }
                if (-not $foundByTitle) {
                    foreach ($f in $vault.KeeperNSFFolderNodes) {
                        if ($f.Name -and $f.Name -ieq $Target) {
                            $folderUid = $f.FolderUid
                            $foundByTitle = $true
                            break
                        }
                    }
                }
                if (-not $foundByTitle) {
                    Write-Host "Error: Target '$Target' not found as record UID, title, folder UID, or folder name." -ForegroundColor Red
                    return
                }
            }
        }
    }

    try {
        $entries = $vault.GetKeeperNSFShortcuts($recordUid, $folderUid)
    } catch {
        Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    if ($entries.Count -eq 0) {
        Write-Host "No shortcut records found." -ForegroundColor DarkYellow
        return
    }

    if ($Format -eq 'json') {
        $jsonItems = @()
        foreach ($e in $entries) {
            $folders = @()
            foreach ($f in $e.Folders) {
                $folders += [ordered]@{ folder_uid = $f.FolderUid; name = $f.Name }
            }
            $jsonItems += [ordered]@{
                record_uid   = $e.RecordUid
                record_title = $e.Title
                folders      = $folders
            }
        }
        $jsonText = $jsonItems | ConvertTo-Json -Depth 4
        if ($Output) {
            $jsonText | Out-File -FilePath $Output -Encoding utf8
            Write-Host "JSON output written to '$Output' ($($entries.Count) shortcuts)." -ForegroundColor Green
        } else {
            $jsonText
        }
        return
    }

    if ($Format -eq 'csv') {
        $csvData = @()
        foreach ($e in $entries) {
            $folderNames = ($e.Folders | ForEach-Object { $_.Name }) -join '; '
            $folderUids  = ($e.Folders | ForEach-Object { $_.FolderUid }) -join '; '
            $csvData += [PSCustomObject]@{
                RecordUid   = $e.RecordUid
                Title       = $e.Title
                FolderCount = $e.Folders.Count
                FolderUids  = $folderUids
                FolderNames = $folderNames
            }
        }
        if ($Output) {
            $csvData | Export-Csv -Path $Output -NoTypeInformation -Encoding utf8
            Write-Host "CSV output written to '$Output' ($($entries.Count) shortcuts)." -ForegroundColor Green
        } else {
            $csvData | ConvertTo-Csv -NoTypeInformation
        }
        return
    }

    Write-Host ""
    Write-Host " Shortcut Records ($($entries.Count)):" -ForegroundColor Cyan
    Write-Host ""
    foreach ($e in $entries) {
        Write-Host " $($e.RecordUid) $($e.Title) [$($e.Folders.Count) folders]" -ForegroundColor White
        foreach ($f in $e.Folders) {
            Write-Host " - $($f.Name) ($($f.FolderUid))"
        }
    }
    Write-Host ""
}

New-Alias -Name nsf-shortcut-list -Value Get-KeeperNSFShortcut

function Set-KeeperNSFShortcutKeep {
    <#
    .Synopsis
    Keep a Keeper NSF record in one folder and remove it from all others.

    .Description
    For a record that appears in multiple Keeper NSF folders (a shortcut),
    keeps it in the specified folder and unlinks it from all other folders.

    .Parameter RecordUid
    Record UID or title of the record.

    .Parameter FolderUid
    Folder UID or folder name to keep the record in.

    .Parameter Force
    Skip confirmation prompt.
#>

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

        [Parameter(Position = 1, Mandatory = $true)]
        [string] $FolderUid,

        [Parameter()]
        [switch] $Force
    )

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

    [KeeperSecurity.Vault.KeeperNSFRecord]$tmpRecord = $null
    if (-not $vault.TryGetKeeperNSFRecord($RecordUid, [ref]$tmpRecord)) {
        $found = $false
        foreach ($r in $vault.KeeperNSFRecordEntries) {
            if ($r.Title -and $r.Title -ieq $RecordUid) {
                $RecordUid = $r.RecordUid
                $found = $true
                break
            }
        }
        if (-not $found) {
            Write-Host "Error: Record '$RecordUid' not found." -ForegroundColor Red
            return
        }
    }

    [KeeperSecurity.Vault.FolderNode]$tmpFolder = $null
    if (-not $vault.TryGetKeeperNSFFolder($FolderUid, [ref]$tmpFolder)) {
        $found = $false
        foreach ($f in $vault.KeeperNSFFolderNodes) {
            if ($f.Name -and $f.Name -ieq $FolderUid) {
                $FolderUid = $f.FolderUid
                $found = $true
                break
            }
        }
        if (-not $found) {
            Write-Host "Error: Folder '$FolderUid' not found." -ForegroundColor Red
            return
        }
    }

    $shortcuts = $vault.GetKeeperNSFShortcuts($RecordUid, $null)
    if ($shortcuts.Count -eq 0) {
        Write-Host "Record '$RecordUid' does not appear in multiple folders." -ForegroundColor DarkYellow
        return
    }

    $entry = $shortcuts[0]
    $keepFolder = $entry.Folders | Where-Object { $_.FolderUid -eq $FolderUid }
    if (-not $keepFolder) {
        Write-Host "Error: Record '$RecordUid' is not in folder '$FolderUid'." -ForegroundColor Red
        return
    }

    $removeFolders = $entry.Folders | Where-Object { $_.FolderUid -ne $FolderUid }

    if (-not $Force) {
        Write-Host ""
        Write-Host " Will remove record '$($entry.Title)' ($RecordUid) from:" -ForegroundColor Yellow
        foreach ($rf in $removeFolders) {
            Write-Host " - $($rf.Name) ($($rf.FolderUid))"
        }
        Write-Host " Keeping in: $($keepFolder.Name) ($($keepFolder.FolderUid))" -ForegroundColor Green
        Write-Host ""
        $confirmation = Read-Host "Are you sure you want to keep the record only in '$($keepFolder.Name)' and remove it from the other folder(s) above? (yes/No)"
        if ($confirmation -notmatch '^(y|yes)$') {
            Write-Host "Shortcut-keep operation cancelled"
            return
        }
    }

    try {
        $result = $vault.KeepKeeperNSFRecordInFolder($RecordUid, $FolderUid).GetAwaiter().GetResult()
    } catch {
        Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    $successCount = ($result.Removals | Where-Object { $_.Success }).Count
    $failCount = ($result.Removals | Where-Object { -not $_.Success }).Count

    foreach ($removal in $result.Removals) {
        if ($removal.Success) {
            Write-Host " [OK] Removed from '$($removal.FolderName)' ($($removal.FolderUid))" -ForegroundColor Green
        } else {
            Write-Host " [FAIL] '$($removal.FolderName)' ($($removal.FolderUid)): $($removal.Message)" -ForegroundColor Red
        }
    }

    Write-Host ""
    Write-Host "Record kept in '$($result.KeptFolderName)'. $successCount removed, $failCount failed." -ForegroundColor Cyan
}

New-Alias -Name nsf-shortcut-keep -Value Set-KeeperNSFShortcutKeep

function Remove-KeeperNSFRecord {
    <#
    .Synopsis
    Removes one or more Keeper NSF records (Keeper NSF v3 API).

    .Parameter Record
    One or more record UIDs or titles.

    .Parameter Folder
    Folder UID or name that provides context (required for unlink).

    .Parameter Operation
    Removal operation: owner-trash, folder-trash, or unlink.

    .Parameter Force
    Skip confirmation after preview.

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

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

        [string] $Folder,

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

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

        [switch] $DryRun
    )

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

        if ($Folder) {
            [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
            }
            $folderHint = $Folder
        }
        elseif ($Operation -ne 'owner-trash' -and $Script:Context.CurrentFolder) {
            $folderHint = $Script:Context.CurrentFolder
        }

        $op = switch ($Operation) {
            'owner-trash' { [KeeperSecurity.Vault.KeeperNSFRecordRemoveOperation]::OwnerTrash }
            'folder-trash' { [KeeperSecurity.Vault.KeeperNSFRecordRemoveOperation]::FolderTrash }
            'unlink' { [KeeperSecurity.Vault.KeeperNSFRecordRemoveOperation]::Unlink }
        }

        if ($op -eq [KeeperSecurity.Vault.KeeperNSFRecordRemoveOperation]::Unlink -and [string]::IsNullOrWhiteSpace($folderHint)) {
            Write-Error -Message "Folder context is required for unlink. Use -Folder or cd into a Keeper NSF folder."
            return
        }
    }

    process {
        foreach ($name in $Record) {
            [KeeperSecurity.Vault.KeeperNSFRecord]$kdRecord = $null
            if (-not $vault.TryResolveKeeperNSFRecord($name, [ref]$kdRecord)) {
                Write-Error -Message "Keeper NSF record `"$name`" was not found. Run Sync-Keeper or nsf-list first."
                continue
            }

            [string]$folderUid = $null
            if (-not $vault.TryResolveKeeperNSFRecordRemovalFolder($kdRecord.RecordUid, $folderHint, $op, [ref]$folderUid)) {
                if ($Folder) {
                    Write-Error -Message "Keeper NSF folder `"$Folder`" was not found. Run Sync-Keeper or nsf-list first."
                }
                else {
                    Write-Error -Message "No folder context for record `"$name`". Use -Folder or -Operation owner-trash."
                }
                continue
            }

            $removal = New-Object KeeperSecurity.Vault.KeeperNSFRecordRemoval
            $removal.RecordUid = $kdRecord.RecordUid
            $removal.FolderUid = $folderUid
            $removal.Operation = $op
            $removals.Add($removal)
        }
    }

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

        Write-Host ""
        Write-Host "=== Keeper NSF Remove Preview ===" -ForegroundColor Cyan
        $previewResult = $vault.RemoveKeeperNSFRecords($removals, $true).GetAwaiter().GetResult()
        Write-KeeperNSFRemoveImpact -Response $previewResult.PreviewResponse

        $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 records 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 records were removed." -ForegroundColor DarkYellow
            return
        }

        if (-not $Force) {
            $prompt = if ($Operation -eq 'owner-trash') {
                "Are you sure you want to move the record(s) above to your trash? (yes/No)"
            } elseif ($Operation -eq 'folder-trash') {
                "Are you sure you want to move the record(s) above to folder trash? (yes/No)"
            } else {
                "Are you sure you want to unlink the record(s) above from the folder? (yes/No)"
            }
            $confirmation = Read-Host $prompt
            if ($confirmation -notmatch '^(y|yes)$') {
                Write-Host "Remove operation cancelled"
                return
            }
        }

        if ($previewResult.PreviewResponse.ConfirmationToken.IsEmpty) {
            Write-Error -Message "Preview did not return a confirmation token."
            return
        }

        Write-Host ""
        Write-Host "Removing records..." -ForegroundColor Cyan
        $confirmResult = $vault.RemoveKeeperNSFRecords($removals, $false).GetAwaiter().GetResult()
        if (-not $confirmResult.Confirmed) {
            Write-Error -Message "Record removal was not confirmed by the server."
            return
        }

        $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null
        Write-Host ""
        Write-Host "Keeper NSF record removal completed." -ForegroundColor Green
    }
}
New-Alias -Name nsf-rm -Value Remove-KeeperNSFRecord
function Link-KeeperNSFRecord {
    <#
    .Synopsis
    Links a Keeper NSF record into a Keeper NSF folder (Keeper NSF v3 API).

    .Parameter Record
    Record UID or title.

    .Parameter Folder
    Destination folder UID, name, or "/" for Keeper NSF root.
#>

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

        [Parameter(Position = 1, Mandatory = $true)]
        [string] $Folder
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault

    [KeeperSecurity.Vault.KeeperNSFRecord]$kdRecord = $null
    if (-not $vault.TryResolveKeeperNSFRecord($Record, [ref]$kdRecord)) {
        Write-Error -Message "Keeper NSF record `"$Record`" was not found. Run Sync-Keeper or nsf-list first."
        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
    }

    $folderLabel = if ([string]::IsNullOrEmpty($folderNode.FolderUid)) { 'root' } else { "$($folderNode.Name) ($($folderNode.FolderUid))" }
    $target = "$($kdRecord.RecordUid) -> $folderLabel"
    if (-not $PSCmdlet.ShouldProcess($target, "Link Keeper NSF record into folder")) {
        return
    }

    try {
        $result = $vault.LinkKeeperNSFRecordToFolder($Record, $Folder).GetAwaiter().GetResult()
        [KeeperSecurity.Vault.VaultOnline]::ValidateFolderRecordUpdateResult($result)
    }
    catch {
        Write-Error -Message $_.Exception.Message
        return
    }

    $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null
    Write-Host "Keeper NSF record linked into folder." -ForegroundColor Green
}
New-Alias -Name nsf-ln -Value Link-KeeperNSFRecord

function Transfer-KeeperNSFRecordOwnership {
    <#
    .Synopsis
    Transfers ownership of one or more Keeper NSF records to another user (Keeper NSF v3 API).

    .Description
    Positional arguments: one or more record UIDs or titles, then the new owner's email address.
    After a successful transfer you will no longer have access to the record(s).

    .Example
    nsf-transfer-record rvwIBG_ban2VTH64OsnzLn alice@example.com

    .Example
    nsf-transfer-record rec1 rec2 rec3 alice@example.com
#>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Default')]
    Param(
        [Parameter(ValueFromRemainingArguments = $true, Mandatory = $true)]
        [string[]] $ArgumentList,

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

    if ($ArgumentList.Count -lt 2) {
        Write-Error -Message "Usage: nsf-transfer-record RECORD [RECORD...] NEW_OWNER_EMAIL"
        return
    }

    $newOwnerEmail = $ArgumentList[-1]
    $recordArgs = @($ArgumentList[0..($ArgumentList.Count - 2)])

    if ($recordArgs.Count -eq 0 -or [string]::IsNullOrWhiteSpace($newOwnerEmail)) {
        Write-Error -Message "Record UID(s) and new owner email are required."
        return
    }

    if ($newOwnerEmail -notmatch '@') {
        Write-Error -Message "New owner must be an email address: `"$newOwnerEmail`""
        return
    }

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    $resolvedRecords = New-Object 'System.Collections.Generic.List[string]'

    foreach ($name in $recordArgs) {
        [KeeperSecurity.Vault.KeeperNSFRecord]$kdRecord = $null
        if (-not $vault.TryResolveKeeperNSFRecord($name, [ref]$kdRecord)) {
            Write-Error -Message "Keeper NSF record `"$name`" was not found. Run Sync-Keeper or nsf-list first."
            return
        }
        $resolvedRecords.Add($kdRecord.RecordUid)
    }

    if (-not $Force) {
        Write-Host ""
        Write-Host "*** WARNING ***" -ForegroundColor Yellow
        Write-Host "After ownership is transferred you will lose owner rights on the record(s)."
        Write-Host "You may still see the record(s) if you retain access via a shared folder or admin role; otherwise they will disappear after sync."
        Write-Host "Make sure the new owner is correct before continuing."
        Write-Host ""
        $confirmation = Read-Host "Are you sure you want to transfer ownership to '$newOwnerEmail'? This action cannot be undone. (yes/No)"
        if ($confirmation -notmatch '^(y|yes)$') {
            Write-Host "Transfer operation cancelled"
            return
        }
    }

    try {
        $results = $vault.TransferKeeperNSFRecordOwnership($resolvedRecords, $newOwnerEmail).GetAwaiter().GetResult()
        [KeeperSecurity.Vault.VaultOnline]::ValidateKeeperNSFTransferResults($results)

        foreach ($result in $results) {
            Write-Host "Record '$($result.RecordUid)' ownership transferred to $($result.Username)." -ForegroundColor Green
            Write-Host "You no longer own this record. Run Sync-Keeper to refresh; it will remain visible only if you retain access via a shared folder or admin role." -ForegroundColor Yellow
        }
    }
    catch {
        Write-Error -Message $_.Exception.Message
        return
    }

    $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null
}
New-Alias -Name nsf-transfer-record -Value Transfer-KeeperNSFRecordOwnership

# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDMzv3inyfLwyrj
# 9PiMyIf4AHcEg9qq+naIrUvyY686zKCCITswggWNMIIEdaADAgECAhAOmxiO+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
# BDEiBCDVAqaTz8AGxgGkU06GvI3D3Jw8WTLFR4VW9FdvFbqgqDANBgkqhkiG9w0B
# AQEFAASCAYATjZ9F51DxERNlNWrxncPaXf0flf1tf1BaiF1SDKE/lxi4TRfMuCJ+
# 0NEngZoXZ25iWEJjDsf9ST3nTJYf7ypciwu3p8B8ILhSUvyZYEP24nO3RFice/Ue
# lM866ynLq1AAcjep7gc8LWV0LgUz0KVm2CznunVmAcsj2TuSm32MHzis/5eBQ8Pu
# oMp0p31k3bj1oOL04fHKOkQYtl747vmlb9IWIK9070gjSujlQa4uDmbDCBGO0KLt
# /h8xovaQoO5tALwZkB2QEh3izyw9FJYqZ2ccPN3d5uYixkLSKOtX55ZF9IfEgQwL
# U2PZUrOIlKFZpaKX223jlDFevS2OT01J+2rqIlSOdJi3+aeyaN5kBJNtH8Ic+4om
# 9vRqkvyXs4ECxbzM9jXOP4vj9pGpjqoDvSwySMNSSh7mKBJQg126F/8NfN/7UVAe
# 2lVWZNJCSfq3FECN3tDOmOA8Ak3pgmks4fB+U8vR7/dD5iTjpbXMfwvNKqoBYRsn
# TePbH7wz2MChggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNjEzMDA1NjMxWjAv
# BgkqhkiG9w0BCQQxIgQgJodudyDiPi6npeKjWtv6//glNgT3/+uUavkSjOkU1QQw
# DQYJKoZIhvcNAQEBBQAEggIAgkXul1H3ba1mofrsSEqW6og2VL4kv4GeNpka4Mft
# wQ0sCD0SHLVKkg7+rZGz/3XzQj2I7QUXoWpJS+hga3D0JQh/PRdFP0qA6cTwVgeD
# iGhb8gU5g9vNH0w1/FPODPI8ID/DKF1kb5auldHz9MSMZ/s/E3ttVlquM2vPJKkK
# haS35f76uXNajmTjHeIo66DpRBYCFUV3ooWjV0ikja720kiVUNwmqL59nYqBE5KJ
# 140tQIMiz+5JclMHcLGHX4NM1yVjdlpQ+5KxE3tktx9/49S+mTiVFpaYmd64Nn6r
# svYdainZvE38wJ0iKEVakjmDRsIZzelOAHRbLoFffh2ABZzctQoUQbsF/Tqy7e9b
# DtO1PkrwV2r5FMr+MMWKXiYol6cIKSyKhGWnyuD2nj4xl5aLBJ/1Cr0gObGmIvv0
# oNWesKaCfjn359gzWd8ArZarkf5UWwAnNcbSJJHBLr//rlHzwlKKX3Tgy3/NU8DI
# 6OByYDY+/4W2/DLIMnxm+QdIEp/r+iHinq3J5zPwjlk+f3iXgAkKtY8+LL4MwEoZ
# dW5G7rrF1XqvMp/i4y1L+6seTqOTPfiKC/jsmhT1FcC2wwnWq/E1UlWPIqyLvAYg
# piOQLVtaZ6DSHUFxj9d6NkdLImdrv205IxW+AO7rshHjujevFParDIf6zoBZnFbd
# /RY=
# SIG # End signature block