SkipSyncCommands.ps1

#requires -Version 5.1

function getKeeperAuth {
    <#
    SkipSync REST helpers only need IAuthentication — not a populated vault.
    Use this instead of getVault so commands work when vault SyncDown was skipped (-SkipSync) or the cache is empty.
    #>

    if (-not $Script:Context.Auth) {
        Write-Error -Message 'Not connected. Run Connect-Keeper first.' -ErrorAction Stop
    }
    $Script:Context.Auth
}

function __AwaitSkipSyncTask {
    param([Parameter(Mandatory)][System.Threading.Tasks.Task]$Task)
    try {
        return $Task.GetAwaiter().GetResult()
    } catch {
        $ex = $_.Exception
        while ($null -ne $ex.InnerException) {
            $ex = $ex.InnerException
        }
        throw $ex
    }
}

function __ResolveSharedFolderUidSkipSync {
    param($SharedFolder)
    if ($SharedFolder -is [Array]) {
        if ($SharedFolder.Count -ne 1) {
            throw 'Only one shared folder is expected.'
        }
        $SharedFolder = $SharedFolder[0]
    }
    $uid = $null
    if ($SharedFolder -is [string]) {
        $uid = $SharedFolder
    }
    elseif ($null -ne $SharedFolder.Uid) {
        $uid = $SharedFolder.Uid
    }
    if (-not $uid) {
        throw "Cannot resolve shared folder UID from: $SharedFolder"
    }

    $vault = $Script:Context.Vault
    if ($vault) {
        [KeeperSecurity.Vault.SharedFolder]$sf = $null
        if ($vault.TryGetSharedFolder($uid, [ref]$sf)) {
            return $sf.Uid
        }
        $sf = $vault.SharedFolders | Where-Object { $_.Name -eq $uid } | Select-Object -First 1
        if ($sf) {
            return $sf.Uid
        }
    }
    return $uid
}

function __NewSharedFolderUserOptionsSkipSync {
    param(
        [System.Nullable[bool]] $ManageRecords,
        [System.Nullable[bool]] $ManageUsers,
        [System.Nullable[DateTimeOffset]] $Expiration
    )
    $options = New-Object KeeperSecurity.Vault.SharedFolderUserOptions
    if ($null -ne $ManageRecords) {
        $options.ManageRecords = $ManageRecords
    } else {
        $options.ManageRecords = $null
    }
    if ($null -ne $ManageUsers) {
        $options.ManageUsers = $ManageUsers
    } else {
        $options.ManageUsers = $null
    }
    if ($null -ne $Expiration) {
        $options.Expiration = $Expiration
    } else {
        $options.Expiration = $null
    }
    return $options
}

function __GetSharedFolderObjectFromResponseSkipSync {
    param($GetSharedFoldersResponse, [string]$SharedFolderUid)
    if (-not $GetSharedFoldersResponse -or -not $GetSharedFoldersResponse.SharedFolders) {
        return $null
    }
    $uid = $SharedFolderUid.Trim()
    foreach ($sharedFolder in $GetSharedFoldersResponse.SharedFolders) {
        if ($sharedFolder -and [string]::Equals($sharedFolder.SharedFolderUid, $uid, [StringComparison]::OrdinalIgnoreCase)) {
            return $sharedFolder
        }
    }
    if ($GetSharedFoldersResponse.SharedFolders.Length -eq 1) {
        return $GetSharedFoldersResponse.SharedFolders[0]
    }
    $null
}

