diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8dde243..6f89679 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
## Unreleased
+## 2026.06.07.1350
+
+- Build produced from commit 1aa51b8cbf9c.
+
+## Unreleased (carried forward)
+
## 2026.06.07.0017
- Build produced from commit 77cb03ec9845.
diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1
index e8b4746..f052ded 100644
--- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1
+++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1
@@ -1,6 +1,6 @@
@{
RootModule = 'PSInfisicalAPI.psm1'
- ModuleVersion = '2026.06.07.0017'
+ ModuleVersion = '2026.06.07.1350'
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
Author = 'Grace Solutions'
CompanyName = 'Grace Solutions'
@@ -19,6 +19,7 @@
'Copy-InfisicalSecret',
'ConvertTo-InfisicalSecretDictionary',
'Export-InfisicalSecrets',
+ 'Import-InfisicalSecret',
'Get-InfisicalProject',
'New-InfisicalProject',
'Update-InfisicalProject',
@@ -60,6 +61,7 @@
'Export-InfisicalScepMdmProfile',
'Write-InfisicalScepMdmProfileToWmi',
'Start-InfisicalProcess',
+ 'Get-InfisicalEnvironmentVariable',
'Get-InfisicalSANList'
)
AliasesToExport = @()
@@ -72,7 +74,7 @@
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
- CommitHash = '77cb03ec9845'
+ CommitHash = '1aa51b8cbf9c'
}
}
}
\ No newline at end of file
diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll
index 9376fcb..855f788 100644
Binary files a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll and b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll differ
diff --git a/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml
index 24f3026..78b9730 100644
--- a/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml
+++ b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml
@@ -295,7 +295,7 @@ $CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters
InfisicalSecretDictionary
- Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins).
+ Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). -Prefix prepends a string to every dictionary key (e.g. SecretName 'API_KEY' with -Prefix 'MYAPP_' becomes key 'MYAPP_API_KEY'); the underlying InfisicalSecret objects are not mutated.
Notes
@@ -322,6 +322,11 @@ $ConvertToInfisicalSecretDictionaryParameters.Verbose = $True
$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters
Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions.
+
+ EXAMPLE 3
+ Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | ConvertTo-InfisicalSecretDictionary -Prefix 'MYAPP_' -AsPlainText
+ Builds a plain-text dictionary whose keys are namespaced with 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY); the source InfisicalSecret objects are unchanged.
+
@@ -369,6 +374,44 @@ $ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsP
+
+
+ Import-InfisicalSecret
+ Reads a previously exported secrets file (Json, Yaml, Env, or Xml) back into a name-keyed Dictionary.
+ Import
+ InfisicalSecret
+
+
+ Loads the file at -Path (which must exist) using the parser matching -Format and returns a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText for a plain string dictionary. -Prefix prepends to every emitted key. -DuplicateKeyBehavior controls collision handling (Error, FirstWins, LastWins). The EnvironmentVariables format is intentionally not supported here; use [Environment]::GetEnvironmentVariable or Get-InfisicalEnvironmentVariable for environment-backed values.
+
+
+ Notes
+
+ Importers expect the same schema that Export-InfisicalSecrets produces (Json/Yaml = array or 'Secrets' root list of {SecretName, SecretValue}; Xml = <Secrets><Secret><SecretName/><SecretValue/>; Env = KEY=VALUE per line, '#' comments allowed). JSON and YAML additionally accept a flat key/value object as a convenience.
+
+
+
+
+ EXAMPLE 1
+ $Secrets = Import-InfisicalSecret -Path '.\secrets.json' -Format Json
+ Reads a JSON export back into a Dictionary<string, SecureString> keyed by the original SecretName values.
+
+
+ EXAMPLE 2
+ $ImportInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+$ImportInfisicalSecretParameters.Path = [System.IO.FileInfo]'.\secrets.env'
+$ImportInfisicalSecretParameters.Format = 'Env'
+$ImportInfisicalSecretParameters.Prefix = 'MYAPP_'
+$ImportInfisicalSecretParameters.DuplicateKeyBehavior = 'LastWins'
+$ImportInfisicalSecretParameters.AsPlainText = $True
+
+$ImportInfisicalSecretResult = Import-InfisicalSecret @ImportInfisicalSecretParameters
+ Loads a .env file into a plain-text dictionary, namespacing every key with 'MYAPP_' and letting the last occurrence win on duplicates.
+
+
+
+
+
Get-InfisicalProject
@@ -1701,6 +1744,37 @@ $StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessPara
+
+
+ Get-InfisicalEnvironmentVariable
+ Reads an environment variable from the first scope that has a non-empty value (Process > User > Machine).
+ Get
+ InfisicalEnvironmentVariable
+
+
+ Returns the value of -Name from the first scope that contains a non-empty value, checking Process, then User, then Machine in that order. Emits nothing when the variable is missing or blank in every scope, so an assignment yields $null without writing errors or warnings. Platform-unsupported scopes (User and Machine on non-Windows) are silently skipped.
+
+
+ Notes
+
+ Designed for the same discovery semantics the rest of PSInfisicalAPI uses when resolving connection inputs from the environment. Pipe-friendly: accepts -Name from the pipeline by value and by property name.
+
+
+
+
+ EXAMPLE 1
+ $Value = Get-InfisicalEnvironmentVariable -Name 'INFISICAL_CLIENT_ID'
+ Returns the first non-empty value of INFISICAL_CLIENT_ID across Process, User, and Machine scopes; assigns $null when the variable is unset everywhere.
+
+
+ EXAMPLE 2
+ @('INFISICAL_BASE_URI','INFISICAL_PROJECT_ID') | Get-InfisicalEnvironmentVariable
+ Pipes a list of variable names through the cmdlet and emits one value per name that is set in any scope.
+
+
+
+
+
Get-InfisicalOrganization
diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
index 24f3026..78b9730 100644
--- a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
+++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
@@ -295,7 +295,7 @@ $CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters
InfisicalSecretDictionary
- Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins).
+ Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). -Prefix prepends a string to every dictionary key (e.g. SecretName 'API_KEY' with -Prefix 'MYAPP_' becomes key 'MYAPP_API_KEY'); the underlying InfisicalSecret objects are not mutated.
Notes
@@ -322,6 +322,11 @@ $ConvertToInfisicalSecretDictionaryParameters.Verbose = $True
$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters
Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions.
+
+ EXAMPLE 3
+ Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | ConvertTo-InfisicalSecretDictionary -Prefix 'MYAPP_' -AsPlainText
+ Builds a plain-text dictionary whose keys are namespaced with 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY); the source InfisicalSecret objects are unchanged.
+
@@ -369,6 +374,44 @@ $ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsP
+
+
+ Import-InfisicalSecret
+ Reads a previously exported secrets file (Json, Yaml, Env, or Xml) back into a name-keyed Dictionary.
+ Import
+ InfisicalSecret
+
+
+ Loads the file at -Path (which must exist) using the parser matching -Format and returns a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText for a plain string dictionary. -Prefix prepends to every emitted key. -DuplicateKeyBehavior controls collision handling (Error, FirstWins, LastWins). The EnvironmentVariables format is intentionally not supported here; use [Environment]::GetEnvironmentVariable or Get-InfisicalEnvironmentVariable for environment-backed values.
+
+
+ Notes
+
+ Importers expect the same schema that Export-InfisicalSecrets produces (Json/Yaml = array or 'Secrets' root list of {SecretName, SecretValue}; Xml = <Secrets><Secret><SecretName/><SecretValue/>; Env = KEY=VALUE per line, '#' comments allowed). JSON and YAML additionally accept a flat key/value object as a convenience.
+
+
+
+
+ EXAMPLE 1
+ $Secrets = Import-InfisicalSecret -Path '.\secrets.json' -Format Json
+ Reads a JSON export back into a Dictionary<string, SecureString> keyed by the original SecretName values.
+
+
+ EXAMPLE 2
+ $ImportInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+$ImportInfisicalSecretParameters.Path = [System.IO.FileInfo]'.\secrets.env'
+$ImportInfisicalSecretParameters.Format = 'Env'
+$ImportInfisicalSecretParameters.Prefix = 'MYAPP_'
+$ImportInfisicalSecretParameters.DuplicateKeyBehavior = 'LastWins'
+$ImportInfisicalSecretParameters.AsPlainText = $True
+
+$ImportInfisicalSecretResult = Import-InfisicalSecret @ImportInfisicalSecretParameters
+ Loads a .env file into a plain-text dictionary, namespacing every key with 'MYAPP_' and letting the last occurrence win on duplicates.
+
+
+
+
+
Get-InfisicalProject
@@ -1701,6 +1744,37 @@ $StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessPara
+
+
+ Get-InfisicalEnvironmentVariable
+ Reads an environment variable from the first scope that has a non-empty value (Process > User > Machine).
+ Get
+ InfisicalEnvironmentVariable
+
+
+ Returns the value of -Name from the first scope that contains a non-empty value, checking Process, then User, then Machine in that order. Emits nothing when the variable is missing or blank in every scope, so an assignment yields $null without writing errors or warnings. Platform-unsupported scopes (User and Machine on non-Windows) are silently skipped.
+
+
+ Notes
+
+ Designed for the same discovery semantics the rest of PSInfisicalAPI uses when resolving connection inputs from the environment. Pipe-friendly: accepts -Name from the pipeline by value and by property name.
+
+
+
+
+ EXAMPLE 1
+ $Value = Get-InfisicalEnvironmentVariable -Name 'INFISICAL_CLIENT_ID'
+ Returns the first non-empty value of INFISICAL_CLIENT_ID across Process, User, and Machine scopes; assigns $null when the variable is unset everywhere.
+
+
+ EXAMPLE 2
+ @('INFISICAL_BASE_URI','INFISICAL_PROJECT_ID') | Get-InfisicalEnvironmentVariable
+ Pipes a list of variable names through the cmdlet and emits one value per name that is set in any scope.
+
+
+
+
+
Get-InfisicalOrganization
diff --git a/build.ps1 b/build.ps1
index 66299a1..d0b2edb 100644
--- a/build.ps1
+++ b/build.ps1
@@ -113,6 +113,7 @@ function Write-Manifest {
'Copy-InfisicalSecret',
'ConvertTo-InfisicalSecretDictionary',
'Export-InfisicalSecrets',
+ 'Import-InfisicalSecret',
'Get-InfisicalProject',
'New-InfisicalProject',
'Update-InfisicalProject',
@@ -154,6 +155,7 @@ function Write-Manifest {
'Export-InfisicalScepMdmProfile',
'Write-InfisicalScepMdmProfileToWmi',
'Start-InfisicalProcess',
+ 'Get-InfisicalEnvironmentVariable',
'Get-InfisicalSANList'
)
AliasesToExport = @()
@@ -219,7 +221,7 @@ if (`$cmds.Count -eq 0) {
throw "No cmdlets were exported by the PSInfisicalAPI module."
}
-`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalOrganization','New-InfisicalOrganization','Update-InfisicalOrganization','Remove-InfisicalOrganization','Get-InfisicalSubOrganization','New-InfisicalSubOrganization','Update-InfisicalSubOrganization','Remove-InfisicalSubOrganization','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess','Get-InfisicalSANList')
+`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Import-InfisicalSecret','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalOrganization','New-InfisicalOrganization','Update-InfisicalOrganization','Remove-InfisicalOrganization','Get-InfisicalSubOrganization','New-InfisicalSubOrganization','Update-InfisicalSubOrganization','Remove-InfisicalSubOrganization','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess','Get-InfisicalEnvironmentVariable','Get-InfisicalSANList')
foreach (`$expected in `$expectedCmds) {
if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
throw "Cmdlet not found: `$expected"
diff --git a/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs
index 25925f4..2009ad3 100644
--- a/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs
+++ b/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs
@@ -21,6 +21,9 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter]
public SwitchParameter AsPlainText { get; set; }
+ [Parameter]
+ public string Prefix { get; set; }
+
private readonly List _buffer = new List();
protected override void ProcessRecord()
@@ -60,10 +63,11 @@ namespace PSInfisicalAPI.Cmdlets
private Dictionary BuildDictionary(Func valueSelector)
{
Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ string prefix = Prefix ?? string.Empty;
foreach (InfisicalSecret secret in _buffer)
{
- string key = secret.SecretName ?? string.Empty;
+ string key = string.Concat(prefix, secret.SecretName ?? string.Empty);
if (dictionary.ContainsKey(key))
{
diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentVariableCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentVariableCmdlet.cs
new file mode 100644
index 0000000..472c423
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentVariableCmdlet.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Management.Automation;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsCommon.Get, "InfisicalEnvironmentVariable")]
+ [OutputType(typeof(string))]
+ public sealed class GetInfisicalEnvironmentVariableCmdlet : PSCmdlet
+ {
+ private static readonly EnvironmentVariableTarget[] TargetOrder = new[]
+ {
+ EnvironmentVariableTarget.Process,
+ EnvironmentVariableTarget.User,
+ EnvironmentVariableTarget.Machine
+ };
+
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
+ [ValidateNotNullOrEmpty]
+ public string Name { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ foreach (EnvironmentVariableTarget target in TargetOrder)
+ {
+ string value;
+ try
+ {
+ value = Environment.GetEnvironmentVariable(Name, target);
+ }
+ catch
+ {
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(value))
+ {
+ WriteObject(value);
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/ImportInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ImportInfisicalSecretCmdlet.cs
new file mode 100644
index 0000000..477e99a
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/ImportInfisicalSecretCmdlet.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Management.Automation;
+using System.Security;
+using PSInfisicalAPI.Errors;
+using PSInfisicalAPI.Imports;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Security;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsData.Import, "InfisicalSecret")]
+ [OutputType(typeof(Dictionary))]
+ [OutputType(typeof(Dictionary))]
+ public sealed class ImportInfisicalSecretCmdlet : InfisicalCmdletBase
+ {
+ [Parameter(Mandatory = true, Position = 0)]
+ [ValidateNotNull]
+ public FileInfo Path { get; set; }
+
+ [Parameter(Mandatory = true)]
+ public InfisicalImportFormat Format { get; set; }
+
+ [Parameter]
+ public InfisicalDuplicateKeyBehavior DuplicateKeyBehavior { get; set; } = InfisicalDuplicateKeyBehavior.Error;
+
+ [Parameter]
+ public SwitchParameter AsPlainText { get; set; }
+
+ [Parameter]
+ public string Prefix { get; set; }
+
+ protected override void EndProcessing()
+ {
+ try
+ {
+ Path.Refresh();
+ if (!Path.Exists)
+ {
+ throw new InfisicalImportException(string.Concat("Import path does not exist: ", Path.FullName));
+ }
+
+ IInfisicalImporter importer = InfisicalImporterFactory.Create(Format);
+ IList> pairs = importer.Import(Path);
+
+ if (AsPlainText.IsPresent)
+ {
+ Dictionary plain = BuildDictionary(pairs, value => value ?? string.Empty);
+ WriteObject(plain);
+ }
+ else
+ {
+ Dictionary secure = BuildDictionary(pairs, value => SecureStringUtility.ToReadOnlySecureString(value ?? string.Empty));
+ WriteObject(secure);
+ }
+ }
+ catch (Exception exception)
+ {
+ ThrowTerminatingForException("ImportInfisicalSecretCmdlet", "ImportSecret", exception);
+ }
+ }
+
+ private Dictionary BuildDictionary(
+ IList> pairs,
+ Func valueSelector)
+ {
+ Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ string prefix = Prefix ?? string.Empty;
+
+ foreach (KeyValuePair pair in pairs)
+ {
+ if (pair.Key == null) { continue; }
+ string key = string.Concat(prefix, pair.Key);
+
+ if (dictionary.ContainsKey(key))
+ {
+ if (DuplicateKeyBehavior == InfisicalDuplicateKeyBehavior.Error)
+ {
+ throw new InfisicalConfigurationException(string.Concat("Duplicate secret name encountered: ", key));
+ }
+
+ if (DuplicateKeyBehavior == InfisicalDuplicateKeyBehavior.LastWins)
+ {
+ dictionary[key] = valueSelector(pair.Value);
+ }
+
+ continue;
+ }
+
+ dictionary[key] = valueSelector(pair.Value);
+ }
+
+ return dictionary;
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Errors/InfisicalException.cs b/src/PSInfisicalAPI/Errors/InfisicalException.cs
index 98f2c1f..bd29745 100644
--- a/src/PSInfisicalAPI/Errors/InfisicalException.cs
+++ b/src/PSInfisicalAPI/Errors/InfisicalException.cs
@@ -75,6 +75,13 @@ namespace PSInfisicalAPI.Errors
public InfisicalExportException(string message, Exception innerException) : base(message, innerException) { }
}
+ public class InfisicalImportException : InfisicalException
+ {
+ public InfisicalImportException() { }
+ public InfisicalImportException(string message) : base(message) { }
+ public InfisicalImportException(string message, Exception innerException) : base(message, innerException) { }
+ }
+
public class InfisicalConfigurationException : InfisicalException
{
public InfisicalConfigurationException() { }
diff --git a/src/PSInfisicalAPI/Imports/EnvInfisicalImporter.cs b/src/PSInfisicalAPI/Imports/EnvInfisicalImporter.cs
new file mode 100644
index 0000000..54f9c02
--- /dev/null
+++ b/src/PSInfisicalAPI/Imports/EnvInfisicalImporter.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.IO;
+using PSInfisicalAPI.Errors;
+
+namespace PSInfisicalAPI.Imports
+{
+ public sealed class EnvInfisicalImporter : IInfisicalImporter
+ {
+ public IList> Import(FileInfo path)
+ {
+ if (path == null) { throw new InfisicalImportException("Path is required for ENV import."); }
+
+ List> result = new List>();
+ string[] lines = File.ReadAllLines(path.FullName);
+
+ foreach (string raw in lines)
+ {
+ if (raw == null) { continue; }
+ string line = raw.Trim();
+ if (line.Length == 0) { continue; }
+ if (line[0] == '#') { continue; }
+
+ int idx = line.IndexOf('=');
+ if (idx <= 0) { continue; }
+
+ string key = line.Substring(0, idx).Trim();
+ string value = line.Substring(idx + 1);
+ if (key.Length == 0) { continue; }
+
+ result.Add(new KeyValuePair(key, value));
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Imports/IInfisicalImporter.cs b/src/PSInfisicalAPI/Imports/IInfisicalImporter.cs
new file mode 100644
index 0000000..26d5d84
--- /dev/null
+++ b/src/PSInfisicalAPI/Imports/IInfisicalImporter.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace PSInfisicalAPI.Imports
+{
+ public interface IInfisicalImporter
+ {
+ IList> Import(FileInfo path);
+ }
+}
diff --git a/src/PSInfisicalAPI/Imports/InfisicalImporterFactory.cs b/src/PSInfisicalAPI/Imports/InfisicalImporterFactory.cs
new file mode 100644
index 0000000..5ace46c
--- /dev/null
+++ b/src/PSInfisicalAPI/Imports/InfisicalImporterFactory.cs
@@ -0,0 +1,20 @@
+using PSInfisicalAPI.Errors;
+using PSInfisicalAPI.Models;
+
+namespace PSInfisicalAPI.Imports
+{
+ public static class InfisicalImporterFactory
+ {
+ public static IInfisicalImporter Create(InfisicalImportFormat format)
+ {
+ switch (format)
+ {
+ case InfisicalImportFormat.Json: return new JsonInfisicalImporter();
+ case InfisicalImportFormat.Yaml: return new YamlInfisicalImporter();
+ case InfisicalImportFormat.Env: return new EnvInfisicalImporter();
+ case InfisicalImportFormat.Xml: return new XmlInfisicalImporter();
+ default: throw new InfisicalImportException(string.Concat("Unsupported import format: ", format.ToString()));
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Imports/JsonInfisicalImporter.cs b/src/PSInfisicalAPI/Imports/JsonInfisicalImporter.cs
new file mode 100644
index 0000000..c507296
--- /dev/null
+++ b/src/PSInfisicalAPI/Imports/JsonInfisicalImporter.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.IO;
+using Newtonsoft.Json.Linq;
+using PSInfisicalAPI.Errors;
+
+namespace PSInfisicalAPI.Imports
+{
+ public sealed class JsonInfisicalImporter : IInfisicalImporter
+ {
+ public IList> Import(FileInfo path)
+ {
+ if (path == null) { throw new InfisicalImportException("Path is required for JSON import."); }
+
+ List> result = new List>();
+ string text = File.ReadAllText(path.FullName);
+ JToken root = JToken.Parse(text);
+
+ if (root.Type == JTokenType.Array)
+ {
+ foreach (JToken item in (JArray)root)
+ {
+ if (item == null || item.Type != JTokenType.Object) { continue; }
+ string key = ReadString((JObject)item, "SecretName") ?? ReadString((JObject)item, "secretName");
+ string value = ReadString((JObject)item, "SecretValue") ?? ReadString((JObject)item, "secretValue");
+ if (string.IsNullOrEmpty(key)) { continue; }
+ result.Add(new KeyValuePair(key, value ?? string.Empty));
+ }
+ }
+ else if (root.Type == JTokenType.Object)
+ {
+ foreach (JProperty prop in ((JObject)root).Properties())
+ {
+ if (prop == null || string.IsNullOrEmpty(prop.Name)) { continue; }
+ string value = prop.Value != null && prop.Value.Type != JTokenType.Null ? prop.Value.ToString() : string.Empty;
+ result.Add(new KeyValuePair(prop.Name, value));
+ }
+ }
+ else
+ {
+ throw new InfisicalImportException("JSON import expects an array of secret objects or a flat key/value object.");
+ }
+
+ return result;
+ }
+
+ private static string ReadString(JObject obj, string name)
+ {
+ JToken token;
+ if (!obj.TryGetValue(name, out token) || token == null || token.Type == JTokenType.Null) { return null; }
+ return token.ToString();
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Imports/XmlInfisicalImporter.cs b/src/PSInfisicalAPI/Imports/XmlInfisicalImporter.cs
new file mode 100644
index 0000000..61f768c
--- /dev/null
+++ b/src/PSInfisicalAPI/Imports/XmlInfisicalImporter.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using PSInfisicalAPI.Errors;
+
+namespace PSInfisicalAPI.Imports
+{
+ public sealed class XmlInfisicalImporter : IInfisicalImporter
+ {
+ public IList> Import(FileInfo path)
+ {
+ if (path == null) { throw new InfisicalImportException("Path is required for XML import."); }
+
+ List> result = new List>();
+ XmlDocument document = new XmlDocument();
+ document.Load(path.FullName);
+
+ XmlNode root = document.DocumentElement;
+ if (root == null || !string.Equals(root.LocalName, "Secrets", System.StringComparison.Ordinal))
+ {
+ throw new InfisicalImportException("XML import expects a root element.");
+ }
+
+ foreach (XmlNode node in root.ChildNodes)
+ {
+ if (node == null || node.NodeType != XmlNodeType.Element) { continue; }
+ if (!string.Equals(node.LocalName, "Secret", System.StringComparison.Ordinal)) { continue; }
+
+ string key = ReadChild(node, "SecretName");
+ string value = ReadChild(node, "SecretValue");
+ if (string.IsNullOrEmpty(key)) { continue; }
+ result.Add(new KeyValuePair(key, value ?? string.Empty));
+ }
+
+ return result;
+ }
+
+ private static string ReadChild(XmlNode parent, string name)
+ {
+ foreach (XmlNode child in parent.ChildNodes)
+ {
+ if (child == null || child.NodeType != XmlNodeType.Element) { continue; }
+ if (string.Equals(child.LocalName, name, System.StringComparison.Ordinal))
+ {
+ return child.InnerText;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Imports/YamlInfisicalImporter.cs b/src/PSInfisicalAPI/Imports/YamlInfisicalImporter.cs
new file mode 100644
index 0000000..a8af0eb
--- /dev/null
+++ b/src/PSInfisicalAPI/Imports/YamlInfisicalImporter.cs
@@ -0,0 +1,61 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using PSInfisicalAPI.Errors;
+using YamlDotNet.Serialization;
+
+namespace PSInfisicalAPI.Imports
+{
+ public sealed class YamlInfisicalImporter : IInfisicalImporter
+ {
+ public IList> Import(FileInfo path)
+ {
+ if (path == null) { throw new InfisicalImportException("Path is required for YAML import."); }
+
+ List> result = new List>();
+ string text = File.ReadAllText(path.FullName);
+
+ IDeserializer deserializer = new DeserializerBuilder().Build();
+ object root = deserializer.Deserialize