Context.psm1

[CmdletBinding()]
param()
$scriptName = 'Context'
Write-Verbose "[$scriptName] - Importing module"

#region - From [functions] - [private]
Write-Verbose "[$scriptName] - [functions] - [private] - Processing folder"

#region - From [functions] - [private] - [Base64]
Write-Verbose "[$scriptName] - [functions] - [private] - [Base64] - Processing folder"

#region - From [functions] - [private] - [Base64] - [ConvertFrom-Base64]
Write-Verbose "[$scriptName] - [functions] - [private] - [Base64] - [ConvertFrom-Base64] - Importing"

function ConvertFrom-Base64 {
    <#
        .SYNOPSIS
        Converts a Base64 encoded string to a string.

        .DESCRIPTION
        Converts a Base64 encoded string to a string.

        .EXAMPLE
        ConvertFrom-Base64 -Base64String 'VGhpc0lzQU5pY2VTdHJpbmc='
        ThisIsANiceString

        Converts the Base64 encoded string 'VGhpc0lzQU5pY2VTdHJpbmc=' to a string.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        # The Base64 encoded string to convert.
        [Parameter(Mandatory = $true)]
        [string] $Base64String
    )

    [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Base64String))
}

Write-Verbose "[$scriptName] - [functions] - [private] - [Base64] - [ConvertFrom-Base64] - Done"
#endregion - From [functions] - [private] - [Base64] - [ConvertFrom-Base64]
#region - From [functions] - [private] - [Base64] - [ConvertTo-Base64]
Write-Verbose "[$scriptName] - [functions] - [private] - [Base64] - [ConvertTo-Base64] - Importing"

function ConvertTo-Base64 {
    <#
        .SYNOPSIS
        Converts a string to a Base64 encoded string.

        .DESCRIPTION
        Converts a string to a Base64 encoded string.

        .EXAMPLE
        ConvertTo-Base64 -String 'ThisIsANiceString'
        VGhpc0lzQU5pY2VTdHJpbmc=

        Converts the string 'ThisIsANiceString' to a Base64 encoded string.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string] $String
    )

    [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($String))
}

Write-Verbose "[$scriptName] - [functions] - [private] - [Base64] - [ConvertTo-Base64] - Done"
#endregion - From [functions] - [private] - [Base64] - [ConvertTo-Base64]
#region - From [functions] - [private] - [Base64] - [Test-Base64]
Write-Verbose "[$scriptName] - [functions] - [private] - [Base64] - [Test-Base64] - Importing"

function Test-Base64 {
    <#
        .SYNOPSIS
        Test if a string is a valid Base64 string.

        .DESCRIPTION
        Test if a string is a valid Base64 string.

        .EXAMPLE
        Test-Base64 -Base64String 'U29tZSBkYXRh'
        True

        Returns $true as the string is a valid Base64 string.
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param (
        [string] $Base64String
    )
    try {
        $null = [Convert]::FromBase64String($Base64String)
        return $true
    } catch {
        return $false
    }
}

Write-Verbose "[$scriptName] - [functions] - [private] - [Base64] - [Test-Base64] - Done"
#endregion - From [functions] - [private] - [Base64] - [Test-Base64]

Write-Verbose "[$scriptName] - [functions] - [private] - [Base64] - Done"
#endregion - From [functions] - [private] - [Base64]

#region - From [functions] - [private] - [JsonToObject]
Write-Verbose "[$scriptName] - [functions] - [private] - [JsonToObject] - Processing folder"

#region - From [functions] - [private] - [JsonToObject] - [Convert-ContextHashtableToObjectRecursive]
Write-Verbose "[$scriptName] - [functions] - [private] - [JsonToObject] - [Convert-ContextHashtableToObjectRecursive] - Importing"