function __TestSharedFolderOwnerIsCurrentUserSkipSync {
    param($SharedFolderObject, [KeeperSecurity.Authentication.IAuthentication]$Auth)
    if (-not $SharedFolderObject -or -not $Auth.AuthContext.AccountUid) {
        return $false
    }
    $owner = $SharedFolderObject.Owner
    if ([string]::IsNullOrWhiteSpace($owner)) {
        return $false
    }
    $myUid = [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($Auth.AuthContext.AccountUid)
    [string]::Equals($owner.Trim(), $myUid.Trim(), [StringComparison]::OrdinalIgnoreCase)
}

function __WriteRecordDetailsSkipSyncResult {
    param(
        [Parameter(Mandatory = $true)][KeeperSecurity.Vault.RecordDetailsSkipSyncResult] $Result,
        [string] $EmptyMessage = 'No records in this shared folder (or folder unavailable).'
    )
    if ($Result.Records.Count -eq 0 -and $Result.FailedRecordUids.Count -eq 0 -and $Result.NoPermissionRecordUids.Count -eq 0) {
        Write-Host $EmptyMessage
        return
    }
    foreach ($record in $Result.Records) {
        $title = $record.Title
        if ([string]::IsNullOrEmpty($title)) { $title = '(no title)' }
        Write-Host " $($record.Uid): $title"
    }
    if ($Result.NoPermissionRecordUids.Count -gt 0) {
        Write-Host " No permission: $($Result.NoPermissionRecordUids -join ', ')"
    }
    if ($Result.FailedRecordUids.Count -gt 0) {
        Write-Host " Failed to decrypt: $($Result.FailedRecordUids -join ', ')"
    }
    if ($Result.InvalidRecordUids.Count -gt 0) {
        Write-Host " Invalid UID format: $($Result.InvalidRecordUids -join ', ')"
    }
}

function __GetRecordDetailsSkipSyncIncludeValue {
    param([string]$Include)
    switch ($Include) {
        'DataOnly' { 1 }
        'ShareOnly' { 2 }
        Default { 0 }
    }
}

function __GetRequestedRecordUidsMissingFromLoadedRecords {
    param(
        [string[]]$RequestedUids,
        [Parameter(Mandatory = $true)][KeeperSecurity.Vault.RecordDetailsSkipSyncResult]$Result
    )
    $loaded = @{}
    foreach ($record in $Result.Records) {
        if ($record.Uid) { $loaded[$record.Uid] = $true }
    }
    $missing = [System.Collections.ArrayList]::new()
    foreach ($uid in $RequestedUids) {
        if ([string]::IsNullOrWhiteSpace($uid)) { continue }
        $trimmedUid = $uid.Trim()
        $found = $false
        foreach ($key in $loaded.Keys) {
            if ([string]::Equals($key, $trimmedUid, [StringComparison]::OrdinalIgnoreCase)) {
                $found = $true
                break
            }
        }
        if (-not $found) {
            [void]$missing.Add($trimmedUid)
        }
    }
    @($missing)
}

function __TryFindSharedFolderUidForRecordFromVault {
    param([string]$RecordUid)
    if ([string]::IsNullOrWhiteSpace($RecordUid)) {
        return $null
    }
    $vault = $Script:Context.Vault
    if (-not $vault) {
        return $null
    }
    $trimmedRecordUid = $RecordUid.Trim()
    foreach ($sf in $vault.SharedFolders) {
        foreach ($recordPermission in $sf.RecordPermissions) {
            if ($recordPermission.RecordUid -and [string]::Equals($recordPermission.RecordUid.Trim(), $trimmedRecordUid, [StringComparison]::OrdinalIgnoreCase)) {
                return $sf.Uid
            }
        }
    }
    $null
}

function __MergeRecordDetailsSkipSyncResultsForSameRequest {
    param(
        [string[]]$RequestedUids,
        [Parameter(Mandatory = $true)][KeeperSecurity.Vault.RecordDetailsSkipSyncResult]$OwnedResult,
        [Parameter(Mandatory = $true)]$SharedFolderResults
    )
    $records = [System.Collections.Generic.List[KeeperSecurity.Vault.KeeperRecord]]::new()
    foreach ($record in $OwnedResult.Records) {
        $records.Add($record)
    }
    foreach ($sf in $SharedFolderResults) {
        foreach ($record in $sf.Records) {
            $records.Add($record)
        }
    }
    $loaded = [System.Collections.Generic.Dictionary[string, bool]]::new([StringComparer]::OrdinalIgnoreCase)
    foreach ($record in $records) {
        if ($record.Uid) {
            $loaded[$record.Uid] = $true
        }
    }
    $noPerm = [System.Collections.ArrayList]::new()
    foreach ($uid in $OwnedResult.NoPermissionRecordUids) {
        if ($uid -and -not $loaded.ContainsKey($uid)) {
            [void]$noPerm.Add($uid)
        }
    }
    foreach ($sfr in $SharedFolderResults) {
        foreach ($uid in $sfr.NoPermissionRecordUids) {
            if ($uid -and -not $loaded.ContainsKey($uid)) {
                [void]$noPerm.Add($uid)
            }
        }
    }
    $invalid = [System.Collections.ArrayList]::new()
    foreach ($uid in $OwnedResult.InvalidRecordUids) {
        if ($uid) { [void]$invalid.Add($uid) }
    }
    foreach ($sfr in $SharedFolderResults) {
        foreach ($uid in $sfr.InvalidRecordUids) {
            if ($uid) { [void]$invalid.Add($uid) }
        }
    }
    $failed = [System.Collections.ArrayList]::new()
    foreach ($uid in $RequestedUids) {
        if ([string]::IsNullOrWhiteSpace($uid)) { continue }
        $trimmedUid = $uid.Trim()
        if (-not $loaded.ContainsKey($trimmedUid)) {
            [void]$failed.Add($trimmedUid)
        }
    }
    New-Object KeeperSecurity.Vault.RecordDetailsSkipSyncResult(
        $records.ToArray(),
        [string[]]@($noPerm),
        [string[]]@($failed),
        [string[]]@($invalid))
}

function __ConvertSharedFolderObjectToDetailsView {
    param(
        [Parameter(Mandatory = $true)] $SharedFolderObject,
        [Parameter(Mandatory = $true)][KeeperSecurity.Vault.GetSharedFoldersResponse] $ApiResponse,
        [Parameter(Mandatory = $false)][switch] $IncludePermissions
    )
    if (-not $IncludePermissions) {
        $recordUids = [System.Collections.ArrayList]::new()
        foreach ($record in @($SharedFolderObject.Records)) {
            if ($record -and $record.RecordUid) { [void]$recordUids.Add($record.RecordUid) }
        }
        $userEmails = [System.Collections.ArrayList]::new()
        foreach ($uid in @($SharedFolderObject.Users)) {
            if ($uid -and $uid.Email) { [void]$userEmails.Add($uid.Email) }
        }
        $teamUids = [System.Collections.ArrayList]::new()
        foreach ($team in @($SharedFolderObject.Teams)) {
            if ($team -and $team.TeamUid) { [void]$teamUids.Add($team.TeamUid) }
        }
        return [pscustomobject][ordered]@{
            ApiResult       = $ApiResponse.result
            ApiIsSuccess    = $ApiResponse.IsSuccess
            ApiMessage      = $ApiResponse.message
            ApiCommand      = $ApiResponse.command
            SharedFolderUid = $SharedFolderObject.SharedFolderUid
            Name            = $SharedFolderObject.Name
            Owner           = $SharedFolderObject.Owner
            Revision        = $SharedFolderObject.Revision
            RecordCount     = $recordUids.Count
            UserCount       = $userEmails.Count
            TeamCount       = $teamUids.Count
            RecordUids      = @($recordUids)
            UserEmails      = @($userEmails)
            TeamUids        = @($teamUids)
        }
    }

    $recObjs = @()
    foreach ($record in @($SharedFolderObject.Records)) {
        if (-not $record) { continue }
        $recObjs += [pscustomobject]@{
            RecordUid = $record.RecordUid
            CanEdit   = $record.CanEdit
            CanShare  = $record.CanShare
        }
    }
    $userObjs = @()
    foreach ($uid in @($SharedFolderObject.Users)) {
        if (-not $uid) { continue }
        $userObjs += [pscustomobject]@{
            Email         = $uid.Email
            ManageUsers   = $uid.ManageUsers
            ManageRecords = $uid.ManageRecords
        }
    }
    $teamObjs = @()
    foreach ($team in @($SharedFolderObject.Teams)) {
        if (-not $team) { continue }
        $teamObjs += [pscustomobject]@{
            TeamUid       = $team.TeamUid
            Name          = $team.Name
            ManageUsers   = $team.ManageUsers
            ManageRecords = $team.ManageRecords
            RestrictEdit  = $team.RestrictEdit
            RestrictShare = $team.RestrictShare
        }
    }
    [pscustomobject][ordered]@{
        ApiResult     = $ApiResponse.result
        ApiIsSuccess  = $ApiResponse.IsSuccess
        ApiMessage    = $ApiResponse.message
        ApiCommand    = $ApiResponse.command
        SharedFolderUid = $SharedFolderObject.SharedFolderUid
        Name            = $SharedFolderObject.Name
        Owner           = $SharedFolderObject.Owner
        Revision        = $SharedFolderObject.Revision
        KeyType         = $SharedFolderObject.KeyType
        ManageUsers     = $SharedFolderObject.ManageUsers
        ManageRecords   = $SharedFolderObject.ManageRecords
        DefaultCanEdit  = $SharedFolderObject.DefaultCanEdit
        DefaultCanShare = $SharedFolderObject.DefaultCanShare
        DefaultManageRecords = $SharedFolderObject.DefaultManageRecords
        DefaultManageUsers   = $SharedFolderObject.DefaultManageUsers
        AccountFolder   = $SharedFolderObject.AccountFolder
        FullSync        = $SharedFolderObject.FullSync
        RecordCount     = $recObjs.Count
        UserCount       = $userObjs.Count
        TeamCount       = $teamObjs.Count
        Records         = $recObjs
        Users           = $userObjs
        Teams           = $teamObjs
    }
}

function Get-KeeperSharedFolderDetailsSkipSync {
    <#
    .SYNOPSIS
    Fetches shared folder payload from the server (get_shared_folders) without a full vault sync.

    .DESCRIPTION
    By default returns a compact PSCustomObject: RecordUids, UserEmails, and TeamUids string arrays plus basic
    folder identity (uid, name, owner, revision) and counts. Raw API objects format poorly in the console
    (e.g. SharedFolders shown as a one-line collection).
     Use Format-List or Select-Object -ExpandProperty Records/Users/Teams to inspect nested data when using -IncludePermissions.

    .PARAMETER IncludePermissions
    When set, includes per-record CanEdit/CanShare, per-user and per-team permission flags, and folder-level
    key and default-permission fields. Omit for list-only output.

    .PARAMETER PassThru
    Return the raw GetSharedFoldersResponse from the SDK.
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)][string] $SharedFolderUid,
        [Parameter()][switch] $IncludePermissions,
        [Parameter()][switch] $PassThru
    )

    $auth = getKeeperAuth
    $task = [KeeperSecurity.Vault.SharedFolderSkipSyncDown]::GetSharedFolderAsync($auth, $SharedFolderUid)
    $rs = __AwaitSkipSyncTask $task
    if (-not $rs) {
        Write-Warning "Shared folder not found or get_shared_folders returned no data for: $SharedFolderUid"
        return $null
    }
    if ($PassThru) {
        return $rs
    }
    $sf = __GetSharedFolderObjectFromResponseSkipSync $rs $SharedFolderUid
    if (-not $sf) {
        Write-Warning 'No matching shared folder in the API response.'
        return $null
    }
    __ConvertSharedFolderObjectToDetailsView -SharedFolderObject $sf -ApiResponse $rs -IncludePermissions:$IncludePermissions
}

