EnterpriseTeam.ps1

function New-KeeperEnterpriseTeam {
    <#
        .SYNOPSIS
        Create an enterprise team

        .DESCRIPTION
        Creates a new Keeper Enterprise Team under the specified parent node.

        .PARAMETER TeamName
        Team name (required)

        .PARAMETER ParentNode
        Parent node name or ID. Defaults to the enterprise root node.

        .PARAMETER RestrictView
        Disable password viewing/copying for team members.

        .PARAMETER RestrictEdit
        Disable record editing for team members.

        .PARAMETER RestrictShare
        Disable record re-sharing for team members.

        .EXAMPLE
        New-KeeperEnterpriseTeam -TeamName "Backend Team"
        Creates a team under the enterprise root node.

        .EXAMPLE
        New-KeeperEnterpriseTeam -TeamName "Support" -ParentNode 12345 -RestrictView
        Creates a team under node 12345 with restrict view enabled.

        .EXAMPLE
        keta "Frontend Team" -ParentNode "Engineering" -RestrictEdit -RestrictShare
        Creates a team using the keta alias with restrict edit and share enabled.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)][string]$TeamName,
        [Parameter()][string] $ParentNode,  
        [Parameter()][Switch] $RestrictView,
        [Parameter()][Switch] $RestrictEdit,
        [Parameter()][Switch] $RestrictShare
    )

    [Enterprise]$enterprise = getEnterprise

    $team = New-Object Keepersecurity.Enterprise.EnterpriseTeam
    $team.Name = $TeamName
    [KeeperSecurity.Enterprise.EnterpriseNode] $parent = $null
    if ($ParentNode) {
        $parent = resolveSingleNode $ParentNode
        $team.ParentNodeId = $parent.Id
    } else {
        $team.ParentNodeId = $enterprise.enterpriseData.RootNode.Id
    }
    if ($RestrictView.IsPresent) {
        $team.RestrictView = $true
    }
    if ($RestrictEdit.IsPresent) {
        $team.RestrictEdit = $true
    }
    if ($RestrictShare.IsPresent) {
        $team.RestrictSharing = $true
    }

    $t = $enterprise.enterpriseData.CreateTeam($team).GetAwaiter().GetResult()
    $t
}
New-Alias -Name keta -Value New-KeeperEnterpriseTeam