function Convert-ContextHashtableToObjectRecursive {
    <#
        .SYNOPSIS
        Converts a hashtable to a context object.

        .DESCRIPTION
        This function is used to convert a hashtable to a context object.
        String values that are prefixed with '[SECURESTRING]', are converted back to SecureString objects.
        Other values are converted to their original types, like ints, booleans, string, arrays, and nested objects.

        .EXAMPLE
        Convert-ContextHashtableToObjectRecursive -Hashtable @{
            Name = 'Test'
            Token = '[SECURESTRING]TestToken'
            Nested = @{
                Name = 'Nested'
                Token = '[SECURESTRING]NestedToken'
            }
        }

        This example converts a hashtable to a context object, where the 'Token' and 'Nested.Token' values are SecureString objects.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingConvertToSecureStringWithPlainText', '',
        Justification = 'The securestring is read from the object this function reads.'
    )]
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param (
        # Hashtable to convert to context object
        [object] $Hashtable
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
    }

    process {
        try {
            $result = [pscustomobject]@{}

            foreach ($key in $Hashtable.Keys) {
                $value = $Hashtable[$key]
                Write-Debug "Processing [$key]"
                Write-Debug "Value: $value"
                Write-Debug "Type: $($value.GetType().Name)"
                if ($value -is [string] -and $value -like '`[SECURESTRING`]*') {
                    Write-Debug "Converting [$key] as [SecureString]"
                    $secureValue = $value -replace '^\[SECURESTRING\]', ''
                    $result | Add-Member -NotePropertyName $key -NotePropertyValue ($secureValue | ConvertTo-SecureString -AsPlainText -Force)
                } elseif ($value -is [hashtable]) {
                    Write-Debug "Converting [$key] as [hashtable]"
                    $result | Add-Member -NotePropertyName $key -NotePropertyValue (Convert-ContextHashtableToObjectRecursive $value)
                } elseif ($value -is [array]) {
                    Write-Debug "Converting [$key] as [IEnumerable], including arrays and hashtables"
                    $result | Add-Member -NotePropertyName $key -NotePropertyValue @(
                        $value | ForEach-Object {
                            if ($_ -is [hashtable]) {
                                Convert-ContextHashtableToObjectRecursive $_
                            } else {
                                $_
                            }
                        }
                    )
                } else {
                    Write-Debug "Converting [$key] as regular value"
                    $result | Add-Member -NotePropertyName $key -NotePropertyValue $value
                }
            }
            return $result
        } catch {
            Write-Error $_
            throw 'Failed to convert hashtable to object'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Write-Verbose "[$scriptName] - [functions] - [private] - [JsonToObject] - [Convert-ContextHashtableToObjectRecursive] - Done"
#endregion - From [functions] - [private] - [JsonToObject] - [Convert-ContextHashtableToObjectRecursive]
#region - From [functions] - [private] - [JsonToObject] - [ConvertFrom-ContextJson]
Write-Verbose "[$scriptName] - [functions] - [private] - [JsonToObject] - [ConvertFrom-ContextJson] - Importing"

function ConvertFrom-ContextJson {
    <#
        .SYNOPSIS
        Converts a JSON string to a context object.

        .DESCRIPTION
        Converts a JSON string to a context object.
        [SECURESTRING] prefixed text is converted to SecureString objects.
        Other values are converted to their original types, like ints, booleans, string, arrays, and nested objects.

        .EXAMPLE
        ConvertFrom-ContextJson -JsonString '{
            "Name": "Test",
            "Token": "[SECURESTRING]TestToken",
            "Nested": {
                "Name": "Nested",
                "Token": "[SECURESTRING]NestedToken"
            }
        }'

        This example converts a JSON string to a context object, where the 'Token' and 'Nested.Token' values are SecureString objects.
    #>

    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param (
        # JSON string to convert to context object
        [Parameter(Mandatory)]
        [string] $JsonString
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
    }

    process {
        try {
            $hashtableObject = $JsonString | ConvertFrom-Json -Depth 100 -AsHashtable
            return Convert-ContextHashtableToObjectRecursive $hashtableObject
        } catch {
            Write-Error $_
            throw 'Failed to convert JSON to object'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Write-Verbose "[$scriptName] - [functions] - [private] - [JsonToObject] - [ConvertFrom-ContextJson] - Done"
#endregion - From [functions] - [private] - [JsonToObject] - [ConvertFrom-ContextJson]

Write-Verbose "[$scriptName] - [functions] - [private] - [JsonToObject] - Done"
#endregion - From [functions] - [private] - [JsonToObject]

#region - From [functions] - [private] - [ObjectToJSON]
Write-Verbose "[$scriptName] - [functions] - [private] - [ObjectToJSON] - Processing folder"

#region - From [functions] - [private] - [ObjectToJSON] - [Convert-ContextObjectToHashtableRecursive]
Write-Verbose "[$scriptName] - [functions] - [private] - [ObjectToJSON] - [Convert-ContextObjectToHashtableRecursive] - Importing"

function Convert-ContextObjectToHashtableRecursive {
    <#
        .SYNOPSIS
        Converts a context object to a hashtable.

        .DESCRIPTION
        This function converts a context object to a hashtable.
        Secure strings are converted to a string representation, prefixed with '[SECURESTRING]'.
        Datetime objects are converted to a string representation using the 'o' format specifier.
        Nested context objects are recursively converted to hashtables.

        .EXAMPLE
        Convert-ContextObjectToHashtableRecursive -Object ([PSCustomObject]@{
            Name = 'MySecret'
            AccessToken = '123123123' | ConvertTo-SecureString -AsPlainText -Force
            Nested = @{
                Name = 'MyNestedSecret'
                NestedAccessToken = '123123123' | ConvertTo-SecureString -AsPlainText -Force
            }
        })

        Converts the context object to a hashtable. Converts the AccessToken and NestedAccessToken secure strings to a string representation.
    #>

    [OutputType([hashtable])]
    [CmdletBinding()]
    param (
        # The object to convert.
        [object] $Object
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
    }

    process {
        try {
            $result = @{}

            if ($Object -is [hashtable]) {
                Write-Debug 'Converting [hashtable] to [PSCustomObject]'
                $Object = [PSCustomObject]$Object
            } elseif ($Object -is [string] -or $Object -is [int] -or $Object -is [bool]) {
                Write-Debug 'returning as string'
                return $Object
            }

            foreach ($property in $Object.PSObject.Properties) {
                $name = $property.Name
                $value = $property.Value
                Write-Debug "Processing [$name]"
                Write-Debug "Value: $value"
                Write-Debug "Type: $($value.GetType().Name)"
                if ($value -is [datetime]) {
                    Write-Debug '- as DateTime'
                    $result[$property.Name] = $value.ToString('o')
                } elseif ($value -is [string] -or $Object -is [int] -or $Object -is [bool]) {
                    Write-Debug '- as string, int, bool'
                    $result[$property.Name] = $value
                } elseif ($value -is [System.Security.SecureString]) {
                    Write-Debug '- as SecureString'
                    $value = $value | ConvertFrom-SecureString -AsPlainText
                    $result[$property.Name] = "[SECURESTRING]$value"
                } elseif ($value -is [psobject] -or $value -is [PSCustomObject] -or $value -is [hashtable]) {
                    Write-Debug '- as PSObject, PSCustomObject or hashtable'
                    $result[$property.Name] = Convert-ContextObjectToHashtableRecursive $value
                } elseif ($value -is [System.Collections.IEnumerable]) {
                    Write-Debug '- as IEnumerable, including arrays and hashtables'
                    $result[$property.Name] = @(
                        $value | ForEach-Object {
                            Convert-ContextObjectToHashtableRecursive $_
                        }
                    )
                } else {
                    Write-Debug '- as regular value'
                    $result[$property.Name] = $value
                }
            }
            return $result
        } catch {
            Write-Error $_
            throw 'Failed to convert context object to hashtable'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Write-Verbose "[$scriptName] - [functions] - [private] - [ObjectToJSON] - [Convert-ContextObjectToHashtableRecursive] - Done"
#endregion - From [functions] - [private] - [ObjectToJSON] - [Convert-ContextObjectToHashtableRecursive]
#region - From [functions] - [private] - [ObjectToJSON] - [ConvertTo-ContextJson]
Write-Verbose "[$scriptName] - [functions] - [private] - [ObjectToJSON] - [ConvertTo-ContextJson] - Importing"

function ConvertTo-ContextJson {
    <#
        .SYNOPSIS
        Takes an object and converts it to a JSON string.

        .DESCRIPTION
        Takes objects or hashtables and converts them to a JSON string.
        SecureStrings are converted to plain text strings and prefixed with [SECURESTRING]. The conversion is recursive for any nested objects.
        Use ConvertFrom-ContextJson to convert back to an object.

        .EXAMPLE
        ConvertTo-ContextJson -Context ([pscustomobject]@{
            Name = 'MySecret'
            AccessToken = '123123123' | ConvertTo-SecureString -AsPlainText -Force
        })

        Returns a JSON string representation of the object.

        ```json
        {
            "Name": "MySecret",
            "AccessToken ": "[SECURESTRING]123123123"
        }
        ```
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        # The object to convert to a Context JSON string.
        [Parameter(Mandatory)]
        [object] $Context,

        # The ID of the context.
        [Parameter(Mandatory)]
        [string] $ID
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
    }

    process {
        try {
            $processedObject = Convert-ContextObjectToHashtableRecursive $Context
            $processedObject['ID'] = $ID
            return ($processedObject | ConvertTo-Json -Depth 100 -Compress)
        } catch {
            Write-Error $_
            throw 'Failed to convert object to JSON'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Write-Verbose "[$scriptName] - [functions] - [private] - [ObjectToJSON] - [ConvertTo-ContextJson] - Done"
#endregion - From [functions] - [private] - [ObjectToJSON] - [ConvertTo-ContextJson]

Write-Verbose "[$scriptName] - [functions] - [private] - [ObjectToJSON] - Done"
#endregion - From [functions] - [private] - [ObjectToJSON]

#region - From [functions] - [private] - [Get-ContextInfo]
Write-Verbose "[$scriptName] - [functions] - [private] - [Get-ContextInfo] - Importing"

function Get-ContextInfo {
    <#
        .SYNOPSIS
        Retrieves all context info from the context vault.

        .DESCRIPTION
        Retrieves all context info from the context vault.

        .EXAMPLE
        Get-ContextInfo

        Get all context info from the context vault.
    #>

    param()

    $vaultName = $script:Config.VaultName
    $secretPrefix = $script:Config.SecretPrefix

    Write-Verbose "Retrieving all context info from [$vaultName]"

    Get-SecretInfo -Vault $vaultName | Where-Object { ($_.Name).StartsWith($secretPrefix) } | ForEach-Object {
        $name64 = $_.Name -replace "^$secretPrefix"
        if (Test-Base64 -Base64String $name64) {
            $name = ConvertFrom-Base64 -Base64String $name64
            Write-Verbose " + $name ($name64)"
            [pscustomobject]@{
                Name64     = $name64
                SecretName = $_.Name
                Name       = $name
                Metadata   = $_.Metadata
                Type       = $_.Type
            }
        }
    }
}

Write-Verbose "[$scriptName] - [functions] - [private] - [Get-ContextInfo] - Done"
#endregion - From [functions] - [private] - [Get-ContextInfo]
#region - From [functions] - [private] - [Get-ContextVault]
Write-Verbose "[$scriptName] - [functions] - [private] - [Get-ContextVault] - Importing"

#Requires -Modules @{ ModuleName = 'Microsoft.PowerShell.SecretManagement'; RequiredVersion = '1.1.2' }

function Get-ContextVault {
    <#
        .SYNOPSIS
        Retrieves the context vault.

        .DESCRIPTION
        Connects to a context vault.
        If the vault name is not set in the configuration, it throws an error.
        If the specified vault is not found, it throws an error.
        Otherwise, it returns the secret vault object.

        .EXAMPLE
        Get-ContextVault

        This example retrieves the context vault.
    #>

    [CmdletBinding()]
    param()

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
    }

    process {
        try {
            if (-not $script:Config.VaultName) {
                throw 'Context vault name not set'
            }

            Write-Verbose "Connecting to context vault [$($script:Config.VaultName)]"
            $secretVault = Get-SecretVault | Where-Object { $_.Name -eq $script:Config.VaultName }
            if (-not $secretVault) {
                Write-Error $_
                throw "Context vault [$($script:Config.VaultName)] not found"
            }

            return $secretVault
        } catch {
            Write-Error $_
            throw 'Failed to get context vault'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Write-Verbose "[$scriptName] - [functions] - [private] - [Get-ContextVault] - Done"
#endregion - From [functions] - [private] - [Get-ContextVault]
#region - From [functions] - [private] - [Initialize-ContextVault]
Write-Verbose "[$scriptName] - [functions] - [private] - [Initialize-ContextVault] - Importing"

#Requires -Modules @{ ModuleName = 'Microsoft.PowerShell.SecretManagement'; RequiredVersion = '1.1.2' }
#Requires -Modules @{ ModuleName = 'Microsoft.PowerShell.SecretStore'; RequiredVersion = '1.0.6' }

function Initialize-ContextVault {
    <#
        .SYNOPSIS
        Initialize a context vault.

        .DESCRIPTION
        Initialize a context vault. If the vault does not exist, it will be created and registered.

        The SecretStore is created with the following parameters:
        - Authentication: None
        - PasswordTimeout: -1 (infinite)
        - Interaction: None
        - Scope: CurrentUser

        .EXAMPLE
        Initialize-ContextVault

        Initializes a context vault named 'ContextVault' using the 'Microsoft.PowerShell.SecretStore' module.
    #>

    [OutputType([Microsoft.PowerShell.SecretManagement.SecretVaultInfo])]
    [CmdletBinding()]
    param (
        # The name of the secret vault.
        [Parameter()]
        [string] $Name = $script:Config.VaultName,

        # The type of the secret vault.
        [Parameter()]
        [string] $Type = $script:Config.VaultType
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
    }

    process {
        try {
            $vault = Get-SecretVault | Where-Object { $_.ModuleName -eq $Type }
            if (-not $vault) {
                Write-Verbose "[$Type] - Configuring vault type"

                $vaultParameters = @{
                    Authentication  = 'None'
                    PasswordTimeout = -1
                    Interaction     = 'None'
                    Scope           = 'CurrentUser'
                    WarningAction   = 'SilentlyContinue'
                    Confirm         = $false
                    Force           = $true
                }
                Reset-SecretStore @vaultParameters
                Write-Verbose "[$Type] - Done"

                Write-Verbose "[$Name] - Registering vault"
                $secretVault = @{
                    Name         = $Name
                    ModuleName   = $Type
                    DefaultVault = $true
                    Description  = 'SecretStore'
                }
                Register-SecretVault @secretVault
                Write-Verbose "[$Name] - Done"
            } else {
                Write-Verbose "[$Name] - Vault already registered"
            }

            Get-SecretVault | Where-Object { $_.ModuleName -eq $Type }
        } catch {
            Write-Error $_
            throw 'Failed to initialize context vault'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Write-Verbose "[$scriptName] - [functions] - [private] - [Initialize-ContextVault] - Done"
#endregion - From [functions] - [private] - [Initialize-ContextVault]

Write-Verbose "[$scriptName] - [functions] - [private] - Done"
#endregion - From [functions] - [private]

#region - From [functions] - [public]
Write-Verbose "[$scriptName] - [functions] - [public] - Processing folder"

#region - From [functions] - [public] - [Context]
Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - Processing folder"

#region - From [functions] - [public] - [Context] - [Get-Context]
Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Get-Context] - Importing"

#Requires -Modules @{ ModuleName = 'Microsoft.PowerShell.SecretManagement'; RequiredVersion = '1.1.2' }

filter Get-Context {
    <#
        .SYNOPSIS
        Retrieves a context from the context vault.

        .DESCRIPTION
        Retrieves a context from the context vault.
        If no name is specified, all contexts from the context vault will be retrieved.

        .EXAMPLE
        Get-Context

        Get all contexts from the context vault.

        .EXAMPLE
        Get-Context -ID 'MySecret'

        Get the context called 'MySecret' from the vault.
    #>

    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param(
        # The name of the context to retrieve from the vault.
        [Parameter()]
        [SupportsWildcards()]
        [Alias('ContextID')]
        [string] $ID
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
        $null = Get-ContextVault
        $vaultName = $script:Config.VaultName
        $contextInfos = Get-ContextInfo
    }

    process {
        try {
            if (-not $PSBoundParameters.ContainsKey('ID')) {
                Write-Verbose "Retrieving all contexts from [$vaultName]"
            } elseif ([string]::IsNullOrEmpty($ID)) {
                Write-Verbose "Return 0 contexts from [$vaultName]"
                return
            } elseif ($ID.Contains('*')) {
                Write-Verbose "Retrieving contexts like [$ID] from [$vaultName]"
                $contextInfos = $contextInfos | Where-Object { $_.Name -like $ID }
            } else {
                Write-Verbose "Retrieving context [$ID] from [$vaultName]"
                $contextInfos = $contextInfos | Where-Object { $_.Name -eq $ID }
            }

            Write-Verbose "Found [$($contextInfos.Count)] contexts in [$vaultName]"
            $contextInfos | ForEach-Object {
                $contextJson = Get-Secret -Name $_.SecretName -Vault $vaultName -AsPlainText
                ConvertFrom-ContextJson -JsonString $contextJson
            }
        } catch {
            Write-Error $_
            throw 'Failed to get context'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Register-ArgumentCompleter -CommandName Get-Context -ParameterName ID -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter

    Get-ContextInfo | Where-Object { $_.Name -like "$wordToComplete*" } |
        ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name)
        }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Get-Context] - Done"
#endregion - From [functions] - [public] - [Context] - [Get-Context]
#region - From [functions] - [public] - [Context] - [Remove-Context]
Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Remove-Context] - Importing"

#Requires -Modules @{ ModuleName = 'DynamicParams'; RequiredVersion = '1.1.8' }
#Requires -Modules @{ ModuleName = 'Microsoft.PowerShell.SecretManagement'; RequiredVersion = '1.1.2' }

filter Remove-Context {
    <#
        .SYNOPSIS
        Removes a context from the context vault.

        .DESCRIPTION
        This function removes a context from the vault. It supports removing a single context by name,
        multiple contexts using wildcard patterns, and can also accept input from the pipeline.
        If the specified context(s) exist, they will be removed from the vault.

        .EXAMPLE
        Remove-Context

        Removes all contexts from the vault.

        .EXAMPLE
        Remove-Context -ID 'MySecret'

        Removes the context called 'MySecret' from the vault.
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the context to remove from the vault.
        [Parameter()]
        [Alias('ContextID')]
        [string] $ID
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
        $null = Get-ContextVault
    }

    process {
        try {

            if ($PSCmdlet.ShouldProcess($ID, 'Remove secret')) {
                Get-ContextInfo | Where-Object { $_.Name -eq $ID } | ForEach-Object {
                    Remove-Secret -Name $_.SecretName -Vault $script:Config.VaultName
                }
            }
        } catch {
            Write-Error $_
            throw 'Failed to remove context'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Register-ArgumentCompleter -CommandName Remove-Context -ParameterName ID -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter

    Get-ContextInfo | Where-Object { $_.Name -like "$wordToComplete*" } |
        ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name)
        }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Remove-Context] - Done"
#endregion - From [functions] - [public] - [Context] - [Remove-Context]
#region - From [functions] - [public] - [Context] - [Rename-Context]
Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Rename-Context] - Importing"

function Rename-Context {
    <#
        .SYNOPSIS
        Renames a context.

        .DESCRIPTION
        This function renames a context.
        It retrieves the context with the old ID, sets the context with the new ID, and removes the context with the old ID.

        .EXAMPLE
        Example of how to use the function or script.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The ID of the context to rename.
        [Parameter(Mandatory)]
        [string] $ID,

        # The new ID of the context.
        [Parameter(Mandatory)]
        [string] $NewID,

        # Force the rename even if the new ID already exists.
        [Parameter()]
        [switch] $Force
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
        $context = Get-Context -ID $ID
        if (-not $context) {
            throw "Context with ID '$ID' not found."
        }

        $existingContext = Get-Context -ID $NewID
        if ($existingContext -and -not $Force) {
            throw "Context with ID '$NewID' already exists."
        }
    }

    process {
        if ($PSCmdlet.ShouldProcess("Renaming context '$ID' to '$NewID'")) {
            try {
                Set-Context -ID $NewID -Context $context
            } catch {
                Write-Error $_
                throw 'Failed to set new context'
            }

            try {
                Remove-Context -ID $ID
            } catch {
                Write-Error $_
                throw 'Failed to remove old context'
            }
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Register-ArgumentCompleter -CommandName Rename-Context -ParameterName ID -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter

    Get-ContextInfo | Where-Object { $_.Name -like "$wordToComplete*" } |
        ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name)
        }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Rename-Context] - Done"
#endregion - From [functions] - [public] - [Context] - [Rename-Context]
#region - From [functions] - [public] - [Context] - [Set-Context]
Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Set-Context] - Importing"

#Requires -Modules @{ ModuleName = 'Microsoft.PowerShell.SecretManagement'; RequiredVersion = '1.1.2' }

function Set-Context {
    <#
        .SYNOPSIS
        Set a context and store it in the context vault.

        .DESCRIPTION
        If the context does not exist, it will be created. If it already exists, it will be updated.

        .EXAMPLE
        Set-Context -ID 'PSModule.GitHub' -Context @{ Name = 'MySecret' }

        Create a context called 'MySecret' in the vault.

        .EXAMPLE
        Set-Context -ID 'PSModule.GitHub' -Context @{ Name = 'MySecret'; AccessToken = '123123123' }

        Creates a context called 'MySecret' in the vault with the settings.
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The ID of the context.
        [Parameter(Mandatory)]
        [Alias('ContextID')]
        [string] $ID,

        # The data of the context.
        [Parameter(Mandatory)]
        [object] $Context
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
        $null = Get-ContextVault
        $vaultName = $script:Config.VaultName
        $secretPrefix = $script:Config.SecretPrefix
    }

    process {
        try {
            $secret = ConvertTo-ContextJson -Context $Context -ID $ID
        } catch {
            Write-Error $_
            throw 'Failed to convert context to JSON'
        }

        try {
            $name64 = ConvertTo-Base64 -String $ID
        } catch {
            Write-Error $_
            throw 'Failed to convert ID to Base64'
        }

        $param = @{
            Name   = "$secretPrefix$name64"
            Secret = $secret
            Vault  = $vaultName
        }
        Write-Verbose ($param | ConvertTo-Json -Depth 5)

        try {
            if ($PSCmdlet.ShouldProcess($ID, 'Set Secret')) {
                Set-Secret @param
            }
        } catch {
            Write-Error $_
            throw 'Failed to set secret'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - [Set-Context] - Done"
#endregion - From [functions] - [public] - [Context] - [Set-Context]

Write-Verbose "[$scriptName] - [functions] - [public] - [Context] - Done"
#endregion - From [functions] - [public] - [Context]

#region - From [functions] - [public] - [ContextSetting]
Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSetting] - Processing folder"

#region - From [functions] - [public] - [ContextSetting] - [Get-ContextSetting]
Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSetting] - [Get-ContextSetting] - Importing"

#Requires -Modules @{ ModuleName = 'Microsoft.PowerShell.SecretManagement'; RequiredVersion = '1.1.2' }

function Get-ContextSetting {
    <#
        .SYNOPSIS
        Retrieve a setting from a context.

        .DESCRIPTION
        This function retrieves a setting from a specified context.

        .EXAMPLE
        Get-ContextSetting -Context 'GitHub' -Name 'APIBaseUri'

        Get the value of the 'APIBaseUri' setting from the 'GitHub' context.
    #>

    [OutputType([object])]
    [CmdletBinding()]
    param (
        # The context to get the configuration from.
        [Parameter(Mandatory)]
        [Alias('ContextID')]
        [string] $ID,

        # Name of a setting to get.
        [Parameter(Mandatory)]
        [string] $Name
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
        $null = Get-ContextVault
    }

    process {
        try {
            $context = Get-Context -ID $ID

            if (-not $context) {
                throw "Context [$ID] not found"
            }

            Write-Verbose "Returning setting: [$Name]"
            $context.$Name
        } catch {
            Write-Error $_
            throw 'Failed to get context setting'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Register-ArgumentCompleter -CommandName Get-ContextSetting -ParameterName ID -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter

    Get-ContextInfo | Where-Object { $_.Name -like "$wordToComplete*" } |
        ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name)
        }
}

Register-ArgumentCompleter -CommandName Get-ContextSetting -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter

    Get-Context -ID $fakeBoundParameter.ID | ForEach-Object {
        $_.PSObject.Properties | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name)
        }
    }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSetting] - [Get-ContextSetting] - Done"
#endregion - From [functions] - [public] - [ContextSetting] - [Get-ContextSetting]
#region - From [functions] - [public] - [ContextSetting] - [Remove-ContextSetting]
Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSetting] - [Remove-ContextSetting] - Importing"

#Requires -Modules @{ ModuleName = 'DynamicParams'; RequiredVersion = '1.1.8' }

filter Remove-ContextSetting {
    <#
        .SYNOPSIS
        Remove a setting from the context.

        .DESCRIPTION
        This function removes a setting from the specified context.
        It supports wildcard patterns for the name and does accept pipeline input.

        .PARAMETER Name
        Name of a setting to remove.

        .EXAMPLE
        Remove-ContextSetting -Name 'APIBaseUri' -Context 'GitHub'

        Remove the APIBaseUri setting from the 'GitHub' context.

        .EXAMPLE
        Get-ContextSetting -Context 'GitHub' | Remove-ContextSetting

        Remove all settings starting with 'API' from the 'GitHub' context.

        .EXAMPLE
        Remove-ContextSetting -Name 'API*' -Context 'GitHub'

        Remove all settings starting with 'API' from the 'GitHub' context.

        .EXAMPLE
        Get-ContextSetting -Context 'GitHub' | Where-Object { $_.Name -like 'API*' } | Remove-ContextSetting

        Remove all settings starting with 'API' from the 'GitHub' context using pipeline input.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The name of the setting to remove.
        [Parameter(Mandatory)]
        [Alias('Setting')]
        [string] $Name,

        # The name of the context where the setting will be removed.
        [Parameter(Mandatory)]
        [Alias('ContextID')]
        [string] $ID
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
    }

    process {
        try {
            $context = Get-Context -ID $ID

            if (-not $context) {
                throw "Context [$ID] not found"
            }

            if ($PSCmdlet.ShouldProcess("[$($context.Name)]", "Remove [$Name]")) {
                Write-Verbose "Setting [$Name] in [$($context.Name)]"
                $context.PSObject.Properties.Remove($Name)
                Set-Context -Context $context -ID $ID
            }
        } catch {
            Write-Error $_
            throw 'Failed to remove context setting'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Register-ArgumentCompleter -CommandName Remove-ContextSetting -ParameterName ID -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter

    Get-ContextInfo | Where-Object { $_.Name -like "$wordToComplete*" } |
        ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name)
        }
}

Register-ArgumentCompleter -CommandName Get-ContextSetting -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter

    Get-Context -ID $fakeBoundParameter.ID | ForEach-Object {
        $_.PSObject.Properties | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name)
        }
    }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSetting] - [Remove-ContextSetting] - Done"