function Get-KeeperSharedFolderRecordUidsSkipSync {
    <#
    .SYNOPSIS
    Returns record UIDs linked to a shared folder from get_shared_folders (no record bodies).

    .DESCRIPTION
    Lightweight folder membership: only the list of record UIDs from the shared-folder payload.

    Use Get-KeeperSharedFolderRecordsSkipSync when you need every record in the folder decrypted in one step.
    Use Get-KeeperRecordDetailsByUidSkipSync when you already know which record UIDs to load; it does not discover
    which records belong to a folder.
    
    This cmdlet fills that gap for "what UIDs are in this folder?" without pulling
    full details — useful for counts, logging, or fetching a subset of records by UID afterward.
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    Param(
        [Parameter(Mandatory = $true)][string] $SharedFolderUid
    )

    $auth = getKeeperAuth
    $task = [KeeperSecurity.Vault.SharedFolderSkipSyncDown]::GetRecordUidsFromSharedFolderAsync($auth, $SharedFolderUid)
    $result = __AwaitSkipSyncTask $task
    if ($null -eq $result -or $result.Count -eq 0) {
        Write-Warning "Shared folder not found or has no records: $SharedFolderUid"
        return $null
    }
    $result
}

function Get-KeeperSharedFolderRecordsSkipSync {
    <#
    .SYNOPSIS
    Lists decrypted records in a shared folder without full vault sync. Chooses decryption path automatically unless you override -Mode.

    .PARAMETER SharedFolder
    Shared folder UID (base64url), name if present in vault cache, or object with a Uid property.

    .PARAMETER Mode
    Auto: use get_shared_folders owner flag — shared-folder record keys if you are not owner, owned record keys if you are owner.
    SharedKey: always decrypt via shared-folder record keys (RecordSkipSyncDown.GetSharedFolderRecordsAsync).
    OwnedKey: always load UIDs from the folder then decrypt with per-record keys (GetOwnedRecordsAsync).

    .PARAMETER Include
    RecordDetailsInclude: DataPlusShare (default), DataOnly, or ShareOnly.

    .PARAMETER PassThru
    Return RecordDetailsSkipSyncResult instead of printing uid/title lines.
    #>

    [CmdletBinding()]
    [OutputType([KeeperSecurity.Vault.RecordDetailsSkipSyncResult])]
    Param(
        [Parameter(Mandatory = $true, Position = 0)] $SharedFolder,
        [Parameter()]
        [ValidateSet('DataPlusShare', 'DataOnly', 'ShareOnly')]
        [string] $Include = 'DataPlusShare',
        [Parameter()]
        [ValidateSet('Auto', 'SharedKey', 'OwnedKey')]
        [string] $Mode = 'Auto',
        [Parameter()][switch] $PassThru
    )

    $auth = getKeeperAuth
    $sfUid = __ResolveSharedFolderUidSkipSync $SharedFolder
    $includeVal = __GetRecordDetailsSkipSyncIncludeValue $Include

    $useOwned = $false
    if ($Mode -eq 'OwnedKey') {
        $useOwned = $true
    }
    elseif ($Mode -eq 'SharedKey') {
        $useOwned = $false
    }
    else {
        $taskFolder = [KeeperSecurity.Vault.SharedFolderSkipSyncDown]::GetSharedFolderAsync($auth, $sfUid)
        $folderRs = __AwaitSkipSyncTask $taskFolder
        $sfObj = __GetSharedFolderObjectFromResponseSkipSync $folderRs $sfUid
        $useOwned = __TestSharedFolderOwnerIsCurrentUserSkipSync $sfObj $auth
    }

    $result = if ($useOwned) {
        $taskUids = [KeeperSecurity.Vault.SharedFolderSkipSyncDown]::GetRecordUidsFromSharedFolderAsync($auth, $sfUid)
        $uids = __AwaitSkipSyncTask $taskUids
        if ($null -eq $uids) {
            $uids = [string[]]@()
        }
        $taskRec = [KeeperSecurity.Vault.RecordSkipSyncDown]::GetOwnedRecordsAsync($auth, $uids, $includeVal)
        __AwaitSkipSyncTask $taskRec
    }
    else {
        $task = [KeeperSecurity.Vault.RecordSkipSyncDown]::GetSharedFolderRecordsAsync($auth, $sfUid, $includeVal)
        __AwaitSkipSyncTask $task
    }

    if ($PassThru) {
        return $result
    }
    __WriteRecordDetailsSkipSyncResult $result
}