function Update-KeeperEnterpriseTeam {
    <#
        .SYNOPSIS
        Updates an existing enterprise team

        .DESCRIPTION
        Updates enterprise team properties including name, parent node, and restrict flags.
        Only specified parameters are changed; omitted parameters retain their current values.

        .PARAMETER Team
        Team UID or team name to update.

        .PARAMETER TeamName
        New team display name.

        .PARAMETER ParentNode
        Parent node name or ID to move the team under.

        .PARAMETER RestrictView
        Disable or enable password viewing/copying: ON or OFF.

        .PARAMETER RestrictEdit
        Disable or enable record editing: ON or OFF.

        .PARAMETER RestrictShare
        Disable or enable record re-sharing: ON or OFF.

        .EXAMPLE
        Update-KeeperEnterpriseTeam -Team "Developers" -RestrictEdit ON
        Enables restrict edit for the Developers team.

        .EXAMPLE
        Update-KeeperEnterpriseTeam -Team "1P7A8XZ9K3J9H" -RestrictShare OFF -RestrictView ON
        Updates restrict flags for a team identified by UID.

        .EXAMPLE
        Update-KeeperEnterpriseTeam -Team "Developers" -TeamName "Engineering" -ParentNode "IT" -RestrictShare OFF
        Renames the team, moves it to the IT node, and disables restrict share.

        .EXAMPLE
        kete "Developers" -RestrictEdit ON
        Updates a team using the kete alias.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)][string] $Team,
        [Parameter()][string] $TeamName,
        [Parameter()][string] $ParentNode,
        [Parameter()][ValidateSet('ON', 'OFF')][string] $RestrictView,
        [Parameter()][ValidateSet('ON', 'OFF')][string] $RestrictEdit,
        [Parameter()][ValidateSet('ON', 'OFF')][string] $RestrictShare
    )

    [Enterprise]$enterprise = getEnterprise
    $teamObject = resolveTeam $enterprise.enterpriseData $Team
    if (-not $teamObject) {
        return
    }

    if ($PSBoundParameters.ContainsKey('TeamName')) {
        if ([string]::IsNullOrWhiteSpace($TeamName)) {
            Write-Error "TeamName cannot be empty." -ErrorAction Stop
        }
        $teamObject.Name = $TeamName.Trim()
    }
    if ($PSBoundParameters.ContainsKey('ParentNode')) {
        if ([string]::IsNullOrWhiteSpace($ParentNode)) {
            Write-Error "ParentNode cannot be empty." -ErrorAction Stop
        }
        $parent = resolveSingleNode $ParentNode
        $teamObject.ParentNodeId = $parent.Id
    }
    if ($PSBoundParameters.ContainsKey('RestrictView')) {
        $teamObject.RestrictView = ($RestrictView -eq 'ON')
    }
    if ($PSBoundParameters.ContainsKey('RestrictEdit')) {
        $teamObject.RestrictEdit = ($RestrictEdit -eq 'ON')
    }
    if ($PSBoundParameters.ContainsKey('RestrictShare')) {
        $teamObject.RestrictSharing = ($RestrictShare -eq 'ON')
    }

    $updateFields = @('TeamName', 'ParentNode', 'RestrictView', 'RestrictEdit', 'RestrictShare') |
        Where-Object { $PSBoundParameters.ContainsKey($_) }
    if ($updateFields.Count -eq 0) {
        Write-Warning "No changes specified. Use -TeamName, -ParentNode, or restrict flags to update the team."
        return
    }

    try {
        $updated = $enterprise.enterpriseData.UpdateTeam($teamObject).GetAwaiter().GetResult()
        Write-Output "Team `"$($updated.Name)`" updated."
        return $updated
    }
    catch {
        Write-Error "Failed to update team `"$($teamObject.Name)`": $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Update-KeeperEnterpriseTeam -ParameterName Team -ScriptBlock $Keeper_TeamNameCompleter
New-Alias -Name kete -Value Update-KeeperEnterpriseTeam

function Remove-KeeperEnterpriseTeam {
    <#
        .SYNOPSIS
        Deletes an enterprise team

        .DESCRIPTION
        Permanently deletes an enterprise team by name or UID. This operation cannot be undone.
        Use -Force to skip the confirmation prompt.

        .PARAMETER Team
        Team UID or team name to delete.

        .PARAMETER Force
        Do not prompt for confirmation before deleting the team.

        .EXAMPLE
        Remove-KeeperEnterpriseTeam -Team "Old Team"
        Deletes the team after confirmation.

        .EXAMPLE
        Remove-KeeperEnterpriseTeam -Team "1P7A8XZ9K3J9H" -Force
        Deletes the team identified by UID without prompting for confirmation.

        .EXAMPLE
        ketdel "Old Team" -Force
        Deletes a team using the ketdel alias without confirmation.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    Param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)][string] $Team,
        [Parameter()][switch] $Force
    )

    [Enterprise]$enterprise = getEnterprise
    $teamObject = resolveTeam $enterprise.enterpriseData $Team
    if (-not $teamObject) {
        return
    }

    $teamName = $teamObject.Name
    $teamUid = $teamObject.Uid

    if (-not $Force -and -not $PSCmdlet.ShouldProcess($teamName, "Delete Enterprise Team")) {
        return
    }

    try {
        $enterprise.enterpriseData.DeleteTeam($teamUid).GetAwaiter().GetResult() | Out-Null
        Write-Output "Team `"$teamName`" deleted successfully."
    }
    catch {
        Write-Error "Failed to delete team `"$teamName`": $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseTeam -ParameterName Team -ScriptBlock $Keeper_TeamNameCompleter
New-Alias -Name ketdel -Value Remove-KeeperEnterpriseTeam

function Get-KeeperEnterpriseTeamUser {
    <#
        .Synopsis
        Get a list of enterprise users for team
    #>

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

    [Enterprise]$enterprise = getEnterprise
    $enterpriseData = $enterprise.enterpriseData
    $uid = $null

    if ($Team -is [String]) {
        $uids = Get-EnterpriseTeam | Where-Object { $_.Uid -ceq $Team -or $_.Name -ieq $Team } | Select-Object -Property Uid
        if ($uids.Length -gt 1) {
            Write-Error -Message "Team name `"$Team`" is not unique. Use Team UID" -ErrorAction Stop
        }

        if ($null -ne $uids.Uid) {
            $uid = $uids.Uid
        }
    }
    elseif ($null -ne $Team.Uid) {
        $uid = $Team.Uid
    }
    if ($uid) {
        $team = $null
        if ($enterpriseData.TryGetTeam($uid, [ref]$team)) {
            foreach ($memberUserId in (Get-EnterpriseDistinctUserIdsForTeam -EnterpriseData $enterpriseData -TeamUid $uid)) {
                $user = $null
                if ($enterpriseData.TryGetUserById($memberUserId, [ref]$user)) {
                    $user
                }
            }
        }
        else {
            Write-Error -Message "Team `"$uid`" not found" -ErrorAction Stop
        }
    }
    else {
        Write-Error -Message "Team `"$Team`" not found" -ErrorAction Stop
    }
}
New-Alias -Name ketu -Value Get-KeeperEnterpriseTeamUser
Register-ArgumentCompleter -CommandName Get-KeeperEnterpriseTeamUser -ParameterName Team -ScriptBlock $Keeper_TeamNameCompleter

function Add-KeeperEnterpriseTeamMember {
    <#
        .SYNOPSIS
        Adds existing enterprise users to a Keeper team.

        .DESCRIPTION
        Adds one or more users by email (-Emails) or by email / enterprise user ID.
        At least one of -Emails or -User must be specified. Use -HideSharedFolders (on/off) for team
        admin types. Inactive users are queued to the team.

        If the user is already on the team, a warning is shown and no duplicate membership is created.
        When -HideSharedFolders is also specified, the team admin type is updated for that existing member.

        .PARAMETER Team
        Team UID or Team Name.

        .PARAMETER Emails
        One or more user email addresses to add to the team.

        .PARAMETER Users
        One or more user email addresses or enterprise user IDs to add to the team.
        -User is an alias for this parameter. Use -Emails, -User, or both together.

        .PARAMETER HideSharedFolders
        on = Admin Only (no shared folders). off = Admin with shared folder access.
        Applies when adding active users. Not applied when queueing inactive users
        (set admin type after the user is active).

        .EXAMPLE
        Add-KeeperEnterpriseTeamMember -Team "Engineering" -Emails "alice@example.com", "bob@example.com"

        .EXAMPLE
        Add-KeeperEnterpriseTeamMember -Team "Engineering" -User "user@example.com"

        .EXAMPLE
        Add-KeeperEnterpriseTeamMember -Team "Engineering" -User "user@example.com" -HideSharedFolders on
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)][string] $Team,
        [Parameter()][string[]] $Emails,
        [Parameter()][Alias('User')][string[]] $Users,
        [ValidateSet('on', 'off')][string]$HideSharedFolders
    )

    try {
        $userInputs = Get-EnterpriseTeamMemberInputs -EmailInputs $Emails -UserInputs $Users
    }
    catch {
        Write-Error $_.Exception.Message -ErrorAction Stop
    }

    [Enterprise]$enterprise = getEnterprise
    $teamTarget = Resolve-EnterpriseTeamTarget -Enterprise $enterprise -TeamInput $Team
    if (-not $teamTarget) { return }

    $processedUserIds = [System.Collections.Generic.HashSet[long]]::new()
    foreach ($userInput in @($userInputs)) {
        try {
            $userObject = resolveUser $enterprise.enterpriseData $userInput
        }
        catch {
            Write-Warning $_.Exception.Message
            continue
        }

        if (-not $processedUserIds.Add($userObject.Id)) {
            Write-Warning "User `"$($userObject.Email)`" was already specified; skipping duplicate."
            continue
        }

        try {
            $request = [EnterpriseTeamMembershipRequest]::new()
            $request.Enterprise = $enterprise
            $request.User = $userObject
            $request.TeamTarget = $teamTarget
            if ($PSBoundParameters.ContainsKey('HideSharedFolders')) {
                $request.HideSharedFolders = $HideSharedFolders
            }
            [void](Add-EnterpriseUserToTeamMembership -Request $request)
        }
        catch {
            Write-Warning "Failed to add user `"$($userObject.Email)`" to team `"$($teamTarget.Name)`": $($_.Exception.Message)"
        }
    }
}
Register-ArgumentCompleter -CommandName Add-KeeperEnterpriseTeamMember -ParameterName Team -ScriptBlock $Keeper_TeamNameCompleter
Register-ArgumentCompleter -CommandName Add-KeeperEnterpriseTeamMember -ParameterName Emails -ScriptBlock $Keeper_EnterpriseUserCompleter
Register-ArgumentCompleter -CommandName Add-KeeperEnterpriseTeamMember -ParameterName User -ScriptBlock $Keeper_EnterpriseUserCompleter
Register-ArgumentCompleter -CommandName Add-KeeperEnterpriseTeamMember -ParameterName Users -ScriptBlock $Keeper_EnterpriseUserCompleter