#endregion - From [functions] - [public] - [ContextSetting] - [Remove-ContextSetting]
#region - From [functions] - [public] - [ContextSetting] - [Set-ContextSetting]
Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSetting] - [Set-ContextSetting] - Importing"

#Requires -Modules @{ ModuleName = 'DynamicParams'; RequiredVersion = '1.1.8' }
#Requires -Modules @{ ModuleName = 'Microsoft.PowerShell.SecretManagement'; RequiredVersion = '1.1.2' }

function Set-ContextSetting {
    <#
        .SYNOPSIS
        Sets a setting in a context.

        .DESCRIPTION
        Sets a setting in the specified context.

        .EXAMPLE
        Set-ContextSetting -Name 'ApiBaseUri' -Value 'https://api.github.com' -Context 'GitHub'

        Sets a setting called 'ApiBaseUri' in the context called 'GitHub'.

        .EXAMPLE
        $secret = 'myAccessToken' | ConvertTo-SecureString -AsPlainText -Force
        Set-ContextSetting -Name 'Secret' -Value $secret -Context 'GitHub'

        Sets a secret in the configuration context called 'GitHub'.
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The name of the setting to set.
        [Parameter(Mandatory)]
        [string] $Name,

        # The value to set for the specified setting. This can be a plain text string or a secure string.
        [Parameter(Mandatory)]
        [AllowNull()]
        [AllowEmptyString()]
        [object] $Value,

        # The name of the context where the setting will be set.
        [Parameter(Mandatory)]
        [Alias('ContextID')]
        [string] $ID
    )

    begin {
        $commandName = $MyInvocation.MyCommand.Name
        Write-Debug "[$commandName] - Start"
    }

    process {
        try {
            $context = Get-Context -ID $ID

            if (-not $context) {
                throw "Context [$ID] not found"
            }

            if ($PSCmdlet.ShouldProcess($Name, "Set value [$Value]")) {
                Write-Verbose "Setting [$Name] to [$Value] in [$ID]"
                if ($context.PSObject.Properties[$Name]) {
                    $context.$Name = $Value
                } else {
                    $context | Add-Member -NotePropertyName $Name -NotePropertyValue $Value -Force
                }
                Set-Context -Context $context -ID $ID
            }
        } catch {
            Write-Error $_
            throw 'Failed to set context setting'
        }
    }

    end {
        Write-Debug "[$commandName] - End"
    }
}