function Get-KeeperRecordDetailsByUidSkipSync {
    <#
    .SYNOPSIS
    Loads record details by UID via vault/get_records_details without a full vault sync.

    .DESCRIPTION
    Default -Mode Auto loads owned records first (per-record keys), then retries any missing UIDs using shared-folder
    keys from get_shared_folders. Use -SharedFolderUid when you know the folder,
    or run Sync-Keeper so the vault can map each record to a shared folder.

    .PARAMETER RecordUid
    One or more record UIDs.

    .PARAMETER SharedFolderUid
    Optional. When set, Auto mode uses this folder for the second-step shared-folder decrypt for UIDs that failed
    the owned path. SharedKey mode requires this parameter.

    .PARAMETER Mode
    Auto (default): owned decrypt first, then shared-folder decrypt for remaining UIDs.
    OwnedKey: only owned-record keys (previous behavior when SharedFolderUid was omitted).
    SharedKey: only shared-folder keys for the folder given by -SharedFolderUid.

    .PARAMETER PassThru
    Return RecordDetailsSkipSyncResult instead of printing lines.
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, Position = 0)][string[]] $RecordUid,
        [Parameter()][string] $SharedFolderUid,
        [Parameter()]
        [ValidateSet('Auto', 'OwnedKey', 'SharedKey')]
        [string] $Mode = 'Auto',
        [Parameter()]
        [ValidateSet('DataPlusShare', 'DataOnly', 'ShareOnly')]
        [string] $Include = 'DataPlusShare',
        [Parameter()][switch] $PassThru
    )

    $auth = getKeeperAuth
    $includeVal = __GetRecordDetailsSkipSyncIncludeValue -Include $Include

    if ($Mode -eq 'SharedKey') {
        if (-not $SharedFolderUid) {
            throw 'SharedKey mode requires -SharedFolderUid.'
        }
        $task = [KeeperSecurity.Vault.RecordSkipSyncDown]::GetSharedFolderRecordsAsync($auth, $SharedFolderUid.Trim(), $RecordUid, $includeVal)
        $result = __AwaitSkipSyncTask $task
    }
    elseif ($Mode -eq 'OwnedKey') {
        $task = [KeeperSecurity.Vault.RecordSkipSyncDown]::GetOwnedRecordsAsync($auth, $RecordUid, $includeVal)
        $result = __AwaitSkipSyncTask $task
    }
    else {
        $taskOwned = [KeeperSecurity.Vault.RecordSkipSyncDown]::GetOwnedRecordsAsync($auth, $RecordUid, $includeVal)
        $owned = __AwaitSkipSyncTask $taskOwned
        $needSf = __GetRequestedRecordUidsMissingFromLoadedRecords -RequestedUids $RecordUid -Result $owned
        if ($needSf.Count -eq 0) {
            $result = $owned
        }
        else {
            Write-Verbose "SkipSync: $($needSf.Count) record UID(s) not loaded with owned keys; trying shared-folder keys."
            $sfResults = [System.Collections.ArrayList]::new()
            if ($SharedFolderUid) {
                $tSf = [KeeperSecurity.Vault.RecordSkipSyncDown]::GetSharedFolderRecordsAsync(
                    $auth, $SharedFolderUid.Trim(), [string[]]$needSf, $includeVal)
                [void]$sfResults.Add((__AwaitSkipSyncTask $tSf))
            }
            else {
                $groups = @{}
                foreach ($uid in $needSf) {
                    $sfUid = __TryFindSharedFolderUidForRecordFromVault -RecordUid $uid
                    if (-not $sfUid) {
                        Write-Verbose "SkipSync: no shared folder in vault cache for record $uid; specify -SharedFolderUid or run Sync-Keeper."
                        continue
                    }
                    if (-not $groups.ContainsKey($sfUid)) {
                        $groups[$sfUid] = [System.Collections.ArrayList]::new()
                    }
                    [void]$groups[$sfUid].Add($uid)
                }
                foreach ($group in $groups.GetEnumerator()) {
                    $uids = [string[]]@($group.Value)
                    $tSf = [KeeperSecurity.Vault.RecordSkipSyncDown]::GetSharedFolderRecordsAsync($auth, $group.Key, $uids, $includeVal)
                    [void]$sfResults.Add((__AwaitSkipSyncTask $tSf))
                }
            }
            if ($sfResults.Count -eq 0) {
                if ($needSf.Count -gt 0 -and -not $SharedFolderUid) {
                    Write-Warning 'One or more records were not loaded with owned keys. They may live in a shared folder: pass -SharedFolderUid <folderUid> or run Sync-Keeper so the vault can resolve the folder.'
                }
                $result = $owned
            }
            else {
                $result = __MergeRecordDetailsSkipSyncResultsForSameRequest -RequestedUids $RecordUid -OwnedResult $owned -SharedFolderResults @($sfResults)
            }
        }
    }

    if ($PassThru) {
        return $result
    }
    __WriteRecordDetailsSkipSyncResult $result
}

