Public/Get-InfisicalSecretVersion.ps1

# Get-InfisicalSecretVersion.ps1
# Lists version history for a specific secret.
# Called by: User directly.
# Dependencies: InfisicalSession class, InfisicalSecret class, Invoke-InfisicalApi, Get-InfisicalSession

function Get-InfisicalSecretVersion {
    <#
    .SYNOPSIS
        Lists version history for a secret in Infisical.

    .DESCRIPTION
        Retrieves past versions of a specific secret by querying the Infisical API
        with version parameters. Returns version objects with CreatedAt, Version number,
        and Value (as SecureString).

    .PARAMETER Name
        The name (key) of the secret to get version history for.

    .PARAMETER Environment
        The environment slug. Overrides the session default if specified.

    .PARAMETER SecretPath
        The Infisical folder path. Defaults to "/".

    .PARAMETER ProjectId
        The project/workspace ID. Overrides the session default if specified.

    .PARAMETER Limit
        Maximum number of versions to return. Defaults to 10, maximum 100.
        Each version requires a separate API call, so higher limits increase
        latency and may trigger rate limiting.

    .EXAMPLE
        Get-InfisicalSecretVersion -Name 'DATABASE_URL'

        Returns up to 10 versions of the DATABASE_URL secret.

    .EXAMPLE
        Get-InfisicalSecretVersion 'API_KEY' -Limit 5

        Returns the last 5 versions of the API_KEY secret.

    .OUTPUTS
        PSCustomObject with Version, Value (SecureString), and CreatedAt properties.

    .NOTES
        Version values are returned as SecureString for consistency with the module's
        security model. Use [System.Net.NetworkCredential]::new('', $v.Value).Password
        to retrieve plaintext values when needed.

    .LINK
        Get-InfisicalSecret
    .LINK
        Set-InfisicalSecret
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter()]
        [string] $Environment,

        [Parameter()]
        [Alias('Path')]
        [string] $SecretPath = '/',

        [Parameter()]
        [string] $ProjectId,

        [Parameter()]
        [ValidateRange(1, 100)]
        [int] $Limit = 10
    )

    $session = Get-InfisicalSession

    $resolvedEnvironment = if ([string]::IsNullOrEmpty($Environment)) { $session.DefaultEnvironment } else { $Environment }
    $resolvedProjectId = if ([string]::IsNullOrEmpty($ProjectId)) { $session.ProjectId } else { $ProjectId }

    # Fetch the current secret first to determine the current version number
    $queryParams = @{
        projectId = $resolvedProjectId
        environment = $resolvedEnvironment
        secretPath  = $SecretPath
    }

    $encodedName = [System.Uri]::EscapeDataString($Name)
    $currentResponse = Invoke-InfisicalApi -Method GET -Endpoint "/api/v4/secrets/$encodedName" -QueryParameters $queryParams -Session $session

    if ($null -eq $currentResponse -or $null -eq $currentResponse.secret) {
        $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            [System.Management.Automation.ItemNotFoundException]::new(
                "Secret '$Name' not found in environment '$resolvedEnvironment' at path '$SecretPath'."
            ),
            'InfisicalSecretNotFound',
            [System.Management.Automation.ErrorCategory]::ObjectNotFound,
            $Name
        )
        $PSCmdlet.WriteError($errorRecord)
        return
    }

    $currentVersion = [int]$currentResponse.secret.version
    $versionsToFetch = [math]::Min($Limit, $currentVersion)

    # Retrieve each version by specifying the version query parameter
    for ($v = $currentVersion; $v -gt ($currentVersion - $versionsToFetch); $v--) {
        $versionParams = @{
            projectId = $resolvedProjectId
            environment = $resolvedEnvironment
            secretPath  = $SecretPath
            version     = $v
        }

        $versionResponse = Invoke-InfisicalApi -Method GET -Endpoint "/api/v4/secrets/$encodedName" -QueryParameters $versionParams -Session $session

        if ($null -ne $versionResponse -and $null -ne $versionResponse.secret) {
            $secretData = $versionResponse.secret

            $secureValue = $null
            if ($null -ne $secretData.secretValue) {
                $secureValue = [System.Security.SecureString]::new()
                foreach ($char in $secretData.secretValue.ToString().ToCharArray()) {
                    $secureValue.AppendChar($char)
                }
                $secureValue.MakeReadOnly()
            }

            $createdAt = [datetime]::MinValue
            if ($secretData.createdAt) {
                [void][datetime]::TryParse($secretData.createdAt, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None, [ref]$createdAt)
            }

            [PSCustomObject]@{
                PSTypeName = 'InfisicalSecretVersion'
                Name       = $secretData.secretKey
                Version    = [int]$secretData.version
                Value      = $secureValue
                CreatedAt  = $createdAt
            }
        }
    }
}