Register-ArgumentCompleter -CommandName Get-ContextSetting -ParameterName ID -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter

    Get-ContextInfo | Where-Object { $_.Name -like "$wordToComplete*" } |
        ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name)
        }
}

Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSetting] - [Set-ContextSetting] - Done"
#endregion - From [functions] - [public] - [ContextSetting] - [Set-ContextSetting]

Write-Verbose "[$scriptName] - [functions] - [public] - [ContextSetting] - Done"
#endregion - From [functions] - [public] - [ContextSetting]


Write-Verbose "[$scriptName] - [functions] - [public] - Done"
#endregion - From [functions] - [public]

#region - From [variables] - [private]
Write-Verbose "[$scriptName] - [variables] - [private] - Processing folder"

#region - From [variables] - [private] - [Config]
Write-Verbose "[$scriptName] - [variables] - [private] - [Config] - Importing"

$script:Config = [pscustomobject]@{
    SecretPrefix = 'Context:'                         # $script:Config.SecretPrefix
    VaultName    = 'SecretStore'                      # $script:Config.VaultName
    VaultType    = 'Microsoft.PowerShell.SecretStore' # $script:Config.VaultType
}

Write-Verbose "[$scriptName] - [variables] - [private] - [Config] - Done"
#endregion - From [variables] - [private] - [Config]

Write-Verbose "[$scriptName] - [variables] - [private] - Done"
#endregion - From [variables] - [private]

#region - From [loader]
Write-Verbose "[$scriptName] - [loader] - Importing"

try {
    Initialize-ContextVault
} catch {
    Write-Error $_
    throw 'Failed to initialize secret vault'
}
Write-Verbose "[$scriptName] - [loader] - Done"
#endregion - From [loader]


$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'Get-Context'
        'Remove-Context'
        'Rename-Context'
        'Set-Context'
        'Get-ContextSetting'
        'Remove-ContextSetting'
        'Set-ContextSetting'
    )
}
Export-ModuleMember @exports