function Get-KeeperAvailableTeamsSkipSync {
    <#
    .SYNOPSIS
    Lists teams available for sharing (get_available_teams). Use with SET 3 team sharing.
    #>

    [CmdletBinding()]
    Param()

    $auth = getKeeperAuth
    $task = [KeeperSecurity.Vault.SharedFolderSkipSyncDown]::GetAvailableTeamsForShareAsync($auth)
    __AwaitSkipSyncTask $task
}

function Get-KeeperTeamUidSkipSync {
    <#
    .SYNOPSIS
    Resolves a team display name to a team UID (for SET 3).
    #>

    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Mandatory = $true)][string] $TeamName
    )

    $auth = getKeeperAuth
    $task = [KeeperSecurity.Vault.SharedFolderSkipSyncDown]::GetTeamUidFromNameAsync($auth, $TeamName)
    $result = __AwaitSkipSyncTask $task
    if ([string]::IsNullOrWhiteSpace($result)) {
        Write-Warning "Team not found: $TeamName"
        return $null
    }
    $result
}


function Grant-KeeperSharedFolderUserSkipSync {
    <#
    .SYNOPSIS
    Adds or updates a user on a shared folder without a full vault sync (PutUserToSharedFolderAsync).

    .DESCRIPTION
    By default, only performs the API call. Use -ShowDetail to also list decrypted records in the folder (shared-folder key path) and write a summary.

    .PARAMETER ShowDetail
    If set, after a successful grant runs Get-KeeperSharedFolderRecordsSkipSync and writes a summary.

    .PARAMETER PassThru
    When -ShowDetail is used, returns RecordDetailsSkipSyncResult from the listing step.

    .PARAMETER ExpireIn
    Optional. Expiration offset from now: a TimeSpan, integer (minutes), or a string that parses as minutes or TimeSpan (same as Grant-KeeperRecordAccess).

    .PARAMETER ExpireAt
    Optional. Absolute expiration as ISO 8601 or RFC 1123 (e.g. "2025-05-23T08:59:11Z").
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    Param(
        [Parameter(Mandatory = $true, Position = 0)] $SharedFolder,
        [Parameter(Mandatory = $true, Position = 1)][string] $User,
        [Parameter()][System.Nullable[bool]] $ManageRecords,
        [Parameter()][System.Nullable[bool]] $ManageUsers,
        [Parameter()][System.Object] $ExpireIn,
        [Parameter()][string] $ExpireAt,
        [Parameter()][switch] $ShowDetail,
        [Parameter()][switch] $PassThru
    )

    $sfUid = __ResolveSharedFolderUidSkipSync $SharedFolder
    $email = $User.Trim()
    try {
        $expirationDto = Get-ExpirationDate -ExpireIn $ExpireIn -ExpireAt $ExpireAt
    } catch {
        Write-Error "Error: $($_.Exception.Message)" -ErrorAction Stop
        throw
    }
    $options = __NewSharedFolderUserOptionsSkipSync -ManageRecords $ManageRecords -ManageUsers $ManageUsers -Expiration $expirationDto
    $didGrant = $false
    if ($PSCmdlet.ShouldProcess("$sfUid", "Grant shared folder access to $email")) {
        $auth = getKeeperAuth
        $task = [KeeperSecurity.Vault.SharedFolderSkipSyncDown]::PutUserToSharedFolderAsync($auth, $sfUid, $email, $options)
        [void](__AwaitSkipSyncTask $task)
        Write-Host "OK: Shared folder $sfUid — user $email added or updated."
        $didGrant = $true
    }
    if ($didGrant -and $ShowDetail) {
        $listed = Get-KeeperSharedFolderRecordsSkipSync -SharedFolder $sfUid -Mode SharedKey -PassThru
        __WriteRecordDetailsSkipSyncResult $listed "No records listed on this shared folder (user $User has folder access)."
        if ($PassThru) { $listed }
    }
}