function Remove-KeeperEnterpriseTeamMember {
    <#
        .SYNOPSIS
        Removes existing enterprise users from a Keeper team.

        .DESCRIPTION
        Removes one or more users by email (-Emails) or by email / enterprise user ID (-User or -Users).
        At least one of -Emails or -User must be specified.

        If the user is not on the team, a warning is shown and no remove request is sent.

        .PARAMETER Team
        Team UID or Team Name from which the users will be removed.

        .PARAMETER Emails
        One or more user email addresses to remove from the team.

        .PARAMETER Users
        One or more user email addresses or enterprise user IDs to remove from the team.
        -User is an alias for this parameter. Use -Emails, -User, or both together.

        .EXAMPLE
        Remove-KeeperEnterpriseTeamMember -Team "Engineering" -Emails "alice@example.com", "bob@example.com"

        .EXAMPLE
        Remove-KeeperEnterpriseTeamMember -Team "Engineering" -User "user@example.com"
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)][string] $Team,
        [Parameter()][string[]] $Emails,
        [Parameter()][Alias('User')][string[]] $Users
    )

    try {
        $userInputs = Get-EnterpriseTeamMemberInputs -EmailInputs $Emails -UserInputs $Users
    }
    catch {
        Write-Error $_.Exception.Message -ErrorAction Stop
    }

    [Enterprise]$enterprise = getEnterprise
    $teamTarget = Resolve-EnterpriseTeamTarget -Enterprise $enterprise -TeamInput $Team
    if (-not $teamTarget) { return }

    $teamUid = $teamTarget.Uid
    $emailList = [System.Collections.Generic.List[string]]::new()
    $processedUserIds = [System.Collections.Generic.HashSet[long]]::new()
    foreach ($userInput in @($userInputs)) {
        try {
            $userObject = resolveUser $enterprise.enterpriseData $userInput
            if (-not $processedUserIds.Add($userObject.Id)) {
                Write-Warning "User `"$($userObject.Email)`" was already specified; skipping duplicate."
                continue
            }
            if (-not (Test-EnterpriseUserOnTeam -EnterpriseData $enterprise.enterpriseData -TeamUid $teamUid -UserId $userObject.Id)) {
                Write-Warning "User `"$($userObject.Email)`" is not a member of team `"$($teamTarget.Name)`"."
                continue
            }
            [void]$emailList.Add($userObject.Email)
        }
        catch { Write-Warning $_.Exception.Message }
    }

    if ($emailList.Count -eq 0) {
        Write-Warning "No team members were removed."
        return
    }

    try {
        Invoke-EnterpriseRemoveUsersFromTeams -EnterpriseData $enterprise.enterpriseData -Emails $emailList.ToArray() -TeamUids @($teamUid)
        $enterprise.loader.Load().GetAwaiter().GetResult() | Out-Null
        Write-Output "Removed $($emailList.Count) user(s) from team '$($teamTarget.Name)'."
    }
    catch {
        Write-Warning "Failed to remove users from team '$($teamTarget.Name)': $($_.Exception.Message)"
    }
}
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseTeamMember -ParameterName Team -ScriptBlock $Keeper_TeamNameCompleter
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseTeamMember -ParameterName Emails -ScriptBlock $Keeper_EnterpriseUserCompleter
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseTeamMember -ParameterName User -ScriptBlock $Keeper_EnterpriseUserCompleter
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseTeamMember -ParameterName Users -ScriptBlock $Keeper_EnterpriseUserCompleter

