diff --git a/README.md b/README.md index 9dcbc41b..00a1f92a 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,44 @@ # Context Modules typically handle two types of data that benefit from persistent secure storage and management: module settings and user settings and secrets. -This module introduces the concept of `Contexts`, which enable persistent and secure data storage for PowerShell modules. It allows module developers to -separate user and module data from the module code, enabling users to resume their work without needing to reconfigure the module or log in again, +This module introduces the concept of `Contexts`, which enable persistent and secure data storage for PowerShell modules. It allows module developers +to separate user and module data from the module code, enabling users to resume their work without needing to reconfigure the module or log in again, provided the service supports session refresh mechanisms (e.g., refresh tokens). -The module uses NaCl-based encryption, provided by the `libsodium` library, to encrypt and decrypt `Context` data. The module that delivers this -functionality is called [`Sodium`](https://github.com/PSModule/Sodium) and is a dependency of this module. The -[`Sodium`](https://github.com/PSModule/Sodium) module is automatically installed when you install this module. +The module uses NaCl-based encryption, provided by the `libsodium` library (delivered via the [`Sodium`](https://github.com/PSModule/Sodium) module), +to encrypt and decrypt `Context` data. The [`Sodium`](https://github.com/PSModule/Sodium) module is automatically installed when you install this +module. ## What is a `Context`? The concept of `Context` is widely used to represent a collection of data that is relevant to a specific use-case. In this module, a `Context` is a way to securely persist user and module data and offers a set of functions to manage this across modules that implement it. Data that is stored in a `Context` can include user-specific settings, secrets, and module configuration data. -A `Context` is identified by a unique ID, which is typically a string that represents the module and user of a module (e.g., `GitHub/john_doe`), but -this is just an example. Any data that can be represented in JSON format can be stored in a `Context`. +A `Context` is identified by a unique ID in the module that implements it. +Any data that can be represented in JSON format can be stored in a `Context`. + +## Grouping Contexts into Vaults + +Contexts can be grouped into `ContextVaults`, which are logical containers for related contexts. This allows for organization and management of +contexts, especially when dealing with multiple users or modules. Vaults are automatically created when you store a context, and they can be managed +using the provided functions in this module. + +### Directory Structure + +```plaintext +$HOME/.contextvaults/ +├── GitHub/ +│ ├── 64a5bbaf-96b8-4090-a77d-75e02ab6c4e0.json +│ ├── f201dc50-c163-4a7a-8d69-aea7f696737d.json +│ └── shard +├── AzureDevOps/ +│ ├── cf49fceb-38d1-47da-a0ae-219ac40e4d8c.json +│ ├── b521a424-dd1c-445b-a0d6-c26a29d93654.json +│ └── shard +``` + +In this example there are two named vaults (`GitHub` and `AzureDevOps`). Each vault contains its own `shard` file (for encryption) and two context +files (with unique GUID filenames) that store encrypted context data. Contexts in different vaults are completely isolated from each other. ### 1. Storing data (object or dictionary) to disk using `Set-Context` @@ -23,7 +46,7 @@ To store data to disk, use the `Set-Context` function. The function needs an ID The object can be anything that can be converted and represented in JSON format. ```pwsh -Set-Context -ID 'john_doe' -Context ([PSCustomObject]@{ +Set-Context -ID 'john_doe' -Vault 'GitHub' -Context ([PSCustomObject]@{ Username = 'john_doe' AuthToken = 'ghp_12345ABCDE67890FGHIJ' | ConvertTo-SecureString -AsPlainText -Force # gitleaks:allow LoginTime = Get-Date @@ -51,15 +74,16 @@ JSON string. ### 3. Storing the context object to disk -When the data is primed for storage, it is finally encrypted using the `Sodium` module and saved to disk. The file is stored in the user's home -directory `$HOME\.contextvault\.json`, where `` is a generated GUID, providing a unique name for the file. +Finally the data is encrypted using the `Sodium` module and saved to disk. The file is stored in the user's home +directory `$HOME/.contextvaults//.json`, where `` is a generated GUID, providing a unique name for the file. The encrypted JSON representation of the data is added to metadata object that holds other info such as the ID of the `Context` and the path to the file where it is stored. ```json { - "ID": "PSModule.GitHub/github.com/john_doe", - "Path": "C:\\Users\\JohnDoe\\.contextvault\\d2edaa6e-95a1-41a0-b6ef-0ecc5d116030.json", + "ID": "github.com/john_doe", + "Vault": "GitHub", + "Path": "C:\\Users\\JohnDoe\\.contextvaults\\GitHub\\d2edaa6e-95a1-41a0-b6ef-0ecc5d116030.json", "Context": "0kGmtbQiEtih7 --< encrypted context data >-- ceqbMiBilUvEzO1Lk" } ``` @@ -75,89 +99,209 @@ Install-PSResource -Name Context -TrustRepository -Repository PSGallery Import-Module -Name Context ``` -## Usage +## Implementation Guide for Module Developers -Let's take a closer look at how to store these types of data using the module. +This section shows how to integrate the Context module into your PowerShell module to provide persistent, secure storage for module settings and user data. -### Module Settings +### Quick Start -A module developer can create additional `Contexts` for settings that share the same lifecycle, such as those associated with a module extension. +The simplest way to implement Contexts is using `Set-Context`, which automatically creates the vault if it doesn't exist: -For example, if we have a module called `GitHub` that needs to store some settings, the module developer could initialize a `Context` called `GitHub` -as part of the loading section in the module code. The module configuration is accessed using the ID `GitHub`. +```pwsh +# Store module configuration - vault is created automatically +Set-Context -ID 'ModuleSettings' -Vault 'MyModule' -Context @{ + DefaultApiEndpoint = 'https://api.example.com' + TimeoutSeconds = 30 +} -### User Configuration +# Store user credentials +Set-Context -ID 'User.JohnDoe' -Vault 'MyModule' -Context @{ + Username = 'johndoe' + ApiKey = 'secret-key' | ConvertTo-SecureString -AsPlainText -Force + LastLogin = Get-Date +} +``` -To store user data, a module developer can create a `Context` that serves as a "namespace" for user-specific configurations. +### Best Practices for Module Integration -Imagine a user named `BobMarley` logs into the `GitHub` module. The following logical structure would be created: +#### 1. Create Wrapper Functions -- `GitHub`: Contains module configuration, like default user, host, and client ID. -- `GitHub/BobMarley`: Contains user configuration, secrets, and default values for API calls. +Create module-specific wrapper functions to provide a familiar interface for your users: -If the same user logs in with another account (`LennyKravitz`), an additional `Context` will be created: +```pwsh +# In your module +function Set-MyModuleContext { + param( + [Parameter(Mandatory)] + [string] $ID, -- `GitHub/LennyKravitz`: Contains user-specific settings and secrets. + [Parameter(Mandatory)] + [object] $Context + ) -This allows users to set a default `Context`, storing its name in the module `Context`, enabling automatic login to the correct account when the -module loads. Users can also switch between accounts by changing the default `Context`. + Set-Context -ID $ID -Vault 'MyModule' -Context $Context +} -### Setup for a New Module +function Get-MyModuleContext { + param( + [string] $ID = '*' + ) -1. Create a new context for the module: + Get-Context -ID $ID -Vault 'MyModule' +} +``` + +#### 2. Module Configuration Pattern + +Store module-wide settings that persist across sessions: ```pwsh -Set-Context -ID 'GitHub' -Context @{ Name = 'GitHub' } +# Initialize module settings on first load +if (-not (Get-Context -ID 'Settings' -Vault 'MyModule' -ErrorAction SilentlyContinue)) { + Set-Context -ID 'Settings' -Vault 'MyModule' -Context @{ + DefaultUser = $null + ApiEndpoint = 'https://api.example.com' + EnableLogging = $false + } +} ``` -2. Add module configuration: +#### 3. User Context Pattern + +Handle multiple user accounts within your module: ```pwsh -$context = Get-Context -ID 'GitHub' -# Modify settings as needed -Set-Context -ID 'GitHub' -Context $context +# Store user-specific data +function Connect-MyService { + param( + [Parameter(Mandatory)] + [string] $Username, + + [Parameter(Mandatory)] + [SecureString] $ApiKey + ) + + # Store user context + Set-Context -ID "User.$Username" -Vault 'MyModule' -Context @{ + Username = $Username + ApiKey = $ApiKey + ConnectedAt = Get-Date + } + + # Update module settings to remember the current user + $moduleSettings = Get-Context -ID 'Settings' -Vault 'MyModule' + $moduleSettings.DefaultUser = $Username + Set-Context -ID 'Settings' -Vault 'MyModule' -Context $moduleSettings +} ``` -### Setup for a New User Context +### Complete Example -1. Create a set of public integration functions using the `Context` module to store user data. This is highly recommended, as it allows module -developers to define a structured `Context` while providing users with familiar function names for interaction. - - `Set-Context` that uses `Set-Context`. - - `Get-Context` that uses `Get-Context`. - - `Remove-Context` that uses `Remove-Context`. - -2. Create a new `Context` for the user: +Here's a complete example of how to implement Contexts in a hypothetical GitHub module: ```pwsh -Connect-GitHub ... -Set-Context -ID 'GitHub.BobMarley' +# Module initialization (in .psm1 file) +$VaultName = 'GitHub' + +# Initialize module settings if they don't exist +if (-not (Get-Context -ID 'ModuleSettings' -Vault $VaultName -ErrorAction SilentlyContinue)) { + Set-Context -ID 'ModuleSettings' -Vault $VaultName -Context @{ + DefaultOrganization = $null + ApiEndpoint = 'https://api.github.com' + DefaultUser = $null + } +} + +# Public function to connect user +function Connect-GitHub { + param( + [Parameter(Mandatory)] + [string] $Username, + + [Parameter(Mandatory)] + [string] $Token + ) + + # Store user context with secure token + Set-Context -ID "User.$Username" -Vault $VaultName -Context @{ + Username = $Username + Token = $Token | ConvertTo-SecureString -AsPlainText -Force + Organizations = @() + LastConnected = Get-Date + } + + # Set as default user + $settings = Get-Context -ID 'ModuleSettings' -Vault $VaultName + $settings.DefaultUser = $Username + Set-Context -ID 'ModuleSettings' -Vault $VaultName -Context $settings + + Write-Host "Connected to GitHub as $Username" +} + +# Public function to get current user context +function Get-GitHubUser { + $settings = Get-Context -ID 'ModuleSettings' -Vault $VaultName + if ($settings.DefaultUser) { + Get-Context -ID "User.$($settings.DefaultUser)" -Vault $VaultName + } +} ``` -3. Modify user configuration: +### Key Implementation Points + +- **Automatic Vault Creation**: `Set-Context` creates the vault automatically if it doesn't exist, and preserves existing encryption keys +- **Consistent Vault Naming**: Use your module name as the vault name for organization +- **Wrapper Functions**: Provide module-specific functions that hide the vault parameter from users +- **SecureString Support**: The Context module automatically handles `SecureString` encryption and decryption +- **Module Settings**: Store module-wide configuration separate from user-specific data + +## Vault Management (Advanced) + +For most use cases, you don't need to manage vaults directly since `Set-Context` creates them automatically. However, you can manage vaults explicitly when needed: ```pwsh -$context = Get-Context -ID 'GitHub.BobMarley' -# Modify settings -Set-Context -ID 'GitHub.BobMarley' -Context $context +# List all vaults +Get-ContextVault + +# Get specific vault information +Get-ContextVault -Name "MyModule" + +# Remove a vault and all its contexts (use with caution) +Remove-ContextVault -Name "OldModule" ``` -4. Retrieve user configuration: +## Context Operations + +### Basic Operations ```pwsh -Get-Context -ID 'GitHub/BobMarley' -``` +# Store a context (creates vault automatically) +Set-Context -ID 'UserSettings' -Vault 'MyModule' -Context @{ + Theme = 'Dark' + Language = 'en-US' +} -## Contributing +# Retrieve a context +Get-Context -ID 'UserSettings' -Vault 'MyModule' -### For Users +# Get all contexts in a vault +Get-Context -Vault 'MyModule' -Even if you don’t code, your insights can help improve the project. If you experience unexpected behavior, errors, or missing functionality, submit a -bug or feature request in the project's issue tracker. +# Remove a context +Remove-Context -ID 'UserSettings' -Vault 'MyModule' -### For Developers +# Rename a context +Rename-Context -ID 'OldName' -NewID 'NewName' -Vault 'MyModule' -If you code, we'd love your contributions! Please read the [Contribution Guidelines](CONTRIBUTING.md) for more details. +# Get context metadata (without decrypting) +Get-ContextInfo -Vault 'MyModule' +``` -## Links +## Important Notes -- Sodium [GitHub](https://github.com/PSModule/Sodium) | [PSGallery](https://www.powershellgallery.com/packages/Sodium) +- **Vault Requirement**: Every context must be stored in a named vault - there is no default vault +- **Automatic Vault Creation**: `Set-Context` automatically creates vaults if they don't exist +- **Encryption Key Preservation**: Existing vault encryption keys are preserved when using `Set-Context` +- **Vault Isolation**: Each vault is isolated with its own encryption keys and storage directory +- **Storage Location**: Vaults are stored under `$HOME/.contextvaults//` +- **SecureString Support**: The module automatically handles encryption/decryption of `SecureString` values diff --git a/src/classes/public/ContextVault.ps1 b/src/classes/public/ContextVault.ps1 new file mode 100644 index 00000000..88b3aa06 --- /dev/null +++ b/src/classes/public/ContextVault.ps1 @@ -0,0 +1,15 @@ +class ContextVault { + [string] $Name + [string] $Path + + ContextVault() {} + + ContextVault([string] $name, [string] $path) { + $this.Name = $name + $this.Path = $path + } + + [string] ToString() { + return $this.Name + } +} diff --git a/src/completers.ps1 b/src/completers.ps1 deleted file mode 100644 index e38b730c..00000000 --- a/src/completers.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -Register-ArgumentCompleter -CommandName ($script:PSModuleInfo.FunctionsToExport) -ParameterName 'ID' -ScriptBlock { - param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) - $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter - - $contextInfos = Get-ContextInfo -ErrorAction SilentlyContinue -Verbose:$false -Debug:$false - $contextInfos | Where-Object { $_.ID -like "$wordToComplete*" } | ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_.ID, $_.ID, 'ParameterValue', $_.ID) - } -} diff --git a/src/formats/ContextVault.Format.ps1xml b/src/formats/ContextVault.Format.ps1xml new file mode 100644 index 00000000..8422f98d --- /dev/null +++ b/src/formats/ContextVault.Format.ps1xml @@ -0,0 +1,53 @@ + + + + + ContextVaultTable + + ContextVault + + + + + + + + + + + + + + + Name + + + Path + + + + + + + + ContextVaultList + + ContextVault + + + + + + + Name + + + Path + + + + + + + + diff --git a/src/functions/private/Completers/Complete-ContextID.ps1 b/src/functions/private/Completers/Complete-ContextID.ps1 new file mode 100644 index 00000000..68befa76 --- /dev/null +++ b/src/functions/private/Completers/Complete-ContextID.ps1 @@ -0,0 +1,29 @@ +function Complete-ContextID { + <# + .SYNOPSIS + Completion function for Context ID parameter. + + .DESCRIPTION + Provides tab completion for Context IDs, optionally filtered by vault. + #> + [CmdletBinding()] + param( + $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameter + ) + + $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter + + $vault = $fakeBoundParameter['Vault'] + $contextInfos = if ($vault) { + Get-ContextInfo -Vault $vault -ErrorAction SilentlyContinue -Verbose:$false -Debug:$false + } else { + Get-ContextInfo -ErrorAction SilentlyContinue -Verbose:$false -Debug:$false + } + $contextInfos | Where-Object { $_.ID -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_.ID, $_.ID, 'ParameterValue', $_.ID) + } +} diff --git a/src/functions/private/Completers/Complete-ContextVaultName.ps1 b/src/functions/private/Completers/Complete-ContextVaultName.ps1 new file mode 100644 index 00000000..59bde046 --- /dev/null +++ b/src/functions/private/Completers/Complete-ContextVaultName.ps1 @@ -0,0 +1,23 @@ +function Complete-ContextVaultName { + <# + .SYNOPSIS + Completion function for Context Vault Name parameter. + + .DESCRIPTION + Provides tab completion for Context Vault names. + #> + [CmdletBinding()] + param( + $commandName, + $parameterName, + $wordToComplete, + $commandAst, + $fakeBoundParameter + ) + $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter + + $vaults = Get-ContextVault -ErrorAction SilentlyContinue -Verbose:$false -Debug:$false + $vaults | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name) + } +} diff --git a/src/functions/private/Get-ContextVaultKeyPair.ps1 b/src/functions/private/Get-ContextVaultKeyPair.ps1 new file mode 100644 index 00000000..e3820415 --- /dev/null +++ b/src/functions/private/Get-ContextVaultKeyPair.ps1 @@ -0,0 +1,49 @@ +function Get-ContextVaultKeyPair { + <# + .SYNOPSIS + Retrieves the public and private keys from the context vault. + + .DESCRIPTION + Retrieves the public and private keys used for encrypting contexts in the context vault. + The keys are stored in a secure manner and can be used to encrypt or decrypt contexts. + + .EXAMPLE + Get-ContextVaultKeyPair + + Output: + ```powershell + PublicKey : + PrivateKey : + ``` + + Retrieves the public and private keys from the context vault. + #> + [OutputType([object])] + [CmdletBinding()] + param( + # The name of the vault to retrieve the keys from. + [Parameter(Mandatory)] + [string] $Vault + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + } + + process { + $vaultObject = Set-ContextVault -Name $Vault + $shardPath = Join-Path -Path $vaultObject.Path -ChildPath $script:Config.ShardFileName + $fileShard = Get-Content -Path $shardPath + $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 + $fileShard # + $userInputShard + $keys = New-SodiumKeyPair -Seed $seed + $keys + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/private/JsonToObject/Convert-ContextHashtableToObjectRecursive.ps1 b/src/functions/private/JsonToObject/Convert-ContextHashtableToObjectRecursive.ps1 index 1aca65e5..4c010567 100644 --- a/src/functions/private/JsonToObject/Convert-ContextHashtableToObjectRecursive.ps1 +++ b/src/functions/private/JsonToObject/Convert-ContextHashtableToObjectRecursive.ps1 @@ -30,7 +30,7 @@ values are SecureString objects. .OUTPUTS - PSCustomObject. + PSCustomObject .NOTES Returns an object where values are converted to their respective types, diff --git a/src/functions/private/JsonToObject/ConvertFrom-ContextJson.ps1 b/src/functions/private/JsonToObject/ConvertFrom-ContextJson.ps1 index c874e1ab..8ade5fb8 100644 --- a/src/functions/private/JsonToObject/ConvertFrom-ContextJson.ps1 +++ b/src/functions/private/JsonToObject/ConvertFrom-ContextJson.ps1 @@ -30,7 +30,7 @@ Converts a JSON string to a context object, ensuring 'Token' and 'Nested.Token' values are SecureString objects. .OUTPUTS - [pscustomobject]. + PSCustomObject .NOTES Returns a PowerShell custom object with SecureString conversion applied where necessary. diff --git a/src/functions/private/ObjectToJson/Convert-ContextObjectToHashtableRecursive.ps1 b/src/functions/private/ObjectToJson/Convert-ContextObjectToHashtableRecursive.ps1 index e4cdd1c4..0fd8221b 100644 --- a/src/functions/private/ObjectToJson/Convert-ContextObjectToHashtableRecursive.ps1 +++ b/src/functions/private/ObjectToJson/Convert-ContextObjectToHashtableRecursive.ps1 @@ -29,7 +29,7 @@ Converts the context object to a hashtable. Secure strings are converted to a string representation. .OUTPUTS - hashtable. + hashtable .NOTES Returns a hashtable representation of the input object. diff --git a/src/functions/private/ObjectToJson/ConvertTo-ContextJson.ps1 b/src/functions/private/ObjectToJson/ConvertTo-ContextJson.ps1 index a67eeab5..23c50f9d 100644 --- a/src/functions/private/ObjectToJson/ConvertTo-ContextJson.ps1 +++ b/src/functions/private/ObjectToJson/ConvertTo-ContextJson.ps1 @@ -26,7 +26,7 @@ Converts the given object into a JSON string, ensuring SecureStrings are handled properly. .OUTPUTS - System.String. + System.String .NOTES A JSON string representation of the provided object, including secure string transformations. diff --git a/src/functions/private/Set-ContextVault.ps1 b/src/functions/private/Set-ContextVault.ps1 deleted file mode 100644 index 9e4e0f3a..00000000 --- a/src/functions/private/Set-ContextVault.ps1 +++ /dev/null @@ -1,77 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.0' } - -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, the keys will be prepared for use. - 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 prepares the vault for use. - - .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" - } -} diff --git a/src/functions/private/Utilities/PowerShell/Get-PSCallStackPath.ps1 b/src/functions/private/Utilities/PowerShell/Get-PSCallStackPath.ps1 index 429aa770..83a056bc 100644 --- a/src/functions/private/Utilities/PowerShell/Get-PSCallStackPath.ps1 +++ b/src/functions/private/Utilities/PowerShell/Get-PSCallStackPath.ps1 @@ -41,7 +41,7 @@ Includes the last function (`Get-PSCallStackPath`) in the call stack output. .OUTPUTS - System.String. + System.String .NOTES A string representing the call stack path, with function names separated by backslashes. diff --git a/src/functions/public/Get-Context.ps1 b/src/functions/public/Get-Context.ps1 index f3537e58..c12cdd8b 100644 --- a/src/functions/public/Get-Context.ps1 +++ b/src/functions/public/Get-Context.ps1 @@ -47,12 +47,17 @@ function Get-Context { Retrieves all contexts from the context vault (directly from disk). .EXAMPLE - Get-Context -ID 'MySecret' + Get-Context -Vault 'MyModule' - Retrieves the context called 'MySecret' from the vault. + Retrieves all contexts from the 'MyModule' vault. .EXAMPLE - 'My*' | Get-Context + Get-Context -ID 'MySecret' -Vault 'MyModule' + + Retrieves the context called 'MySecret' from the 'MyModule' vault. + + .EXAMPLE + 'My*' | Get-Context -Vault 'MyModule' Output: ```powershell @@ -90,42 +95,41 @@ function Get-Context { ValueFromPipeline, ValueFromPipelineByPropertyName )] - [AllowEmptyString()] + [ArgumentCompleter({ Complete-ContextID @args })] + [SupportsWildcards()] + [string[]] $ID = '*', + + # The name of the vault to store the context in. + [Parameter()] + [ArgumentCompleter({ Complete-ContextVaultName @args })] [SupportsWildcards()] - [string[]] $ID = '*' + [string[]] $Vault = '*' ) 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]" - # Read context files directly from disk instead of using in-memory cache - $contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse - foreach ($file in $contextFiles) { - try { - $contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json - if ($contextInfo.ID -like $item) { - # Decrypt and return the context - $params = @{ - SealedBox = $contextInfo.Context - PublicKey = $script:Config.PublicKey - PrivateKey = $script:Config.PrivateKey - } - $contextObj = ConvertFrom-SodiumSealedBox @params - ConvertFrom-ContextJson -JsonString $contextObj - } - } catch { - Write-Warning "Failed to read or decrypt context file: $($file.FullName). Error: $_" + $contextInfos = Get-ContextInfo -ID $ID -Vault $Vault -ErrorAction Stop + foreach ($contextInfo in $contextInfos) { + Write-Verbose "Retrieving context - ID: [$($contextInfo.ID)], Vault: [$($contextInfo.Vault)]" + try { + if (-not (Test-Path -Path $contextInfo.Path)) { + Write-Warning "Context file does not exist: $($contextInfo.Path)" + continue + } + $keys = Get-ContextVaultKeyPair -Vault $contextInfo.Vault + $params = @{ + SealedBox = $contextInfo.Context + PublicKey = $keys.PublicKey + PrivateKey = $keys.PrivateKey } + $contextObj = ConvertFrom-SodiumSealedBox @params + ConvertFrom-ContextJson -JsonString $contextObj + } catch { + Write-Warning "Failed to read or decrypt context file: $($contextInfo.Path). Error: $_" } } } diff --git a/src/functions/public/Get-ContextInfo.ps1 b/src/functions/public/Get-ContextInfo.ps1 index 82eb8303..69d99251 100644 --- a/src/functions/public/Get-ContextInfo.ps1 +++ b/src/functions/public/Get-ContextInfo.ps1 @@ -1,11 +1,11 @@ function Get-ContextInfo { <# .SYNOPSIS - Retrieves info about a context from the context vault. + Retrieves info about a context from a context vault. .DESCRIPTION - Retrieves info about context files directly from the vault directory on disk. - If no ID is specified, all available info on contexts will be returned. + Retrieves info about contexts directly from a ContextVault. + If no ID is specified, info on all contexts will be returned. Wildcards are supported to match multiple contexts. Only metadata (ID and Path) is returned without decrypting the context contents. @@ -14,8 +14,9 @@ Output: ```powershell - ID : MySettings - Path : ...\b7c01dbe-bccd-4c7e-b075-c5aac1c43b1a.json + ID Path + -- ---- + MySettings C:\Users\\.contextvaults\Vaults\Contexts\b7c01dbe-bccd-4c7e-b075-c5aac1c43b1a.json ID : MyConfig Path : ...\feacc853-5bea-48d1-b751-41ce9768d48e.json @@ -61,7 +62,7 @@ Retrieves all contexts that start with 'My' from the context vault (directly from disk). .OUTPUTS - [System.Object] + [PSCustomObject] .NOTES Returns a list of context information matching the specified ID or all contexts if no ID is specified. @@ -70,46 +71,52 @@ .LINK https://psmodule.io/Context/Functions/Get-ContextInfo/ #> - [OutputType([object])] + [OutputType([PSCustomObject])] [CmdletBinding()] param( # The name of the context to retrieve from the vault. Supports wildcards. - [Parameter( - ValueFromPipeline, - ValueFromPipelineByPropertyName - )] - [AllowEmptyString()] + [Parameter()] + [ArgumentCompleter({ Complete-ContextID @args })] [SupportsWildcards()] - [string[]] $ID = '*' + [string[]] $ID = '*', + + # The name of the vault to retrieve context info from. Supports wildcards. + [Parameter()] + [ArgumentCompleter({ Complete-ContextVaultName @args })] + [string[]] $Vault = '*' ) begin { - $stackPath = Get-PSCallStackPath - Write-Debug "[$stackPath] - Start" - - if (-not $script:Config.Initialized) { - Set-ContextVault + $debug = $DebugPreference -eq 'Continue' + if ($debug) { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" } } process { - Write-Verbose "Retrieving context info - ID: [$ID]" - foreach ($item in $ID) { - # Read context files directly from disk instead of using in-memory cache - $contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse - foreach ($file in $contextFiles) { - try { - $contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json - if ($contextInfo.ID -like $item) { - # Return only metadata (ID and Path), don't decrypt the Context property - [PSCustomObject]@{ - ID = $contextInfo.ID - Path = $contextInfo.Path - } - } - } catch { - Write-Warning "Failed to read context file: $($file.FullName). Error: $_" - } + $vaults = foreach ($vaultName in $Vault) { + Get-ContextVault -Name $vaultName -ErrorAction Stop + } + if ($debug) { + Write-Debug "[$stackPath] - Found $($vaults.Count) vault(s) matching '$($Vault -join ', ')'." + } + + $files = foreach ($vaultObject in $vaults) { + Get-ChildItem -Path $vaultObject.Path -Filter *.json -File + } + if ($debug) { + Write-Debug "[$stackPath] - Found $($files.Count) context file(s) in vault(s)." + } + + foreach ($file in $files) { + $contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json + if ($debug) { + Write-Debug "[$stackPath] - Processing file: $($file.FullName)" + $contextInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug "[$stackPath] $_" } + } + if ($contextInfo.ID -like $ID) { + $contextInfo } } } diff --git a/src/functions/public/Remove-Context.ps1 b/src/functions/public/Remove-Context.ps1 index a8d94a57..017b2b8e 100644 --- a/src/functions/public/Remove-Context.ps1 +++ b/src/functions/public/Remove-Context.ps1 @@ -11,7 +11,7 @@ The function accepts pipeline input for easier batch removal. .EXAMPLE - Remove-Context -ID 'MySecret' + Remove-Context -ID 'MySecret' -Vault "MyModule" Output: ```powershell @@ -19,7 +19,7 @@ Removed item: MySecret ``` - Removes a context called 'MySecret' by specifying its ID. + Removes a context called 'MySecret' from the "MyModule" vault by specifying its ID. .EXAMPLE Remove-Context -ID 'Ctx1','Ctx2' @@ -82,37 +82,30 @@ ValueFromPipeline, ValueFromPipelineByPropertyName )] + [ArgumentCompleter({ Complete-ContextID @args })] [SupportsWildcards()] - [string[]] $ID + [string[]] $ID, + + # The name of the vault to remove contexts from. + [Parameter()] + [ArgumentCompleter({ Complete-ContextVaultName @args })] + [string] $Vault ) 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]" - # Find contexts by scanning disk files instead of using in-memory cache - $contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse - foreach ($file in $contextFiles) { - try { - $contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json - if ($contextInfo.ID -like $item) { - Write-Verbose "Removing context [$($contextInfo.ID)]" - if ($PSCmdlet.ShouldProcess($contextInfo.ID, 'Remove secret')) { - $file.FullName | Remove-Item -Force - Write-Verbose "Removed context file: $($file.FullName)" - } - } - } catch { - Write-Warning "Failed to read context file: $($file.FullName). Error: $_" - } + $contextInfo = Get-ContextInfo -ID $ID -Vault $Vault + foreach ($contextInfo in $contextInfo) { + $contextId = $contextInfo.ID + + if ($PSCmdlet.ShouldProcess("Context '$contextId'", 'Remove')) { + Write-Debug "[$stackPath] - Removing context [$contextId]" + $contextInfo.Path | Remove-Item -Force -ErrorAction Stop + Write-Output "Removed item: $contextId" } } } diff --git a/src/functions/public/Rename-Context.ps1 b/src/functions/public/Rename-Context.ps1 index bd05409c..295b8acf 100644 --- a/src/functions/public/Rename-Context.ps1 +++ b/src/functions/public/Rename-Context.ps1 @@ -47,6 +47,7 @@ ValueFromPipeline, ValueFromPipelineByPropertyName )] + [ArgumentCompleter({ Complete-ContextID @args })] [string] $ID, # The new ID of the context. @@ -55,32 +56,33 @@ # Force the rename even if the new ID already exists. [Parameter()] - [switch] $Force + [switch] $Force, + + # The name of the vault containing the context. + [Parameter()] + [ArgumentCompleter({ Complete-ContextVaultName @args })] + [string] $Vault ) begin { $stackPath = Get-PSCallStackPath Write-Debug "[$stackPath] - Start" - - if (-not $script:Config.Initialized) { - Set-ContextVault - } } process { - $context = Get-Context -ID $ID + $context = Get-Context -ID $ID -Vault $Vault if (-not $context) { - throw "Context with ID '$ID' not found." + throw "Context with ID '$ID' not found$(if ($Vault) { " in vault '$Vault'" })." } - $existingContext = Get-Context -ID $NewID + $existingContext = Get-Context -ID $NewID -Vault $Vault if ($existingContext -and -not $Force) { - throw "Context with ID '$NewID' already exists." + throw "Context with ID '$NewID' already exists$(if ($Vault) { " in vault '$Vault'" })." } - if ($PSCmdlet.ShouldProcess("Renaming context '$ID' to '$NewID'")) { - $context | Set-Context -ID $NewID - Remove-Context -ID $ID + if ($PSCmdlet.ShouldProcess("Renaming context '$ID' to '$NewID'$(if ($Vault) { " in vault '$Vault'" })")) { + $context | Set-Context -ID $NewID -Vault $Vault + Remove-Context -ID $ID -Vault $Vault } } diff --git a/src/functions/public/Set-Context.ps1 b/src/functions/public/Set-Context.ps1 index 143edf45..f0fedc51 100644 --- a/src/functions/public/Set-Context.ps1 +++ b/src/functions/public/Set-Context.ps1 @@ -3,41 +3,43 @@ function Set-Context { <# .SYNOPSIS - Set a context and store it in the context vault. + Set a context in a context vault. .DESCRIPTION If the context does not exist, it will be created. If it already exists, it will be updated. - The context is securely stored on disk using encryption mechanisms. - Each context operation reads the current state from disk to ensure consistency across processes. + The context is encrypted and stored on disk. If the context vault does not exist, it will be created. .EXAMPLE - Set-Context -ID 'PSModule.GitHub' -Context @{ Name = 'MySecret' } + Set-Context -ID 'MyUser' -Context @{ Name = 'MyUser' } -Vault 'MyModule' Output: ```powershell - ID : PSModule.GitHub + ID : MyUser Path : C:\Vault\Guid.json - Context : @{ Name = 'MySecret' } + Context : @{ Name = 'MyUser' } ``` - Creates a context called 'MySecret' in the vault. + Creates a context called 'MyUser' in the 'MyModule' vault. .EXAMPLE - Set-Context -ID 'PSModule.GitHub' -Context @{ Name = 'MySecret'; AccessToken = '123123123' } + $context = @{ + ID = 'MySecret' + Name = 'SomeSecretIHave' + AccessToken = '123123123' | ConvertTo-SecureString -AsPlainText -Force + } + $context | Set-Context Output: ```powershell - ID : PSModule.GitHub + ID : MyUser Path : C:\Vault\Guid.json - Context : @{ Name = 'MySecret'; AccessToken = '123123123' } + Context : { + ID = MySecret + Name = MyUser + AccessToken = System.Security.SecureString + } ``` - 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 @@ -50,7 +52,8 @@ function Set-Context { .LINK https://psmodule.io/Context/Functions/Set-Context/ #> - [OutputType([object])] + [Alias('New-Context', 'Update-Context')] + [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess)] param( # The ID of the context. @@ -61,21 +64,21 @@ function Set-Context { [Parameter(ValueFromPipeline)] [object] $Context = @{}, - # Pass the context through the pipeline. - [Parameter()] - [switch] $PassThru + # The name of the vault to store the context in. + [Parameter(Mandatory)] + [ArgumentCompleter({ Complete-ContextVaultName @args })] + [string] $Vault ) begin { $stackPath = Get-PSCallStackPath Write-Debug "[$stackPath] - Start" - - if (-not $script:Config.Initialized) { - Set-ContextVault - } } process { + $vaultObject = Set-ContextVault -Name $Vault + Write-Verbose "$($vaultObject | Format-List | Out-String)" + if ($context -is [System.Collections.IDictionary]) { $Context = [PSCustomObject]$Context } @@ -86,47 +89,37 @@ function Set-Context { if (-not $ID) { throw 'An ID is required, either as a parameter or as a property of the context object.' } - $existingContextFile = $null - # Check if context already exists by scanning disk files - $contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse - foreach ($file in $contextFiles) { - try { - $contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json - if ($contextInfo.ID -eq $ID) { - $existingContextFile = $file - $Path = $contextInfo.Path - break - } - } catch { - Write-Warning "Failed to read context file: $($file.FullName). Error: $_" - } - } - if (-not $existingContextFile) { - Write-Verbose "Context [$ID] not found in vault" - $Guid = [Guid]::NewGuid().ToString() - $Path = Join-Path -Path $script:Config.VaultPath -ChildPath "$Guid.json" + $contextInfo = Get-ContextInfo -ID $ID -Vault $Vault + Write-Verbose 'Context info:' + $contextInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Verbose $_ } + if (-not $contextInfo) { + Write-Verbose "[$stackPath] - Creating context [$ID] in [$Vault]" + $guid = [Guid]::NewGuid().Guid + $contextPath = Join-Path -Path $vaultObject.Path -ChildPath "$guid.json" } else { - Write-Verbose "Context [$ID] found in vault" + Write-Verbose "[$stackPath] - Context [$ID] found in [$Vault]" + $contextPath = $contextInfo.Path } + Write-Verbose "[$stackPath] - Context path: [$contextPath]" $contextJson = ConvertTo-ContextJson -Context $Context -ID $ID - - $param = [pscustomobject]@{ + $keys = Get-ContextVaultKeyPair -Vault $Vault + $content = [pscustomobject]@{ ID = $ID - Path = $Path - Context = ConvertTo-SodiumSealedBox -Message $contextJson -PublicKey $script:Config.PublicKey + Path = $contextPath + Vault = $Vault + Context = ConvertTo-SodiumSealedBox -Message $contextJson -PublicKey $keys.PublicKey } | ConvertTo-Json -Depth 5 - Write-Debug ($param | ConvertTo-Json -Depth 5) + Write-Verbose 'Content:' + $content | ConvertTo-Json -Depth 5 | Out-String -Stream | ForEach-Object { Write-Verbose $_ } - if ($PSCmdlet.ShouldProcess($ID, 'Set context')) { - Write-Verbose "Setting context [$ID] in vault" - Set-Content -Path $Path -Value $param + if ($PSCmdlet.ShouldProcess("file: [$contextPath]", 'Set content')) { + Write-Verbose "[$stackPath] - Setting context [$ID] in vault [$Vault]" + Set-Content -Path $contextPath -Value $content } - if ($PassThru) { - Get-Context -ID $ID - } + Get-Context -ID $ID -Vault $Vault } end { diff --git a/src/functions/public/Vault/Get-ContextVault.ps1 b/src/functions/public/Vault/Get-ContextVault.ps1 new file mode 100644 index 00000000..5f41f45e --- /dev/null +++ b/src/functions/public/Vault/Get-ContextVault.ps1 @@ -0,0 +1,60 @@ +function Get-ContextVault { + <# + .SYNOPSIS + Retrieves context vaults. + + .DESCRIPTION + Retrieves context vaults. If no name is specified, all available vaults will be returned. Supports wildcard matching. + + .EXAMPLE + Get-ContextVault + + Lists all available context vaults. + + .EXAMPLE + Get-ContextVault -Name 'MyModule' + + Gets information about the 'MyModule' vault. + + .EXAMPLE + Get-ContextVault -Name 'My*' + + Gets information about all vaults starting with 'My'. + + .OUTPUTS + [ContextVault[]] + + .LINK + https://psmodule.io/Context/Functions/Vault/Get-ContextVault/ + #> + [OutputType([ContextVault[]])] + [CmdletBinding()] + param( + # The name of the vault to retrieve. Supports wildcards. + [Parameter()] + [ArgumentCompleter({ Complete-ContextVaultName @args })] + [SupportsWildcards()] + [string[]] $Name = '*' + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + if (-not (Test-Path -Path $script:Config.RootPath)) { + return + } + $vaults = Get-ChildItem $script:Config.RootPath -Directory + } + + process { + foreach ($nameItem in $Name) { + foreach ($vault in ($vaults | Where-Object { $_.Name -like $nameItem })) { + [ContextVault]::new($vault.Name, $vault.FullName) + } + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/public/Vault/Remove-ContextVault.ps1 b/src/functions/public/Vault/Remove-ContextVault.ps1 new file mode 100644 index 00000000..59058eab --- /dev/null +++ b/src/functions/public/Vault/Remove-ContextVault.ps1 @@ -0,0 +1,59 @@ +function Remove-ContextVault { + <# + .SYNOPSIS + Removes a context vault. + + .DESCRIPTION + Removes an existing context vault and all its context data. This operation + is irreversible and will delete all contexts stored in the vault. + + .EXAMPLE + Remove-ContextVault -Name 'OldModule' + + Removes the 'OldModule' vault and all its contexts. + + .LINK + https://psmodule.io/Context/Functions/Vault/Remove-ContextVault/ + #> + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + param( + # The name of the vault to remove. + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'By Name')] + [ArgumentCompleter({ Complete-ContextVaultName @args })] + [SupportsWildcards()] + [string[]] $Name, + + # The vault object to remove. + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'As ContextVault')] + [ContextVault[]] $InputObject + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + $vaults = Get-ContextVault + } + + process { + switch ($PSCmdlet.ParameterSetName) { + 'By Name' { + foreach ($vaultName in $Name) { + foreach ($vault in ($vaults | Where-Object { $_.Name -like $vaultName })) { + Write-Verbose "Removing ContextVault [$($vault.Name)] at path [$($vault.Path)]" + if ($PSCmdlet.ShouldProcess("ContextVault: [$($vault.Name)]", 'Remove')) { + Remove-Item -Path $vault.Path -Recurse -Force + Write-Verbose "ContextVault [$($vault.Name)] removed successfully." + } + } + } + } + 'As ContextVault' { + $InputObject.Name | Remove-ContextVault + } + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/public/Vault/Reset-ContextVault.ps1 b/src/functions/public/Vault/Reset-ContextVault.ps1 new file mode 100644 index 00000000..87dc082c --- /dev/null +++ b/src/functions/public/Vault/Reset-ContextVault.ps1 @@ -0,0 +1,63 @@ +function Reset-ContextVault { + <# + .SYNOPSIS + Resets a context vault. + + .DESCRIPTION + Resets an existing context vault by deleting all contexts and regenerating + the encryption keys. The vault configuration and name are preserved. + + .EXAMPLE + Reset-ContextVault -Name 'MyModule' + + Resets the 'MyModule' vault, deleting all contexts and regenerating encryption keys. + + .OUTPUTS + [ContextVault] + + .LINK + https://psmodule.io/Context/Functions/Vault/Reset-ContextVault/ + #> + [OutputType([ContextVault])] + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + param( + # The name of the vault to reset. + [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'By Name')] + [ArgumentCompleter({ Complete-ContextVaultName @args })] + [SupportsWildcards()] + [string[]] $Name = '*', + + # The vault object to reset. + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'As ContextVault')] + [ContextVault[]] $InputObject + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + } + + process { + switch ($PSCmdlet.ParameterSetName) { + 'By Name' { + foreach ($vaultName in $Name) { + foreach ($vault in ($vaults | Where-Object { $_.Name -like $vaultName })) { + Write-Verbose "Resetting ContextVault [$($vault.Name)] at path [$($vault.Path)]" + if ($PSCmdlet.ShouldProcess("ContextVault: [$($vault.Name)]", 'Reset')) { + Remove-ContextVault -Name $($vault.Name) -Confirm:$false + Set-ContextVault -Name $($vault.Name) + Write-Verbose "ContextVault [$($vault.Name)] reset successfully." + } + } + } + } + 'As ContextVault' { + $InputObject.Name | Reset-ContextVault + } + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/functions/public/Vault/Set-ContextVault.ps1 b/src/functions/public/Vault/Set-ContextVault.ps1 new file mode 100644 index 00000000..43ba675d --- /dev/null +++ b/src/functions/public/Vault/Set-ContextVault.ps1 @@ -0,0 +1,62 @@ +function Set-ContextVault { + <# + .SYNOPSIS + Creates or updates a context vault configuration. + + .DESCRIPTION + Declaratively creates or updates a context vault configuration. If the vault exists, + its configuration is updated with the provided parameters. If the vault does not exist, + it is created with the specified configuration. + + .EXAMPLE + Set-ContextVault -Name 'MyModule' + + Creates a new vault named 'MyModule' or updates its description if it already exists. + + .OUTPUTS + [ContextVault] + + .LINK + https://psmodule.io/Context/Functions/Vault/Set-ContextVault/ + #> + [OutputType([ContextVault])] + [CmdletBinding(SupportsShouldProcess)] + param( + # The name of the vault to create or update. + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [ArgumentCompleter({ Complete-ContextVaultName @args })] + [string[]] $Name + ) + + begin { + $stackPath = Get-PSCallStackPath + Write-Debug "[$stackPath] - Start" + } + + process { + foreach ($vaultName in $Name) { + Write-Verbose "Processing vault: $vaultName" + + $vaultPath = Join-Path -Path $script:Config.RootPath -ChildPath $vaultName + if (-not (Test-Path $vaultPath)) { + Write-Verbose "Creating new vault [$($vault.Name)]" + if ($PSCmdlet.ShouldProcess("context vault folder $vaultName", 'Set')) { + $null = New-Item -Path $vaultPath -ItemType Directory -Force + } + } + $fileShardPath = Join-Path -Path $vaultPath -ChildPath $script:Config.ShardFileName + if (-not (Test-Path $fileShardPath)) { + Write-Verbose "Generating encryption keys for vault [$($vault.Name)]" + if ($PSCmdlet.ShouldProcess("shard file $fileShardPath", 'Set')) { + Set-Content -Path $fileShardPath -Value ([System.Guid]::NewGuid().ToString()) + } + } + + [ContextVault]::new($vaultName, $vaultPath) + } + } + + end { + Write-Debug "[$stackPath] - End" + } +} diff --git a/src/main.ps1 b/src/main.ps1 deleted file mode 100644 index 346b3f2b..00000000 --- a/src/main.ps1 +++ /dev/null @@ -1 +0,0 @@ -Set-ContextVault diff --git a/src/variables/private/Config.ps1 b/src/variables/private/Config.ps1 index 9ab4afd4..732f7086 100644 --- a/src/variables/private/Config.ps1 +++ b/src/variables/private/Config.ps1 @@ -1,7 +1,4 @@ $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) + RootPath = Join-Path -Path $HOME -ChildPath '.contextvaults' # Base directory for context vaults + ShardFileName = 'shard' # Shard path (relative to each vault) } diff --git a/tests/Context.Tests.ps1 b/tests/Context.Tests.ps1 index 65b5a8ae..daa798f0 100644 --- a/tests/Context.Tests.ps1 +++ b/tests/Context.Tests.ps1 @@ -1,40 +1,57 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Test code only' +#Requires -Modules @{ ModuleName = 'Pester'; RequiredVersion = '5.7.1' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Pester grouping syntax: known issue.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingConvertToSecureStringWithPlainText', '', + Justification = 'Used to create a secure string for testing.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingWriteHost', '', + Justification = 'Log outputs to GitHub Actions logs.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidLongLines', '', + Justification = 'Long test descriptions and skip switches' )] [CmdletBinding()] param() BeforeAll { - $contexts = Get-ContextInfo -Verbose - Write-Verbose "Contexts: $($contexts.Count)" -Verbose - Write-Verbose ($contexts | Format-Table | Out-String) -Verbose - $contexts | Remove-Context -Verbose - Remove-Module -Name Context -Force -Verbose - Import-Module -Name Context -Force -Verbose -Version 999.0.0 + Get-ContextVault | Remove-ContextVault -Confirm:$false + # Create two vaults for multi-vault tests + Set-ContextVault -Name 'VaultA' + Set-ContextVault -Name 'VaultB' } -Describe 'Functions' { - Context 'Function: Set-Context' { - It "Set-Context -ID 'TestID1'" { - { Set-Context -ID 'TestID1' } | Should -Not -Throw - $result = Get-Context -ID 'TestID1' - Write-Verbose ($result | Out-String) -Verbose +AfterAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false +} + +Describe 'Context' { + Context 'Set-Context' { + It "Set-Context -ID 'TestID1' -Vault 'VaultA'" { + { Set-Context -ID 'TestID1' -Vault 'VaultA' } | Should -Not -Throw + $result = Get-Context -ID 'TestID1' -Vault 'VaultA' + Write-Host ($result | Out-String) $result | Should -Not -BeNullOrEmpty $result.ID | Should -Be 'TestID1' } - It "Set-Context -ID 'TestID2' -Context @{}" { - { Set-Context -ID 'TestID2' -Context @{} } | Should -Not -Throw - $result = Get-Context -ID 'TestID2' + It "Set-Context -ID 'TestID2' -Context @{} -Vault 'VaultA'" { + { Set-Context -ID 'TestID2' -Context @{} -Vault 'VaultA' } | Should -Not -Throw + $result = Get-Context -ID 'TestID2' -Vault 'VaultA' $result | Should -Not -BeNullOrEmpty $result.ID | Should -Be 'TestID2' } - It "Set-Context -ID 'TestID2' -Context @{} - Again" { - { Set-Context -ID 'TestID2' -Context @{} } | Should -Not -Throw - $result = Get-Context -ID 'TestID2' + It "Set-Context -ID 'TestID2' -Context @{} - Again -Vault 'VaultA'" { + { Set-Context -ID 'TestID2' -Context @{} -Vault 'VaultA' } | Should -Not -Throw + $result = Get-Context -ID 'TestID2' -Vault 'VaultA' $result | Should -Not -BeNullOrEmpty $result.ID | Should -Be 'TestID2' } - It "Set-Context -ID 'john_doe' -Context [advanced object]" { + It "Set-Context -ID 'john_doe' -Context [advanced object] -Vault 'VaultA'" { $contextData = [PSCustomObject]@{ Username = 'john_doe' AuthToken = 'ghp_12345ABCDE67890FGHIJ' | ConvertTo-SecureString -AsPlainText -Force #gitleaks:allow @@ -99,8 +116,8 @@ Describe 'Functions' { } } - { Set-Context -ID 'john_doe' -Context $contextData } | Should -Not -Throw - $context = Get-Context -ID 'john_doe' + { Set-Context -ID 'john_doe' -Context $contextData -Vault 'VaultA' } | Should -Not -Throw + $context = Get-Context -ID 'john_doe' -Vault 'VaultA' $context | Should -Not -BeNullOrEmpty $context.ID | Should -Be 'john_doe' $context.Username | Should -Be 'john_doe' @@ -137,173 +154,155 @@ Describe 'Functions' { $context.SessionMetaData.BrowserInfo.Name | Should -Be 'Chrome' $context.SessionMetaData.BrowserInfo.Version | Should -Be '118.0.1' } - # It "Get-Context -> Update -> Set-Context - Updates the context" { - # Set-Context -ID 'JimmyDoe' -Context @{ - # Name = 'Jimmy Doe' - # Email = 'JD@example.com' - # } - # $context = Get-Context -ID 'JimmyDoe' - # $context.Name = 'Jimmy Doe Jr.' - # $context | Set-Context - # } } - Context 'Function: Get-Context' { - It 'Get-Context - Should return all contexts' { - Write-Verbose (Get-Context | Out-String) -Verbose - (Get-Context).Count | Should -Be 3 + Context 'Get-Context' { + It 'Get-Context - Should return all contexts in VaultA' { + Write-Host (Get-Context -Vault 'VaultA' | Out-String) + (Get-Context -Vault 'VaultA').Count | Should -Be 3 } - It "Get-Context -ID '*' - Should return all contexts" { - Write-Verbose (Get-Context -ID '*' | Out-String) -Verbose - (Get-Context -ID '*').Count | Should -Be 3 + It "Get-Context -ID '*' - Should return all contexts in VaultA" { + Write-Host (Get-Context -ID '*' -Vault 'VaultA' | Out-String) + (Get-Context -ID '*' -Vault 'VaultA').Count | Should -Be 3 } - It "Get-Context -ID 'TestID*' - Should return all contexts" { - Write-Verbose (Get-Context -ID 'TestID*' | Out-String) -Verbose - (Get-Context -ID 'TestID*').Count | Should -Be 2 + It "Get-Context -ID 'TestID*' - Should return all contexts in VaultA" { + Write-Host (Get-Context -ID 'TestID*' -Vault 'VaultA' | Out-String) + (Get-Context -ID 'TestID*' -Vault 'VaultA').Count | Should -Be 2 } - It "Get-Context -ID '' - Should return no contexts" { - Write-Verbose (Get-Context -ID '' | Out-String) -Verbose - { Get-Context -ID '' } | Should -Not -Throw - Get-Context -ID '' | Should -BeNullOrEmpty + It "Get-Context -ID '' - Should return no contexts in VaultA" { + Write-Host (Get-Context -ID '' -Vault 'VaultA' | Out-String) + { Get-Context -ID '' -Vault 'VaultA' } | Should -Not -Throw + Get-Context -ID '' -Vault 'VaultA' | Should -BeNullOrEmpty } - It 'Get-Context -ID $null - Should return no contexts' { - Write-Verbose (Get-Context -ID $null | Out-String) -Verbose - { Get-Context -ID $null } | Should -Not -Throw - Get-Context -ID $null | Should -BeNullOrEmpty + It 'Get-Context -ID $null - Should return no contexts in VaultA' { + Write-Host (Get-Context -ID $null -Vault 'VaultA' | Out-String) + { Get-Context -ID $null -Vault 'VaultA' } | Should -Not -Throw + Get-Context -ID $null -Vault 'VaultA' | Should -BeNullOrEmpty } } - Context 'Function: Remove-Context' { - It "Remove-Context -ID 'AContextID' - Should remove the context" { - Get-Context | Remove-Context - + Context 'Remove-Context' { + It "Remove-Context -ID 'AContextID' - Should remove the context from VaultA" { + Get-Context -Vault 'VaultA' | Remove-Context -Vault 'VaultA' { 1..10 | ForEach-Object { - Set-Context -Context @{} -ID "Temp$_" + Set-Context -Context @{} -ID "Temp$_" -Vault 'VaultA' } } | Should -Not -Throw - - (Get-Context -ID 'Temp*').Count | Should -Be 10 - + (Get-Context -ID 'Temp*' -Vault 'VaultA').Count | Should -Be 10 { 1..10 | ForEach-Object { - Remove-Context -ID "Temp$_" + Remove-Context -ID "Temp$_" -Vault 'VaultA' } } | Should -Not -Throw - (Get-Context -ID 'Temp*').Count | Should -Be 0 + (Get-Context -ID 'Temp*' -Vault 'VaultA').Count | Should -Be 0 } - - It "Remove-Context -ID 'NonExistentContext' - Should not throw" { - { Remove-Context -ID 'NonExistentContext' } | Should -Not -Throw + It "Remove-Context -ID 'NonExistentContext' - Should not throw in VaultA" { + { Remove-Context -ID 'NonExistentContext' -Vault 'VaultA' } | Should -Not -Throw } - - It "'john_doe' | Remove-Context - Should remove the context" { - { 'john_doe' | Remove-Context } | Should -Not -Throw - Get-Context -ID 'john_doe' | Should -BeNullOrEmpty + It "'john_doe' | Remove-Context - Should remove the context from VaultA" { + { 'john_doe' | Remove-Context -Vault 'VaultA' } | Should -Not -Throw + Get-Context -ID 'john_doe' -Vault 'VaultA' | Should -BeNullOrEmpty } } - Context 'Function: Rename-Context' { + Context 'Rename-Context' { BeforeEach { - # Ensure no contexts exist before starting tests - Get-Context | Remove-Context + Get-Context -Vault 'VaultA' | Remove-Context -Vault 'VaultA' } AfterEach { - # Cleanup any contexts created during tests - Get-Context | Remove-Context + Get-Context -Vault 'VaultA' | Remove-Context -Vault 'VaultA' } - It 'Renames the context successfully' { + It 'Renames the context successfully in VaultA' { $ID = 'TestContext' $newID = 'RenamedContext' - Set-Context -ID $ID - - # Rename the context - Rename-Context -ID $ID -NewID $newID - - # Verify the old context no longer exists - Get-Context -ID $ID | Should -BeNullOrEmpty - Get-Context -ID $newID | Should -Not -BeNullOrEmpty + Set-Context -ID $ID -Vault 'VaultA' + Rename-Context -ID $ID -NewID $newID -Vault 'VaultA' + Get-Context -ID $ID -Vault 'VaultA' | Should -BeNullOrEmpty + Get-Context -ID $newID -Vault 'VaultA' | Should -Not -BeNullOrEmpty } - It 'Throws an error when renaming a non-existent context' { - { Rename-Context -ID 'NonExistentContext' -NewID 'NewContext' } | Should -Throw + It 'Throws an error when renaming a non-existent context in VaultA' { + { Rename-Context -ID 'NonExistentContext' -NewID 'NewContext' -Vault 'VaultA' } | Should -Throw } - It 'Renaming a context to an existing context throws without force' { + It 'Renaming a context to an existing context throws without force in VaultA' { $existingID = 'ExistingContext' - Set-Context -ID $existingID - Set-Context -ID 'TestContext' - - # Attempt to rename the context to an existing context - { Rename-Context -ID 'TestContext' -NewID $existingID } | Should -Throw + Set-Context -ID $existingID -Vault 'VaultA' + Set-Context -ID 'TestContext' -Vault 'VaultA' + { Rename-Context -ID 'TestContext' -NewID $existingID -Vault 'VaultA' } | Should -Throw } - It 'Sets a context where the ID is in the context data' { + It 'Sets a context where the ID is in the context data in VaultA' { $contextData = [PSCustomObject]@{ ID = 'TestContext' Data = 'Some data' } - { Set-Context -Context $contextData } | Should -Not -Throw - $result = Get-Context -ID 'TestContext' + { Set-Context -Context $contextData -Vault 'VaultA' } | Should -Not -Throw + $result = Get-Context -ID 'TestContext' -Vault 'VaultA' $result | Should -Not -BeNullOrEmpty $result.ID | Should -Be 'TestContext' $result.Data | Should -Be 'Some data' } - It 'Renaming a context to an existing context does not throw with force' { + It 'Renaming a context to an existing context does not throw with force in VaultA' { $existingID = 'ExistingContext' - Set-Context -ID $existingID - Set-Context -ID 'TestContext' - - # Attempt to rename the context to an existing context - { Rename-Context -ID 'TestContext' -NewID $existingID -Force } | Should -Not -Throw + Set-Context -ID $existingID -Vault 'VaultA' + Set-Context -ID 'TestContext' -Vault 'VaultA' + { Rename-Context -ID 'TestContext' -NewID $existingID -Vault 'VaultA' -Force } | Should -Not -Throw } } - # New tests to verify that pipeline input is fully supported - Context 'Pipeline Input support' { - It 'Get-Context supports pipeline input as strings' { - # Create two contexts to test pipeline input - Set-Context -ID 'PipeContext1' -Context @{ Dummy = 1 } - Set-Context -ID 'PipeContext2' -Context @{ Dummy = 2 } - $result = 'PipeContext1', 'PipeContext2' | Get-Context - $result | Should -Not -BeNullOrEmpty - $result.ID | Should -Contain 'PipeContext1' - $result.ID | Should -Contain 'PipeContext2' + Context 'Get-ContextInfo' { + BeforeAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false + + Set-Context -ID 'TestID1' -Vault 'VaultA' + Set-Context -ID 'TestID2' -Vault 'VaultA' + Set-Context -ID 'TestID3' -Vault 'VaultA' + Set-Context -ID 'TestID1' -Vault 'VaultB' + Set-Context -ID 'TestID2' -Vault 'VaultB' } - It 'Get-Context supports pipeline input by property name' { - # Create a context and pass an object with an ID property - Set-Context -ID 'PipeContext3' -Context @{ Dummy = 3 } - $obj = [PSCustomObject]@{ ID = 'PipeContext3' } - $result = $obj | Get-Context - $result | Should -Not -BeNullOrEmpty - $result.ID | Should -Be 'PipeContext3' + AfterAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false } - It 'Get-ContextInfo supports pipeline input as strings' { - # Create two contexts and verify that Get-ContextInfo excludes the Context property - Set-Context -ID 'PipeInfo1' -Context @{ Dummy = 1 } - Set-Context -ID 'PipeInfo2' -Context @{ Dummy = 2 } - $result = 'PipeInfo1', 'PipeInfo2' | Get-ContextInfo - $result | Should -Not -BeNullOrEmpty - $result.ID | Should -Contain 'PipeInfo1' - $result.ID | Should -Contain 'PipeInfo2' - $result | ForEach-Object { $_.PSObject.Properties.Name | Should -BeIn @('ID', 'Path') } + It 'Should return all contexts' { + $results = Get-ContextInfo + $results | Should -Not -BeNullOrEmpty + $results | Should -HaveCount 5 + $results | ForEach-Object { $_ | Should -BeOfType [PSCustomObject] } } - It 'Get-ContextInfo supports pipeline input by property name' { - # Create a context and pass an object with an ID property to Get-ContextInfo - Set-Context -ID 'PipeInfo3' -Context @{ Dummy = 3 } - $obj = [PSCustomObject]@{ ID = 'PipeInfo3' } - $result = $obj | Get-ContextInfo + It 'Should return all contexts in VaultA' { + $results = Get-ContextInfo -Vault 'VaultA' + $results | Should -Not -BeNullOrEmpty + $results | Should -HaveCount 3 + $results | ForEach-Object { $_ | Should -BeOfType [PSCustomObject] } + } + + It 'Should return specific context by ID in VaultA' { + $result = Get-ContextInfo -ID 'TestID1' -Vault 'VaultA' $result | Should -Not -BeNullOrEmpty - $result.ID | Should -Be 'PipeInfo3' - $result | ForEach-Object { $_.PSObject.Properties.Name | Should -BeIn @('ID', 'Path') } + $result.ID | Should -Be 'TestID1' + } + + It 'Should return multiple contexts matching wildcard ID in VaultA' { + $results = Get-ContextInfo -ID 'TestID*' -Vault 'VaultA' + $results | Should -HaveCount 3 + $results.ID | Should -Contain 'TestID1' + $results.ID | Should -Contain 'TestID2' + $results.ID | Should -Contain 'TestID3' + } + + It 'Should return no results for non-existent context ID in VaultA' { + $result = Get-ContextInfo -ID 'NonExistentContext' -Vault 'VaultA' + $result | Should -BeNullOrEmpty } } } diff --git a/tests/ContextVaults.Tests.ps1 b/tests/ContextVaults.Tests.ps1 new file mode 100644 index 00000000..fa1e0fd4 --- /dev/null +++ b/tests/ContextVaults.Tests.ps1 @@ -0,0 +1,244 @@ +#Requires -Modules @{ ModuleName = 'Pester'; RequiredVersion = '5.7.1' } + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Pester grouping syntax: known issue.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingConvertToSecureStringWithPlainText', '', + Justification = 'Used to create a secure string for testing.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingWriteHost', '', + Justification = 'Log outputs to GitHub Actions logs.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidLongLines', '', + Justification = 'Long test descriptions and skip switches' +)] +[CmdletBinding()] +param() + +BeforeAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false +} + +AfterAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false +} + +Describe 'ContextVault' { + Context 'Set-ContextVault' { + BeforeAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false + } + + AfterAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false + } + + It 'Should create a new vault with a single name parameter' { + $result = Set-ContextVault -Name 'test-vault1' + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [ContextVault] + $result.Name | Should -Be 'test-vault1' + $result.Path | Should -Not -BeNullOrEmpty + } + + It 'Should create multiple vaults from array parameter' { + $results = Set-ContextVault -Name 'test-vault2', 'test-vault3' + $results | Should -HaveCount 2 + $results | ForEach-Object { $_ | Should -BeOfType [ContextVault] } + $results[0].Name | Should -Be 'test-vault2' + $results[1].Name | Should -Be 'test-vault3' + } + + It 'Should accept pipeline input for vault creation' { + $results = 'test-pipeline1', 'test-pipeline2' | Set-ContextVault + $results | Should -HaveCount 2 + $results | ForEach-Object { $_ | Should -BeOfType [ContextVault] } + $results.Name | Should -Contain 'test-pipeline1' + $results.Name | Should -Contain 'test-pipeline2' + } + + It 'Should not throw when setting an existing vault' { + { Set-ContextVault -Name 'test-vault1' } | Should -Not -Throw + } + } + + Context 'Get-ContextVault' { + BeforeAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false + Set-ContextVault -Name 'get-test1' + Set-ContextVault -Name 'get-test2' + Set-ContextVault -Name 'other-test1' + } + + AfterAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false + } + + It 'Should return all vaults when no name is specified' { + $results = Get-ContextVault + $results | Should -Not -BeNullOrEmpty + $results | Should -HaveCount 3 + $results | ForEach-Object { $_ | Should -BeOfType [ContextVault] } + } + + It 'Should return a specific vault by exact name' { + $result = Get-ContextVault -Name 'get-test1' + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [ContextVault] + $result.Name | Should -Be 'get-test1' + } + + It 'Should return multiple vaults with wildcard matching' { + $results = Get-ContextVault -Name 'get-*' + $results | Should -HaveCount 2 + $results.Name | Should -Contain 'get-test1' + $results.Name | Should -Contain 'get-test2' + } + + It 'Should return multiple vaults with array of names' { + $vaultNames = @('get-test1', 'other-test1') + $results = Get-ContextVault -Name $vaultNames + $results | Should -HaveCount 2 + $results.Name | Should -Contain 'get-test1' + $results.Name | Should -Contain 'other-test1' + } + + It 'Should return empty results for non-existent vault names' { + $result = Get-ContextVault -Name 'nonexistent-vault' + $result | Should -BeNullOrEmpty + } + } + + Context 'Remove-ContextVault' { + BeforeEach { + Get-ContextVault | Remove-ContextVault -Confirm:$false + Set-ContextVault -Name 'remove-test1' + Set-ContextVault -Name 'remove-test2' + Set-ContextVault -Name 'remove-test3' + Set-ContextVault -Name 'keep-test1' + } + + AfterAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false + } + + + It 'Should remove a specific vault by name' { + Remove-ContextVault -Name 'remove-test1' -Confirm:$false + $result = Get-ContextVault -Name 'remove-test1' + $result | Should -BeNullOrEmpty + } + + It 'Should remove multiple vaults using wildcards' { + Remove-ContextVault -Name 'remove-*' -Confirm:$false + $results = Get-ContextVault -Name 'remove-*' + $results | Should -BeNullOrEmpty + + # Verify other vaults are still present + $kept = Get-ContextVault -Name 'keep-*' + $kept | Should -Not -BeNullOrEmpty + } + + It 'Should remove vaults using pipeline input from names' { + 'remove-test2', 'remove-test3' | Remove-ContextVault -Confirm:$false + $results = Get-ContextVault -Name @('remove-test2', 'remove-test3') + $results | Should -BeNullOrEmpty + } + + It 'Should remove vaults using pipeline input from Get-ContextVault' { + Get-ContextVault -Name 'remove-*' | Remove-ContextVault -Confirm:$false + $results = Get-ContextVault -Name 'remove-*' + $results | Should -BeNullOrEmpty + } + + It 'Should not throw when removing non-existent vault' { + { Remove-ContextVault -Name 'nonexistent-vault' -Confirm:$false } | Should -Not -Throw + } + } + + Context 'Reset-ContextVault' { + BeforeAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false + Set-ContextVault -Name 'reset-test1' + Set-ContextVault -Name 'reset-test2' + Set-ContextVault -Name 'reset-other' + } + + AfterAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false + } + + It 'Should reset a specific vault by name' { + { Reset-ContextVault -Name 'reset-test1' -Confirm:$false } | Should -Not -Throw + $result = Get-ContextVault -Name 'reset-test1' + $result | Should -Not -BeNullOrEmpty + } + + It 'Should reset multiple vaults using wildcards' { + { Reset-ContextVault -Name 'reset-*' -Confirm:$false } | Should -Not -Throw + $results = Get-ContextVault -Name 'reset-*' + $results | Should -Not -BeNullOrEmpty + $results | Should -HaveCount 3 + } + + It 'Should reset vaults using pipeline input from vault objects' { + { Get-ContextVault -Name 'reset-other' | Reset-ContextVault -Confirm:$false } | Should -Not -Throw + $result = Get-ContextVault -Name 'reset-other' + $result | Should -Not -BeNullOrEmpty + } + + It 'Should not throw when resetting non-existent vault' { + { Reset-ContextVault -Name 'nonexistent-vault' -Confirm:$false } | Should -Not -Throw + } + } + + Context 'Pipeline and Combined Operations' { + BeforeAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false + 'pipeline-test1', 'pipeline-test2', 'pipeline-test3' | Set-ContextVault + } + + AfterAll { + Get-ContextVault | Remove-ContextVault -Confirm:$false + } + + It 'Should support pipeline operations with variables' { + $testVaults = @('pipeline-var1', 'pipeline-var2') + $results = $testVaults | Set-ContextVault + $results | Should -HaveCount 2 + + $getResults = Get-ContextVault -Name $testVaults + $getResults | Should -HaveCount 2 + + $getResults | Remove-ContextVault -Confirm:$false + $checkResults = Get-ContextVault -Name $testVaults + $checkResults | Should -BeNullOrEmpty + } + + It 'Should get vaults and pass to reset through pipeline' { + $vaults = Get-ContextVault -Name 'pipeline-test*' + $vaults | Should -HaveCount 3 + + { $vaults | Reset-ContextVault -Confirm:$false } | Should -Not -Throw + + $resetVaults = Get-ContextVault -Name 'pipeline-test*' + $resetVaults | Should -HaveCount 3 + } + + It 'Should process arrays of vault names through the pipeline' { + $vaultBatch = @('batch-test1', 'batch-test2', 'batch-test3') + $vaultBatch | Set-ContextVault + + $results = Get-ContextVault -Name 'batch-*' + $results | Should -HaveCount 3 + + $results | Remove-ContextVault -Confirm:$false + $checkResults = Get-ContextVault -Name 'batch-*' + $checkResults | Should -BeNullOrEmpty + } + } +}