function Revoke-KeeperSharedFolderUserSkipSync {
    <#
    .SYNOPSIS
    Removes a user from a shared folder without a full vault sync (RemoveUserFromSharedFolderAsync).

    .DESCRIPTION
    By default, only performs the API call. Use -ShowDetail to also list decrypted records still in the folder.

    .PARAMETER ShowDetail
    If set, after a successful revoke runs Get-KeeperSharedFolderRecordsSkipSync and writes a summary.

    .PARAMETER PassThru
    When -ShowDetail is used, returns RecordDetailsSkipSyncResult from the listing step.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    Param(
        [Parameter(Mandatory = $true, Position = 0)] $SharedFolder,
        [Parameter(Mandatory = $true, Position = 1)][string] $User,
        [Parameter()][switch] $ShowDetail,
        [Parameter()][switch] $PassThru
    )

    $sfUid = __ResolveSharedFolderUidSkipSync $SharedFolder
    $email = $User.Trim()
    $didRevoke = $false
    if ($PSCmdlet.ShouldProcess("$sfUid", "Remove shared folder access for $email")) {
        $auth = getKeeperAuth
        $task = [KeeperSecurity.Vault.SharedFolderSkipSyncDown]::RemoveUserFromSharedFolderAsync($auth, $sfUid, $email)
        [void](__AwaitSkipSyncTask $task)
        Write-Host "OK: Shared folder $sfUid — user $email removed."
        $didRevoke = $true
    }
    if ($didRevoke -and $ShowDetail) {
        $listed = Get-KeeperSharedFolderRecordsSkipSync -SharedFolder $sfUid -Mode SharedKey -PassThru
        __WriteRecordDetailsSkipSyncResult $listed 'No records listed on this shared folder after remove.'
        if ($PassThru) { $listed }
    }
}

function Grant-KeeperSharedFolderTeamSkipSync {
    <#
    .SYNOPSIS
    Adds or updates a team on a shared folder without a full vault sync.
    Team may be a team UID (base64url) or a team name resolved via the SDK.

    .DESCRIPTION
    By default, only performs the API call. Use -ShowDetail to also list decrypted records in the folder.

    .PARAMETER ShowDetail
    If set, after a successful grant runs Get-KeeperSharedFolderRecordsSkipSync and writes a summary.

    .PARAMETER PassThru
    When -ShowDetail is used, returns RecordDetailsSkipSyncResult from the listing step.

    .PARAMETER ExpireIn
    Optional. Same semantics as Grant-KeeperRecordAccess / Grant-KeeperSharedFolderUserSkipSync.

    .PARAMETER ExpireAt
    Optional. Absolute expiration (ISO 8601 or RFC 1123).
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    Param(
        [Parameter(Mandatory = $true, Position = 0)] $SharedFolder,
        [Parameter(Mandatory = $true, Position = 1)][string] $Team,
        [Parameter()][System.Nullable[bool]] $ManageRecords,
        [Parameter()][System.Nullable[bool]] $ManageUsers,
        [Parameter()][System.Object] $ExpireIn,
        [Parameter()][string] $ExpireAt,
        [Parameter()][switch] $ShowDetail,
        [Parameter()][switch] $PassThru
    )

    $sfUid = __ResolveSharedFolderUidSkipSync $SharedFolder
    $teamKey = $Team.Trim()
    try {
        $expirationDto = Get-ExpirationDate -ExpireIn $ExpireIn -ExpireAt $ExpireAt
    } catch {
        Write-Error "Error: $($_.Exception.Message)" -ErrorAction Stop
        throw
    }
    $options = __NewSharedFolderUserOptionsSkipSync -ManageRecords $ManageRecords -ManageUsers $ManageUsers -Expiration $expirationDto
    $didGrant = $false
    if ($PSCmdlet.ShouldProcess("$sfUid", "Grant shared folder access to team $Team")) {
        $auth = getKeeperAuth
        $task = [KeeperSecurity.Vault.SharedFolderSkipSyncDown]::PutTeamToSharedFolderAsync($auth, $sfUid, $teamKey, $options)
        [void](__AwaitSkipSyncTask $task)
        Write-Host "OK: Shared folder $sfUid — team $teamKey added or updated."
        $didGrant = $true
    }
    if ($didGrant -and $ShowDetail) {
        $listed = Get-KeeperSharedFolderRecordsSkipSync -SharedFolder $sfUid -Mode SharedKey -PassThru
        __WriteRecordDetailsSkipSyncResult $listed "No records listed on this shared folder (team $Team has folder access)."
        if ($PassThru) { $listed }
    }
}