function Get-KeeperEnterpriseTeamRole {
    <#
        .SYNOPSIS
        Gets enterprise roles assigned to a team.

        .DESCRIPTION
        Read-only query matching enterprise-team role membership (used by -ar/-rr).
        Only approved teams are supported; queued-only teams return an empty list.

        .PARAMETER Team
        Team name, UID, or EnterpriseTeam object.

        .EXAMPLE
        Get-KeeperEnterpriseTeamRole -Team "Engineering"
    #>

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

    [Enterprise]$enterprise = getEnterprise
    $teamInput = if ($Team -is [string]) { $Team }
    elseif ($null -ne $Team.Uid) { [string]$Team.Uid }
    elseif ($Team.Name) { [string]$Team.Name }
    else { [string]$Team }

    $teamTarget = Resolve-EnterpriseTeamTarget -Enterprise $enterprise -TeamInput $teamInput
    if (-not $teamTarget) { return @() }
    if (-not $teamTarget.Team) {
        Write-Warning "Team `"$($teamTarget.Name)`" is queued only. Roles are listed after the team is approved."
        return @()
    }

    $roleIds = @($enterprise.roleData.GetRolesForTeam($teamTarget.Uid))
    if ($roleIds.Count -eq 0) { return @() }

    $roles = foreach ($roleId in $roleIds) {
        $role = $null
        if ($enterprise.roleData.TryGetRole($roleId, [ref]$role)) { $role }
    }
    return @($roles | Sort-Object { $_.DisplayName })
}
Register-ArgumentCompleter -CommandName Get-KeeperEnterpriseTeamRole -ParameterName Team -ScriptBlock $Keeper_TeamNameCompleter
New-Alias -Name ketr -Value Get-KeeperEnterpriseTeamRole

function Get-TeamMembersBatch {
    <#
    .SYNOPSIS
    Fetches team members in batches from the API.
    
    .DESCRIPTION
    Internal helper function that retrieves team member emails for multiple teams
    using parallel API calls in configurable batch sizes.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param (
        [Parameter(Mandatory)][KeeperSecurity.Authentication.IAuthentication]$Auth,
        [Parameter(Mandatory)][array]$TeamUids,
        [int]$BatchSize = 20
    )
    
    if ($TeamUids.Count -eq 0) { return @{} }
    $results = @{}
    
    for ($i = 0; $i -lt $TeamUids.Count; $i += $BatchSize) {
        $batch = $TeamUids[$i..([Math]::Min($i + $BatchSize - 1, $TeamUids.Count - 1))]
        $tasks = @{}
        
        foreach ($uid in $batch) {
            try {
                $request = New-Object Enterprise.GetTeamMemberRequest
                $request.TeamUid = [Google.Protobuf.ByteString]::CopyFrom(
                    [KeeperSecurity.Utils.CryptoUtils]::Base64UrlDecode($uid))
                $tasks[$uid] = $Auth.ExecuteAuthRest(
                    "vault/get_team_members",
                    $request,
                    [Enterprise.GetTeamMemberResponse]
                )
            }
            catch {
                Write-Warning "Failed to create request for team $uid : $($_.Exception.Message)"
                $results[$uid] = [System.Collections.Generic.List[string]]::new()
            }
        }
        
        if ($tasks.Count -eq 0) { continue }
        
        try {
            [System.Threading.Tasks.Task]::WhenAll($tasks.Values).GetAwaiter().GetResult() | Out-Null
        }
        catch {
            Write-Warning "Some team member requests failed: $($_.Exception.Message)"
        }
        
        foreach ($uid in $tasks.Keys) {
            $task = $tasks[$uid]
            if ($task.IsCompletedSuccessfully) {
                $emails = [System.Collections.Generic.List[string]]::new()
                if ($task.Result.EnterpriseUser) {
                    foreach ($u in $task.Result.EnterpriseUser) {
                        $emails.Add($u.Email)
                    }
                }
                $results[$uid] = $emails
            }
            else {
                $results[$uid] = [System.Collections.Generic.List[string]]::new()
            }
        }
    }
    
    return $results
}

function Get-KeeperEnterpriseTeams {
    <#
        .SYNOPSIS
        Lists all Keeper Enterprise teams.

        .DESCRIPTION
        Show details for all teams you have access to within your organization.

        .PARAMETER ShowMembers
        List team members from cache (fast, may be incomplete). Alias: -v

        .PARAMETER ShowAllMembers
        List team members, fetching from server if cache is empty (slower, complete). Alias: -vv

        .PARAMETER All
        Show all teams including those from managed companies (MSP admin). Alias: -a

        .PARAMETER Sort
        Sort teams by column: company, team_uid, name (default: company)

        .EXAMPLE
        Get-KeeperEnterpriseTeams # Default sort by company
        Get-KeeperEnterpriseTeams -Sort name # Sort by team name
        Get-KeeperEnterpriseTeams -Sort team_uid # Sort by team UID
        Get-KeeperEnterpriseTeams -v # Show members from cache (fast)
        Get-KeeperEnterpriseTeams -vv # Show all members (fetches from server if needed)
        Get-KeeperEnterpriseTeams -a # Include teams outside primary organization (MSP admin)
        Get-KeeperEnterpriseTeams -vv -a # All teams (including managed companies) with complete member list
    #>

    [CmdletBinding()]
    param (
        [Parameter()][Alias('v')][Switch] $ShowMembers,
        [Parameter()][Alias('vv')][Switch] $ShowAllMembers,
        [Parameter()][Alias('a')][Switch] $All,
        [Parameter()][ValidateSet('company', 'team_uid', 'name')][string] $Sort = 'company'
    )

    if (-not $Script:Context.Auth) {
        Write-Error "Not connected. Please run Connect-Keeper first." -ErrorAction Stop
    }

    $includeManagedCompanyTeams = $All.IsPresent
    $memberMode = if ($ShowAllMembers.IsPresent) { 'full' } elseif ($ShowMembers.IsPresent) { 'cache' } else { 'none' }
    $showMemberInfo = $memberMode -ne 'none'

    [Enterprise]$enterprise = $null
    if ($showMemberInfo) {
        try {
            $enterprise = getEnterprise
        }
        catch {
            Write-Warning "Could not load enterprise data for member info: $($_.Exception.Message)"
            $enterprise = $null
        }
        if (-not $enterprise -or -not $enterprise.enterpriseData) {
            Write-Warning "Member information will not be displayed."
            $showMemberInfo = $false
        }
    }
    $results = [System.Collections.ArrayList]::new()
    $teamByUid = @{}

    try {
        $request = New-Object Records.GetShareObjectsRequest
        $response = $Script:Context.Auth.ExecuteAuthRest(
            "vault/get_share_objects",
            $request,
            [Records.GetShareObjectsResponse]
        ).GetAwaiter().GetResult()

        if (-not $response) {
            Write-Warning "Empty response from API"
            return
        }

        $enterpriseNames = @{}
        if ($response.ShareEnterpriseNames) {
            foreach ($ent in $response.ShareEnterpriseNames) {
                $enterpriseNames[$ent.EnterpriseId] = $ent.Enterprisename
            }
        }

        $apiTeams = if ($response.ShareTeams) { @($response.ShareTeams) } else { @() }
        if ($includeManagedCompanyTeams -and $response.ShareMCTeams) {
            $apiTeams += @($response.ShareMCTeams)
        }
        
        $primaryEnterpriseId = $null
        try {
            $primaryEnterpriseId = $Script:Context.Auth.AuthContext.License.EnterpriseId
        }
        catch {
            $primaryEnterpriseId = $null
        }
        $hasNoValidEnterpriseId = ($null -eq $primaryEnterpriseId -or $primaryEnterpriseId -le 0)
        $hasShareTeams = ($response.ShareTeams -and $response.ShareTeams.Count -gt 0)
        if ($hasNoValidEnterpriseId -and $hasShareTeams) {
            $primaryEnterpriseId = $response.ShareTeams[0].EnterpriseId
        }

        if (-not $includeManagedCompanyTeams -and $null -ne $primaryEnterpriseId -and $primaryEnterpriseId -gt 0) {
            $apiTeams = @($apiTeams | Where-Object { $_.EnterpriseId -eq $primaryEnterpriseId })
        }

        foreach ($team in $apiTeams) {
            $teamUid = [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($team.TeamUid.ToByteArray())
            if ($teamByUid.ContainsKey($teamUid)) { continue }

            $companyName = $enterpriseNames[$team.EnterpriseId]

            $members = [System.Collections.Generic.List[string]]::new()
            if ($showMemberInfo) {
                foreach ($userId in $enterprise.enterpriseData.GetUsersForTeam($teamUid)) {
                    $user = $null
                    if ($enterprise.enterpriseData.TryGetUserById($userId, [ref]$user)) {
                        $members.Add($user.Email)
                    }
                }
            }

            $teamByUid[$teamUid] = @{
                Uid     = $teamUid
                Name    = $team.Teamname
                Company = $companyName
                Members = $members
            }
        }
    }
    catch {
        Write-Warning "Failed to fetch teams from API: $($_.Exception.Message)"
        return
    }

    $allTeams = @($teamByUid.Values)

    if ($memberMode -eq 'full' -and $showMemberInfo) {
        $teamsNeedToFetch = @($allTeams | Where-Object { $_.Members.Count -eq 0 } | ForEach-Object { $_.Uid })
        if ($teamsNeedToFetch.Count -gt 0) {
            $fetchedMembers = Get-TeamMembersBatch -Auth $Script:Context.Auth -TeamUids $teamsNeedToFetch
            
            if ($fetchedMembers) {
                foreach ($team in $allTeams) {
                    if ($team.Members.Count -eq 0 -and $fetchedMembers.ContainsKey($team.Uid)) {
                        $team.Members = $fetchedMembers[$team.Uid]
                    }
                }
            }
        }
    }

    $allTeams = @(switch ($Sort) {
        'team_uid' { $allTeams | Sort-Object { if ($_.Uid) { $_.Uid.ToLower() } else { '' } } }
        'name'     { $allTeams | Sort-Object { if ($_.Name) { $_.Name.ToLower() } else { '' } } }
        default    { $allTeams | Sort-Object { if ($_.Company) { $_.Company.ToLower() } else { '' } }, { if ($_.Name) { $_.Name.ToLower() } else { '' } } }
    })

    $index = 0
    foreach ($team in $allTeams) {
        $index++
        $props = [ordered]@{
            '#'        = $index
            'Company'  = $team.Company
            'Team UID' = $team.Uid
            'Name'     = $team.Name
        }
        if ($showMemberInfo) {
            $props['Member'] = if ($team.Members.Count -gt 0) { $team.Members[0] } else { '' }
        }
        [void]$results.Add([PSCustomObject]$props)

        if ($showMemberInfo) {
            for ($i = 1; $i -lt $team.Members.Count; $i++) {
                $memberRow = [ordered]@{ '#' = ''; 'Company' = ''; 'Team UID' = ''; 'Name' = ''; 'Member' = $team.Members[$i] }
                [void]$results.Add([PSCustomObject]$memberRow)
            }
        }
    }

    if ($results.Count -eq 0) {
        Write-Host "No teams found."
        return
    }

    Write-Host "`nFound $($allTeams.Count) team(s).`n"
    $results | Format-Table -AutoSize
}
New-Alias -Name list-team -Value Get-KeeperEnterpriseTeams
# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBp4ICGgxrSpl5n
# lIspdzrSocs1zy4aoPC8APLU2Ut936CCITswggWNMIIEdaADAgECAhAOmxiO+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
# BDEiBCBonOEohUSlJv41kfIcZvfjCVtt5eLjLjlzjbnNpbvc1DANBgkqhkiG9w0B
# AQEFAASCAYBo933Ufs3OdO+w1Kl407frqLSMeMYZKfE9L92dP3ULHFPDyP2r9HOq
# awbo1mZGBDZzMtB3fd243jhSO/RXnB7wHaGLWfRGzwp/+nppoWHdw25FC0lSFlJ5
# mMr1Q2xBzpGQV8GXgjFYOBe+c1rVF5J90hRIBvOG8O11lzTMNupvlvduZyz35cv8
# hnmUA1ijmhvjBSzmg/NYxUDwf8qfdaJLXyFDQesmDiN5wC5o0LlyebUZe0Xr36fp
# JGanf+lKeEcQzvMAKFvcGdtJ/sEnXeAt3MiHuZ/DA4b8M+U3jFHLG44P8X8WagaP
# s3Zxaoknd0Yn60OgpIgcMf+sQul12SLrG/hg1TtJ3/kNLGqOzsH0goIzHRNfh4e6
# F81/sfabaJjXBWKpMqwYPwYcHDWOcObGnVzQzT5fcsKTTdJelZLpZ8BmgAJbs10e
# Il8k341Gneo/RlUxmF5tclTSQoo1jalYNP1bgG7mx/Ncy4ADY4ldJTtwDW+VvmdR
# CH/sjwPvh4yhggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwODE0MjExOTEzWjAv
# BgkqhkiG9w0BCQQxIgQgur3Q2ZIO+viuZFKdjGi31WedQ/WHkIkiMJe+jetLQkgw
# DQYJKoZIhvcNAQEBBQAEggIATToskwn+ozT2Ds1rJqolbsVjcJcfKbdYtJ0gzXk/
# QsziAlDIdzBGHmo6bjzzavCSxuA3i5AdHgA911cebk/S6taASpsLpipYooVLthdY
# GNGMq7VFumnDFOttKQM+yzr2wPQgpBH+oeDu4ePqm0vNrk7rHlFfp1LiLEAWF6ur
# tSsOsWS1EZzsa1JDFIfJj0D4yWARwXzKVB/YLIByuntvN6UJQA1IEtb0q3TQHQdg
# KIHI6yIULvcD4B5mmlHJMQN2QbgB5PTF8AR1isO+bXcarENMHMfUy26bEec738+b
# Bmjq9oNHI8Tw3UN4Uiy7EvubK6gd4zy1b3Cmfu09KbkE8mHfe4xJrZ5oNQm6Wqw3
# yYlhFrkFyaWkW1GdgxnGIcnSVz3ZAuo4Evd3XncqGsx5I/qm9cMaCnJIrm7TXzGu
# gAoK6CasoN7vDqdhmxo7r778g2W/+hVbshfH4ps45fHNf4xCLqey+wZpVdZLfuud
# 2bkiT/n/Y+nh/llCIqkfs0hM3lL+5Mf17/V2UqDnRpVzMMvQTBLerxuDve0CO1ay
# c5McNRj2STb5BtszGtMYmgyIP/iisSno1hOszbDODfZvkZNDxXIRegq/YG+d7Z4v
# QHv1H7YR1fQ7SmlIXd1yMNpAbgNlDbpg+lgD143VGYN+W40nbO/tPHcxWVewy7I/
# h3M=
# SIG # End signature block