Context.psm1

[CmdletBinding()]
param()
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
$script:PSModuleInfo = Test-ModuleManifest -Path "$PSScriptRoot\$baseName.psd1"
$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ }
$scriptName = $script:PSModuleInfo.Name
Write-Debug "[$scriptName] - Importing module"
#region [functions] - [private]
Write-Debug "[$scriptName] - [functions] - [private] - Processing folder"
#region [functions] - [private] - [Import-Context]
Write-Debug "[$scriptName] - [functions] - [private] - [Import-Context] - Importing"
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.1.1' }

filter Import-Context {
    <#
        .SYNOPSIS
        Imports the context vault into memory.

        .DESCRIPTION
        Imports all context files from the context vault directory into memory.
        Each context is decrypted using the configured private key and stored
        in the script-wide context collection for further use.

        .EXAMPLE
        Import-Context

        Output:
        ```powershell
        VERBOSE: Importing contexts from vault: [C:\Vault]
        VERBOSE: Found [3] contexts
        VERBOSE: Importing context: [123456]
        ```

        Imports all contexts from the context vault into memory.

        .OUTPUTS
        [pscustomobject].

        .NOTES
        Represents the imported context object containing ID, Path, and Context properties.

        .LINK
        https://psmodule.io/Sodium/Functions/Import-Context/
    #>

    [OutputType([object])]
    [CmdletBinding()]
    param()

    begin {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - Start"
        if (-not $script:Config.Initialized) {
            Set-ContextVault
        }
    }

    process {
        try {
            Write-Verbose "Importing contexts from vault: [$($script:Config.VaultPath)]"
            $contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse
            Write-Verbose "Found [$($contextFiles.Count)] contexts"
            $contextFiles | ForEach-Object {
                $contextInfo = Get-Content -Path $_.FullName | ConvertFrom-Json
                Write-Verbose "Importing context: [$($contextInfo.ID)]"
                Write-Verbose ($contextInfo | Format-List | Out-String)
                $params = @{
                    SealedBox  = $contextInfo.Context
                    PublicKey  = $script:Config.PublicKey
                    PrivateKey = $script:Config.PrivateKey
                }
                $context = ConvertFrom-SodiumSealedBox @params
                $script:Contexts[$contextInfo.ID] = [pscustomobject]@{
                    ID      = $contextInfo.ID
                    Path    = $contextInfo.Path
                    Context = ConvertFrom-ContextJson -JsonString $context
                }
            }
        } catch {
            Write-Error $_
            throw 'Failed to get context'
        }
    }

    end {
        Write-Debug "[$stackPath] - End"
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [Import-Context] - Done"
#endregion [functions] - [private] - [Import-Context]
#region [functions] - [private] - [Set-ContextVault]
Write-Debug "[$scriptName] - [functions] - [private] - [Set-ContextVault] - Importing"
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.1.1' }

function Set-ContextVault {
    <#
        .SYNOPSIS
        Sets the context vault.

        .DESCRIPTION
        Sets the context vault. If the vault does not exist, it will be initialized.
        Once the context vault is set, it will be imported into memory.
        The vault consists of multiple security shards, including a machine-specific shard,
        a user-specific shard, and a seed shard stored within the vault directory.

        .EXAMPLE
        Set-ContextVault

        Initializes or loads the context vault, setting up necessary key pairs.

        .OUTPUTS
        None.

        .NOTES
        This function modifies the script-scoped configuration and imports the vault.

        .LINK
        https://psmodule.io/Context/Functions/Set-ContextVault
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param()

    begin {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - Start"
    }

    process {
        try {
            Write-Verbose "Loading context vault from [$($script:Config.VaultPath)]"
            $vaultExists = Test-Path $script:Config.VaultPath
            Write-Verbose "Vault exists: $vaultExists"

            if (-not $vaultExists) {
                Write-Verbose 'Initializing new vault'
                $null = New-Item -Path $script:Config.VaultPath -ItemType Directory
            }

            Write-Verbose 'Checking for existing seed shard'
            $seedShardPath = Join-Path -Path $script:Config.VaultPath -ChildPath $script:Config.SeedShardPath
            $seedShardExists = Test-Path $seedShardPath
            Write-Verbose "Seed shard exists: $seedShardExists"

            if (-not $seedShardExists) {
                Write-Verbose 'Creating new seed shard'
                $keys = New-SodiumKeyPair
                Set-Content -Path $seedShardPath -Value "$($keys.PrivateKey)$($keys.PublicKey)"
            }

            $seedShard = Get-Content -Path $seedShardPath
            $machineShard = [System.Environment]::MachineName
            $userShard = [System.Environment]::UserName
            #$userInputShard = Read-Host -Prompt 'Enter a seed shard' # Eventually 4 shards. +1 for user input.
            $seed = $machineShard + $userShard + $seedShard + $userInputShard
            $keys = New-SodiumKeyPair -Seed $seed
            $script:Config.PrivateKey = $keys.PrivateKey
            $script:Config.PublicKey = $keys.PublicKey
            Write-Verbose 'Vault initialized'
            $script:Config.Initialized = $true
        } catch {
            Write-Error $_
            throw 'Failed to initialize context vault'
        }
    }

    end {
        Write-Debug "[$stackPath] - End"
        Import-Context
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [Set-ContextVault] - Done"
#endregion [functions] - [private] - [Set-ContextVault]
#region [functions] - [private] - [JsonToObject]
Write-Debug "[$scriptName] - [functions] - [private] - [JsonToObject] - Processing folder"
#region [functions] - [private] - [JsonToObject] - [Convert-ContextHashtableToObjectRecursive]
Write-Debug "[$scriptName] - [functions] - [private] - [JsonToObject] - [Convert-ContextHashtableToObjectRecursive] - Importing"
function Convert-ContextHashtableToObjectRecursive {
    <#
        .SYNOPSIS
        Converts a hashtable into a structured context object.

        .DESCRIPTION
        This function recursively converts a hashtable into a structured PowerShell object.
        String values prefixed with '[SECURESTRING]' are converted back to SecureString objects.
        Other values retain their original data types, including integers, booleans, strings, arrays,
        and nested objects.

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

        Output:
        ```powershell
        Name : Test
        Token : System.Security.SecureString
        Nested : @{ Name = Nested; Token = System.Security.SecureString }
        ```

        This example converts a hashtable into a structured object, where 'Token' and 'Nested.Token'
        values are SecureString objects.

        .OUTPUTS
        PSCustomObject.

        .NOTES
        Returns an object where values are converted to their respective types,
        including SecureString for sensitive values, arrays for list structures, and nested objects
        for hashtables.

        .LINK
        https://psmodule.io/Context/Functions/Convert-ContextHashtableToObjectRecursive
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingConvertToSecureStringWithPlainText', '',
        Justification = 'The SecureString is extracted from the object being processed by this function.'
    )]
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param (
        # Hashtable to convert into a structured context object
        [Parameter(Mandatory)]
        [hashtable] $Hashtable
    )

    begin {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - 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 [array], processing elements individually"
                    $result | Add-Member -NotePropertyName $key -NotePropertyValue @(
                        $value | ForEach-Object {
                            if ($_ -is [hashtable]) {
                                Convert-ContextHashtableToObjectRecursive $_
                            } else {
                                $_
                            }
                        }
                    )
                } else {
                    Write-Debug "Adding [$key] as a standard value"
                    $result | Add-Member -NotePropertyName $key -NotePropertyValue $value
                }
            }
            return $result
        } catch {
            Write-Error $_
            throw 'Failed to convert hashtable to object'
        }
    }

    end {
        Write-Debug "[$stackPath] - End"
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [JsonToObject] - [Convert-ContextHashtableToObjectRecursive] - Done"
#endregion [functions] - [private] - [JsonToObject] - [Convert-ContextHashtableToObjectRecursive]
#region [functions] - [private] - [JsonToObject] - [ConvertFrom-ContextJson]
Write-Debug "[$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. Text prefixed with `[SECURESTRING]` is converted to SecureString objects.
        Other values are converted to their original types, such as integers, booleans, strings, arrays, and nested objects.

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

        Output:
        ```powershell
        Name : Test
        Token : System.Security.SecureString
        Nested : @{Name=Nested; Token=System.Security.SecureString}
        ```

        Converts a JSON string to a context object, ensuring 'Token' and 'Nested.Token' values are SecureString objects.

        .OUTPUTS
        [pscustomobject].

        .NOTES
        Returns a PowerShell custom object with SecureString conversion applied where necessary.

        .LINK
        https://psmodule.io/Context/Functions/ConvertFrom-ContextJson/
    #>

    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param (
        # JSON string to convert to context object
        [Parameter()]
        [string] $JsonString = '{}'
    )

    begin {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - 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 "[$stackPath] - End"
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [JsonToObject] - [ConvertFrom-ContextJson] - Done"
#endregion [functions] - [private] - [JsonToObject] - [ConvertFrom-ContextJson]
Write-Debug "[$scriptName] - [functions] - [private] - [JsonToObject] - Done"
#endregion [functions] - [private] - [JsonToObject]
#region [functions] - [private] - [ObjectToJson]
Write-Debug "[$scriptName] - [functions] - [private] - [ObjectToJson] - Processing folder"
#region [functions] - [private] - [ObjectToJson] - [Convert-ContextObjectToHashtableRecursive]
Write-Debug "[$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
            }
        })

        Output:
        ```powershell
        Name : MySecret
        AccessToken : [SECURESTRING]123123123
        Nested : @{Name=MyNestedSecret; NestedAccessToken=[SECURESTRING]123123123}
        ```

        Converts the context object to a hashtable. Secure strings are converted to a string representation.

        .OUTPUTS
        hashtable.

        .NOTES
        Returns a hashtable representation of the input object.
        Secure strings are converted to prefixed string values.

        .LINK
        https://psmodule.io/Context/Functions/Convert-ContextObjectToHashtableRecursive
    #>

    [OutputType([hashtable])]
    [CmdletBinding()]
    param (
        # The object to convert.
        [Parameter()]
        [object] $Object = @{}
    )

    begin {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - 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 "[$stackPath] - End"
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [ObjectToJson] - [Convert-ContextObjectToHashtableRecursive] - Done"
#endregion [functions] - [private] - [ObjectToJson] - [Convert-ContextObjectToHashtableRecursive]
#region [functions] - [private] - [ObjectToJson] - [ConvertTo-ContextJson]
Write-Debug "[$scriptName] - [functions] - [private] - [ObjectToJson] - [ConvertTo-ContextJson] - Importing"
function ConvertTo-ContextJson {
    <#
        .SYNOPSIS
        Converts an object into a JSON string.

        .DESCRIPTION
        Converts objects or hashtables into a JSON string. SecureStrings are converted to plain text strings and
        prefixed with `[SECURESTRING]`. The conversion is recursive for any nested objects. The function allows
        converting back using `ConvertFrom-ContextJson`.

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

        Output:
        ```json
        {
            "Name": "MySecret",
            "AccessToken": "[SECURESTRING]123123123",
            "ID": "CTX-001"
        }
        ```

        Converts the given object into a JSON string, ensuring SecureStrings are handled properly.

        .OUTPUTS
        System.String.

        .NOTES
        A JSON string representation of the provided object, including secure string transformations.

        .LINK
        https://psmodule.io/Context/Functions/ConvertTo-ContextJson
    #>

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

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

    begin {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - 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 "[$stackPath] - End"
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [ObjectToJson] - [ConvertTo-ContextJson] - Done"
#endregion [functions] - [private] - [ObjectToJson] - [ConvertTo-ContextJson]
Write-Debug "[$scriptName] - [functions] - [private] - [ObjectToJson] - Done"
#endregion [functions] - [private] - [ObjectToJson]
#region [functions] - [private] - [Utilities]
Write-Debug "[$scriptName] - [functions] - [private] - [Utilities] - Processing folder"
#region [functions] - [private] - [Utilities] - [PowerShell]
Write-Debug "[$scriptName] - [functions] - [private] - [Utilities] - [PowerShell] - Processing folder"
#region [functions] - [private] - [Utilities] - [PowerShell] - [Get-PSCallStackPath]
Write-Debug "[$scriptName] - [functions] - [private] - [Utilities] - [PowerShell] - [Get-PSCallStackPath] - Importing"
function Get-PSCallStackPath {
    <#
        .SYNOPSIS
        Creates a string representation of the current call stack.

        .DESCRIPTION
        This function generates a string representation of the current call stack.
        It allows skipping the first and last elements of the call stack using the `SkipFirst`
        and `SkipLatest` parameters. By default, it skips the first function (typically `<ScriptBlock>`)
        and the last function (`Get-PSCallStackPath`) to present a cleaner view of the actual call stack.

        .EXAMPLE
        Get-PSCallStackPath

        Output:
        ```powershell
        First-Function\Second-Function\Third-Function
        ```

        Returns the call stack with the first (`<ScriptBlock>`) and last (`Get-PSCallStackPath`)
        functions removed.

        .EXAMPLE
        Get-PSCallStackPath -SkipFirst 0

        Output:
        ```powershell
        <ScriptBlock>\First-Function\Second-Function\Third-Function
        ```

        Includes the first function (typically `<ScriptBlock>`) in the call stack output.

        .EXAMPLE
        Get-PSCallStackPath -SkipLatest 0

        Output:
        ```powershell
        First-Function\Second-Function\Third-Function\Get-PSCallStackPath
        ```

        Includes the last function (`Get-PSCallStackPath`) in the call stack output.

        .OUTPUTS
        System.String.

        .NOTES
        A string representing the call stack path, with function names separated by backslashes.

        .LINK
        https://psmodule.io/PSCallStack/Functions/Get-PSCallStackPath/
    #>

    [CmdletBinding()]
    param(
        # The number of functions to skip from the last function called.
        # The last function in the stack is this function (`Get-PSCallStackPath`).
        [Parameter()]
        [int] $SkipLatest = 1,

        # The number of functions to skip from the first function called.
        # The first function is typically `<ScriptBlock>`.
        [Parameter()]
        [int] $SkipFirst = 1
    )

    $skipFirst++
    $cmds = (Get-PSCallStack).Command
    $functionPath = $cmds[($cmds.Count - $skipFirst)..$SkipLatest] -join '\'
    $functionPath = $functionPath -replace '^.*<ScriptBlock>\\'
    $functionPath = $functionPath -replace '^.*.ps1\\'
    return $functionPath
}
Write-Debug "[$scriptName] - [functions] - [private] - [Utilities] - [PowerShell] - [Get-PSCallStackPath] - Done"
#endregion [functions] - [private] - [Utilities] - [PowerShell] - [Get-PSCallStackPath]
Write-Debug "[$scriptName] - [functions] - [private] - [Utilities] - [PowerShell] - Done"
#endregion [functions] - [private] - [Utilities] - [PowerShell]
Write-Debug "[$scriptName] - [functions] - [private] - [Utilities] - Done"
#endregion [functions] - [private] - [Utilities]
Write-Debug "[$scriptName] - [functions] - [private] - Done"
#endregion [functions] - [private]
#region [functions] - [public]
Write-Debug "[$scriptName] - [functions] - [public] - Processing folder"
#region [functions] - [public] - [Get-Context]
Write-Debug "[$scriptName] - [functions] - [public] - [Get-Context] - Importing"
function Get-Context {
    <#
        .SYNOPSIS
        Retrieves a context from the in-memory context vault.

        .DESCRIPTION
        Retrieves a context from the loaded contexts stored in memory.
        If no ID is specified, all available contexts will be returned.
        Wildcards are supported to match multiple contexts.

        .EXAMPLE
        Get-Context

        Output:
        ```powershell
        Repositories : {@{Languages=System.Object[]; IsPrivate=False; Stars=130;
                            CreatedDate=2/9/2024 10:45:11 AM; Name=Repo2}}
        AccessScopes : {repo, user, gist, admin:org}
        AuthToken : MyFirstSuperSecretToken
        TwoFactorMethods : {TOTP, SMS}
        IsTwoFactorAuth : True
        ApiRateLimits : @{ResetTime=2/9/2025 11:15:11 AM; Remaining=4985; Limit=5000}
        UserPreferences : @{CodeReview=System.Object[]; Notifications=; Theme=dark; DefaultBranch=main}
        SessionMetaData : @{Device=Windows-PC; Location=; BrowserInfo=; SessionID=sess_abc123}
        LastLoginAttempts : {@{Success=True; Timestamp=2/9/2025 9:45:11 AM; IP=192.168.1.101}, @{Success=False}}
        ID : GitHub/User-3
        Username : john_doe
        LoginTime : 2/9/2025 10:45:11 AM

        Repositories : {@{Languages=System.Object[]; IsPrivate=False; Stars=130;
                            CreatedDate=2/9/2024 10:45:11 AM; Name=Repo2}}
        AccessScopes : {repo, user, gist, admin:org}
        AuthToken : MySuperSecretToken
        TwoFactorMethods : {TOTP, SMS}
        IsTwoFactorAuth : True
        ApiRateLimits : @{ResetTime=2/9/2025 11:15:11 AM; Remaining=4985; Limit=5000}
        UserPreferences : @{CodeReview=System.Object[]; Notifications=; Theme=dark; DefaultBranch=main}
        SessionMetaData : @{Device=Windows-PC; Location=; BrowserInfo=; SessionID=sess_abc123}
        LastLoginAttempts : {@{Success=True; Timestamp=2/9/2025 9:45:11 AM; IP=192.168.1.101}, @{Success=False}}
        ID : GitHub/User-8
        Username : jane_doe
        LoginTime : 2/9/2025 10:45:11 AM
        ```

        Retrieves all contexts from the context vault (in memory).

        .EXAMPLE
        Get-Context -ID 'MySecret'

        Retrieves the context called 'MySecret' from the vault.

        .EXAMPLE
        'My*' | Get-Context

        Output:
        ```powershell
        ID : MyConfig
        Config : {ConfigKey=ConfigValue}

        ID : MySecret
        Key : EncryptedValue
        AuthToken : EncryptedToken
        Favorite : {Color=Blue; Number=7}

        ID : MySettings
        Setting : {SettingKey=SettingValue}
        Config : {ConfigKey=ConfigValue}
        YourData : {DataKey=DataValue}
        ```

        Retrieves all contexts that start with 'My' from the context vault (in memory).

        .OUTPUTS
        [System.Object]

        .NOTES
        Returns a list of contexts matching the specified ID or all contexts if no ID is specified.
        Each context object contains its ID and corresponding stored properties.

        .LINK
        https://psmodule.io/Context/Functions/Get-Context/
    #>

    [OutputType([object])]
    [CmdletBinding()]
    param(
        # The name of the context to retrieve from the vault. Supports wildcards.
        [Parameter(
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [AllowEmptyString()]
        [SupportsWildcards()]
        [string[]] $ID = '*'
    )

    begin {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - Start"

        if (-not $script:Config.Initialized) {
            Set-ContextVault
        }
    }

    process {
        Write-Verbose "Retrieving contexts - ID: [$($ID -join ', ')]"
        foreach ($item in $ID) {
            Write-Verbose "Retrieving contexts - ID: [$item]"
            $script:Contexts.Values | Where-Object { $_.ID -like $item } | Select-Object -ExpandProperty Context
        }
    }

    end {
        Write-Debug "[$stackPath] - End"
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Get-Context] - Done"
#endregion [functions] - [public] - [Get-Context]
#region [functions] - [public] - [Get-ContextInfo]
Write-Debug "[$scriptName] - [functions] - [public] - [Get-ContextInfo] - Importing"
function Get-ContextInfo {
    <#
        .SYNOPSIS
        Retrieves info about a context from the in-memory context vault.

        .DESCRIPTION
        Retrieves info about context from the loaded contexts stored in memory.
        If no ID is specified, all available info on contexts will be returned.
        Wildcards are supported to match multiple contexts.

        .EXAMPLE
        Get-ContextInfo

        Output:
        ```powershell
        ID : MySettings
        Path : ...\b7c01dbe-bccd-4c7e-b075-c5aac1c43b1a.json

        ID : MyConfig
        Path : ...\feacc853-5bea-48d1-b751-41ce9768d48e.json

        ID : MySecret
        Path : ...\3e223259-f242-4e97-91c8-f0fd054cfea7.json

        ID : Data
        Path : ...\b7c01dbe-bccd-4c7e-b075-c5aac1c43b1a.json

        ID : PSModule.GitHub
        Path : ...\feacc853-5bea-48d1-b751-41ce9768d48e.json
        ```

        Retrieves all contexts from the context vault (in memory).

        .EXAMPLE
        Get-ContextInfo -ID 'MySecret'

        Output:
        ```powershell
        ID : MySecret
        Path : ...\3e223259-f242-4e97-91c8-f0fd054cfea7.json
        ```

        Retrieves the context called 'MySecret' from the context vault (in memory).

        .EXAMPLE
        'My*' | Get-ContextInfo

        Output:
        ```powershell
        ID : MyConfig
        Path : .../feacc853-5bea-48d1-b751-41ce9768d48e.json

        ID : MySecret
        Path : .../3e223259-f242-4e97-91c8-f0fd054cfea7.json

        ID : MySettings
        Path : .../b7c01dbe-bccd-4c7e-b075-c5aac1c43b1a.json
        ```

        Retrieves all contexts that start with 'My' from the context vault (in memory).

        .OUTPUTS
        [System.Object]

        .NOTES
        Returns a list of context information matching the specified ID or all contexts if no ID is specified.
        Each context object contains its ID and corresponding path to where the context is stored on disk.

        .LINK
        https://psmodule.io/Context/Functions/Get-ContextInfo/
    #>

    [OutputType([object])]
    [CmdletBinding()]
    param(
        # The name of the context to retrieve from the vault. Supports wildcards.
        [Parameter(
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [AllowEmptyString()]
        [SupportsWildcards()]
        [string[]] $ID = '*'
    )

    begin {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - Start"

        if (-not $script:Config.Initialized) {
            Set-ContextVault
        }
    }

    process {
        Write-Verbose "Retrieving context info - ID: [$ID]"
        foreach ($item in $ID) {
            $script:Contexts.Values | Where-Object { $_.ID -like $item } | Select-Object -ExcludeProperty Context
        }
    }

    end {
        Write-Debug "[$stackPath] - End"
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Get-ContextInfo] - Done"
#endregion [functions] - [public] - [Get-ContextInfo]
#region [functions] - [public] - [Remove-Context]
Write-Debug "[$scriptName] - [functions] - [public] - [Remove-Context] - Importing"
function Remove-Context {
    <#
        .SYNOPSIS
        Removes a context from the context vault.

        .DESCRIPTION
        This function removes a context (or multiple contexts) from the vault. It supports:
        - Supply one or more IDs as strings (e.g. -ID 'Ctx1','Ctx2')
        - Supply objects that contain an ID property

        The function accepts pipeline input for easier batch removal.

        .EXAMPLE
        Remove-Context -ID 'MySecret'

        Output:
        ```powershell
        Removing context [MySecret]
        Removed item: MySecret
        ```

        Removes a context called 'MySecret' by specifying its ID.

        .EXAMPLE
        Remove-Context -ID 'Ctx1','Ctx2'

        Output:
        ```powershell
        Removing context [Ctx1]
        Removed item: Ctx1
        Removing context [Ctx2]
        Removed item: Ctx2
        ```

        Removes two contexts, 'Ctx1' and 'Ctx2'.

        .EXAMPLE
        'Ctx1','Ctx2' | Remove-Context

        Output:
        ```powershell
        Removing context [Ctx1]
        Removed item: Ctx1
        Removing context [Ctx2]
        Removed item: Ctx2
        ```

        Removes two contexts, 'Ctx1' and 'Ctx2' via pipeline input.

        .EXAMPLE
        $ctxList = @(
            [PSCustomObject]@{ ID = 'Ctx1' },
            [PSCustomObject]@{ ID = 'Ctx2' }
        )
        $ctxList | Remove-Context

        Output:
        ```powershell
        Removing context [Ctx1]
        Removed item: Ctx1
        Removing context [Ctx2]
        Removed item: Ctx2
        ```

        Accepts pipeline input: multiple objects each having an ID property.

        .OUTPUTS
        [System.String]

        .NOTES
        Returns the name of each removed context if successful.

        .LINK
        https://psmodule.io/Context/Functions/Remove-Context/
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        # One or more IDs as strings of the contexts to remove.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [SupportsWildcards()]
        [string[]] $ID
    )

    begin {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - Begin"

        if (-not $script:Config.Initialized) {
            Set-ContextVault
        }
    }

    process {
        foreach ($item in $ID) {
            Write-Verbose "Processing ID [$item]"
            $script:Contexts.Keys | Where-Object { $_ -like $item } | ForEach-Object {
                Write-Verbose "Removing context [$_]"
                if ($PSCmdlet.ShouldProcess($_, 'Remove secret')) {
                    $script:Contexts[$_].Path | Remove-Item -Force

                    Write-Verbose "Attempting to remove context: $_"
                    [PSCustomObject]$removedItem = $null
                    if ($script:Contexts.TryRemove($_, [ref]$removedItem)) {
                        Write-Verbose "Removed item: $removedItem"
                    } else {
                        Write-Verbose 'Key not found'
                    }
                }
            }
        }
    }

    end {
        Write-Debug "[$stackPath] - End"
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Remove-Context] - Done"
#endregion [functions] - [public] - [Remove-Context]
#region [functions] - [public] - [Rename-Context]
Write-Debug "[$scriptName] - [functions] - [public] - [Rename-Context] - Importing"
function Rename-Context {
    <#
        .SYNOPSIS
        Renames a context.

        .DESCRIPTION
        This function renames a context by retrieving the existing context with the old ID,
        setting the new context with the provided new ID, and removing the old context.
        If a context with the new ID already exists, the operation will fail unless
        the `-Force` switch is specified.

        .EXAMPLE
        Rename-Context -ID 'PSModule.GitHub' -NewID 'PSModule.GitHub2'

        Output:
        ```powershell
        Context 'PSModule.GitHub' renamed to 'PSModule.GitHub2'
        ```

        Renames the context 'PSModule.GitHub' to 'PSModule.GitHub2'.

        .EXAMPLE
        'PSModule.GitHub' | Rename-Context -NewID 'PSModule.GitHub2'

        Output:
        ```powershell
        Context 'PSModule.GitHub' renamed to 'PSModule.GitHub2'
        ```

        Renames the context 'PSModule.GitHub' to 'PSModule.GitHub2' using pipeline input.

        .OUTPUTS
        [System.String]

        .NOTES
        The confirmation message indicating the successful renaming of the context.

        .LINK
        https://psmodule.io/Context/Functions/Rename-Context/
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param (
        # The ID of the context to rename.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [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 {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - Start"

        if (-not $script:Config.Initialized) {
            Set-ContextVault
        }
    }

    process {
        $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."
        }

        if ($PSCmdlet.ShouldProcess("Renaming context '$ID' to '$NewID'")) {
            $context | Set-Context -ID $NewID
            Remove-Context -ID $ID
        }
    }

    end {
        Write-Debug "[$stackPath] - End"
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Rename-Context] - Done"
#endregion [functions] - [public] - [Rename-Context]
#region [functions] - [public] - [Set-Context]
Write-Debug "[$scriptName] - [functions] - [public] - [Set-Context] - Importing"
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.1.1' }

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.
        The context is cached in memory for faster access. This function ensures that the context
        is securely stored using encryption mechanisms.

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

        Output:
        ```powershell
        ID : PSModule.GitHub
        Path : C:\Vault\Guid.json
        Context : @{ Name = 'MySecret' }
        ```

        Creates a context called 'MySecret' in the vault.

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

        Output:
        ```powershell
        ID : PSModule.GitHub
        Path : C:\Vault\Guid.json
        Context : @{ Name = 'MySecret'; AccessToken = '123123123' }
        ```

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

        .EXAMPLE
        $context = @{ ID = 'MySecret'; Name = 'SomeSecretIHave'; AccessToken = '123123123' }
        $context | Set-Context

        Sets a context using a hashtable object.

        .OUTPUTS
        [PSCustomObject]

        .NOTES
        Returns an object representing the stored or updated context.
        The object includes the ID, path, and securely stored context information.

        .LINK
        https://psmodule.io/Context/Functions/Set-Context/
    #>

    [OutputType([object])]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The ID of the context.
        [Parameter()]
        [string] $ID,

        # The data of the context.
        [Parameter(ValueFromPipeline)]
        [object] $Context = @{},

        # Pass the context through the pipeline.
        [Parameter()]
        [switch] $PassThru
    )

    begin {
        $stackPath = Get-PSCallStackPath
        Write-Debug "[$stackPath] - Start"

        if (-not $script:Config.Initialized) {
            Set-ContextVault
        }
    }

    process {
        if ($context -is [System.Collections.IDictionary]) {
            $Context = [PSCustomObject]$Context
        }

        if (-not $ID) {
            $ID = $Context.ID
        }
        if (-not $ID) {
            throw "An ID is required, either as a parameter or as a property of the context object."
        }
        $existingContextInfo = $script:Contexts[$ID]
        if (-not $existingContextInfo) {
            Write-Verbose "Context [$ID] not found in vault"
            $Guid = [Guid]::NewGuid().ToString()
            $Path = Join-Path -Path $script:Config.VaultPath -ChildPath "$Guid.json"
        } else {
            Write-Verbose "Context [$ID] found in vault"
            $Path = $existingContextInfo.Path
        }

        $contextJson = ConvertTo-ContextJson -Context $Context -ID $ID

        $param = [pscustomobject]@{
            ID      = $ID
            Path    = $Path
            Context = ConvertTo-SodiumSealedBox -Message $contextJson -PublicKey $script:Config.PublicKey
        } | ConvertTo-Json -Depth 5
        Write-Debug ($param | ConvertTo-Json -Depth 5)

        if ($PSCmdlet.ShouldProcess($ID, 'Set context')) {
            Write-Verbose "Setting context [$ID] in vault"
            Set-Content -Path $Path -Value $param
            $content = Get-Content -Path $Path
            $contextInfoObj = $content | ConvertFrom-Json
            $params = @{
                SealedBox  = $contextInfoObj.Context
                PublicKey  = $script:Config.PublicKey
                PrivateKey = $script:Config.PrivateKey
            }
            $contextObj = ConvertFrom-SodiumSealedBox @params
            Write-Verbose ($contextObj | Format-List | Out-String)
            $script:Contexts[$ID] = [PSCustomObject]@{
                ID      = $ID
                Path    = $Path
                Context = ConvertFrom-ContextJson -JsonString $contextObj
            }
        }

        if ($PassThru) {
            Get-Context -ID $ID
        }
    }

    end {
        Write-Debug "[$stackPath] - End"
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Set-Context] - Done"
#endregion [functions] - [public] - [Set-Context]
Write-Debug "[$scriptName] - [functions] - [public] - Done"
#endregion [functions] - [public]
#region [variables] - [private]
Write-Debug "[$scriptName] - [variables] - [private] - Processing folder"
#region [variables] - [private] - [Config]
Write-Debug "[$scriptName] - [variables] - [private] - [Config] - Importing"
$script:Config = [pscustomobject]@{
    Initialized   = $false                                           # Has the vault been initialized?
    VaultPath     = Join-Path -Path $HOME -ChildPath '.contextvault' # Vault directory path
    SeedShardPath = 'vault.shard'                                  # Seed shard path (relative to VaultPath)
    PrivateKey    = $null                                            # Private key (populated on init)
    PublicKey     = $null                                            # Public key (populated on init)
}
Write-Debug "[$scriptName] - [variables] - [private] - [Config] - Done"
#endregion [variables] - [private] - [Config]
#region [variables] - [private] - [Contexts]
Write-Debug "[$scriptName] - [variables] - [private] - [Contexts] - Importing"
# Using a dictionary to get the benefit of reference type storage in PowerShell while multiple functions are running in parallel.
# Using concurrent dictionary to be able to safely access the dictionary from multiple threads /ForEach -Parallel.
$script:Contexts = [System.Collections.Concurrent.ConcurrentDictionary[string, [PSCustomObject]]]::new()
Write-Debug "[$scriptName] - [variables] - [private] - [Contexts] - Done"
#endregion [variables] - [private] - [Contexts]
Write-Debug "[$scriptName] - [variables] - [private] - Done"
#endregion [variables] - [private]
#region [completers]
Write-Debug "[$scriptName] - [completers] - Importing"
Register-ArgumentCompleter -CommandName 'Get-Context', 'Set-Context', 'Remove-Context', 'Rename-Context' -ParameterName 'ID' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter

    $script:Contexts.Values.ID | Where-Object { $_ -like "$wordToComplete*" } |
        ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
        }
}
Write-Debug "[$scriptName] - [completers] - Done"
#endregion [completers]
#region [main]
Write-Debug "[$scriptName] - [main] - Importing"
Set-ContextVault
Write-Debug "[$scriptName] - [main] - Done"
#endregion [main]

#region Member exporter
$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'Get-Context'
        'Get-ContextInfo'
        'Remove-Context'
        'Rename-Context'
        'Set-Context'
    )
}
Export-ModuleMember @exports
#endregion Member exporter