function Revoke-KeeperSharedFolderTeamSkipSync {
    <#
    .SYNOPSIS
    Removes a team from a shared folder without a full vault sync (RemoveTeamFromSharedFolderAsync).

    .DESCRIPTION
    By default, only performs the API call. Use -ShowDetail to also list decrypted records still in the folder.

    .PARAMETER ShowDetail
    If set, after a successful revoke runs Get-KeeperSharedFolderRecordsSkipSync and writes a summary.

    .PARAMETER PassThru
    When -ShowDetail is used, returns RecordDetailsSkipSyncResult from the listing step.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    Param(
        [Parameter(Mandatory = $true, Position = 0)] $SharedFolder,
        [Parameter(Mandatory = $true, Position = 1)][string] $Team,
        [Parameter()][switch] $ShowDetail,
        [Parameter()][switch] $PassThru
    )

    $sfUid = __ResolveSharedFolderUidSkipSync $SharedFolder
    $teamKey = $Team.Trim()
    $didRevoke = $false
    if ($PSCmdlet.ShouldProcess("$sfUid", "Remove shared folder access for team $Team")) {
        $auth = getKeeperAuth
        $task = [KeeperSecurity.Vault.SharedFolderSkipSyncDown]::RemoveTeamFromSharedFolderAsync($auth, $sfUid, $teamKey)
        [void](__AwaitSkipSyncTask $task)
        Write-Host "OK: Shared folder $sfUid — team $teamKey removed."
        $didRevoke = $true
    }
    if ($didRevoke -and $ShowDetail) {
        $listed = Get-KeeperSharedFolderRecordsSkipSync -SharedFolder $sfUid -Mode SharedKey -PassThru
        __WriteRecordDetailsSkipSyncResult $listed 'No records listed on this shared folder after remove.'
        if ($PassThru) { $listed }
    }
}


# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA+pNPek/QJTs22
# jh49/qzRZ44daZnGUkWSbUpsRsjSo6CCITswggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg
# MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit
# eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS
# 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM
# swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC
# Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3
# /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j
# q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5
# OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo
# 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU
# tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm
# KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP
# TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq
# hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK
# r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda
# qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+
# lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a
# brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS
# y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK
# iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb
# KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q
# xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm
# zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn
# HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w
# gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1
# c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo
# dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi
# 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg
# xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF
# cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ
# m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS
# GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1
# ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9
# MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7
# Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG
# RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6
# X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd
# BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx
# XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF
# BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
# aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo
# dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy
# bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL
# BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj
# aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0
# hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0
# F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT
# mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf
# ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE
# wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh
# OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX
# gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO
# LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG
# WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg
# AwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG
# EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0
# IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex
# MB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMx
# FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy
# NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZI
# hvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3
# zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8Tch
# TySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWj
# FDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2Uo
# yrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjP
# KHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KS
# uNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7w
# JNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vW
# doUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOg
# rY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K
# 096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCf
# gPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zy
# Me39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL
# TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG
# AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
# b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp
# Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy
# dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j
# cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB
# CwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZ
# D9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/
# ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu
# +WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4o
# bEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2h
# ECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasn
# M9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol
# /DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgY
# xQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3oc
# CVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcB
# ZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB0kwggUx
# oAMCAQICEAe0P3SLJmcoVNrErUyxTt0wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE
# BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy
# dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB
# MTAeFw0yNTEyMzEwMDAwMDBaFw0yOTAxMDIyMzU5NTlaMIHRMRMwEQYLKwYBBAGC
# NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ
# cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC
# VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK
# ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5
# IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUcNMoSVmxAi0a
# vG+StFJMNFFTUIOo3HdBZ+0gqA1XpNgUx11vB1vCZrvFsD9m5oA58tdp4gZN3LmQ
# aMvCl2ANUT7MilI02Hf1RWlygBzon6iE0GpU3lgRrwrk1dhtLpGsR6dbMKUUHprc
# vKpXk90/VN+vhzY1uik1tCTxkDCPu/AYJg7m9+tR2KqvMuYMaMLhii66eWUAGsBC
# h/uZxjkGoJF6qZ0DgFd7rW7VYljbfYSNPeZNGTDgB0J/wOsKl0mn612DTseIvAKt
# 4vra/FLFukyEyStnfQ8lWYDcLLCMCjNVrzGipmT5E2iyx7Y1RZCIpNwVogp3Ixbk
# Gbq5A/41YNOLLd4cFewyB2F037RevBCRsUODZEt1qBf7Jbu3DiYo1G+zTj9E0R1s
# FzyijcfdsTm6X5ble+yCJeGkX5XgsyPnZpyz/FX9Fr0N9pMPGWwW2PKyHEnSytXm
# 0Dxdq2P4mA4CBUxq7YoV26L2PF6QEh9BQdXTPcnLysUv7SI/a0ECAwEAAaOCAgIw
# ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRG
# 4H6CH8pvNX632bsdnrda4MtJLDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB
# BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC
# B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p
# bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI
# QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT
# QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA
# A4ICAQA1Wlq0WzJa3N6DgjgBU7nagIJBab1prPARXZreX1MOv9VjnS5o0CrfQLr6
# z3bmWHw7xT8dt6bcSwRixqvPJtv4q8Rvo80O3eUMvMxQzqmi7z1zf+HG+/3G4F+2
# IYegvPc8Ui151XCV9rjA8tvFWRLRMX0ZRxY1zfT027HMw0iYL20z44+Cky//FAnL
# iRwoNDGiRkZiHbB9YOftPAYNMG3gm1z3zOW5RdfKPrqvMuijE+dfyLIAA6Immpzu
# FMH+Wgn8NnSlot9b4YKycaqqdjd7wXDjPub/oQ7VShuCSBWj+UNOTVh0vcZGackc
# H1DLVgwp2dcKlxJiQKtkHT/T6LloY6LTe6+8wkVkr8EAv1W+q/+M1a4Ao+ykFbIA
# 2LBEmA9qdgoLtenAYIiEg+48SjMPgyBbVPE3bhL1vIqjEIxYCfdmi6wx33oYX7HB
# +bJ7zitHw4GgtpfPV8y8QRZImKmeDOKyXjQPDmQM/Eglm/Ns0GzBkVXM8h6UI34b
# WZrHz9sbLSE20m5Svmxftvw5zju+I3WsmS/stNfWlOkwU0niUgwPHaz21kjXEA5A
# g+aqv26wodqZcnGOlChoWDvSJ8KKgdOFbeAYKAMp1NY7iWV315zpGH19RipCR1NH
# 0ND8iIubk3WGNf2rzEfqlOi3h2ywqVkU6AKXHdO5JV4otSKKEDGCBdkwggXVAgEB
# MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD
# VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI
# QTM4NCAyMDIxIENBMQIQB7Q/dIsmZyhU2sStTLFO3TANBglghkgBZQMEAgEFAKCB
# hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE
# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ
# BDEiBCCYSpU+Ljv2VADsMSyhvd3IYgQ2FoLZWTo5tmlLyI74oTANBgkqhkiG9w0B
# AQEFAASCAYBniIlxzMZP2/L3amufKaF6vcMOCaJ/hfvp72FxShJ7ljEwpNkwT3EH
# UU3MkxhU61Gl8X7MXxR0LZMR/Bwmh41Y2kxeqyMpm1iQgJrY4tMam772ieXEUbKR
# AKKWuRiFPTssge5qbfJGDnr6LQk19e6Q4XK8vxFBUBSLPtxXJdheK7Yits+Efee+
# zFvSMNchPLG3NUgHK4Bg7uwvETeAf9hOjiruR6id5ZHPk6wZSjorqyUgPqPp1ZAD
# 9t+P1uVqnGVha09vtpUmMVBlO6lf9Ub/A8ktaoX/c4+IANOA/eyuv867+yavUaX7
# KOheiHcg/y1osQW0HljetG0CAZck4lOmGxHVXjzu+au7I8t/SYP5mXL4JeYQfCCe
# o5pcDS0nFE1sVuwESIqSdS216kOxuLle4EWszv45WirPanf+9Ulg8kcTRmhNAGHq
# X/aGQnK5PlpE0WmOeKPc4hyx4DZA4FDuF5FCCuPzS8LrGgR/3ugkTk4v/aPgYGuh
# M3aJWRQpyr2hggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNTA1MTgxNzA5WjAv
# BgkqhkiG9w0BCQQxIgQgdOAMEcAaxzm+xa9fzTTKRjr5XVwEy5i/dtP8j3PJZaYw
# DQYJKoZIhvcNAQEBBQAEggIAXrheGpWfDaCf7EIMtXbGE9vOaLrGhlCGZsZ9D7Ac
# CYLW74sXdhkB/av6N1fOWFzjNVs9gGUxwA7S677wBJHSRFlFu5gmmO0D7Vnkiubg
# k8QcY+JJKbj9Jkg0h6sPLY2kM0r1T+E68kDP80GfGVXao5pH2qXvr5eeGu4NKIrA
# b6t0DA8sNXhh8iIA+3PgLKIdl5LQAVM54CvPXrdPi5j0w5aKPkI8zugQb8QBOVjZ
# v94qjat1sGFnw9wAPWF91ysQ1vEW3zVutKvq5+ECDaK4WGXxE89xiFsQLV8FbJZv
# hkR5SRIQl82YYVbLcv6zB+pbeq8h+9Lmllb3GkH/aV4V9rnuRtHI7xqxQtr/2EYj
# ixGSr1o3ItKA3MJalMjUkIOSvPzFHiadckNSOt0sMowPC3Ahh9FMmMoUhFQ+qJ/j
# pYJ8jaGiNHTS/IJEekK8mliEzTibKCQS4xqm2FUVjQ/+HVVgqVpoVlDhduNrYKj9
# nZ4GrEvFknkJCQv3JDh1vPC2ike0N63Em9Qspf+3JSYUy86N2uqZ8Yy5vhWN6vs7
# 28RpyVfhhSMtBHiIx05oRZdBT/NOiYiiuIknZD1zBISGZErwojgIVVuEMgyegJkx
# 20iid+7xBqgNlL7fqmhHNFesp/gM44tUEKb1sQaH131GqsLVBkc+hQiY32ovtBfa
# BV0=
# SIG # End signature block