PSInfisical.psm1

# PSInfisical.psm1
# Root module loader for PSInfisical. Dot-sources all class, private, and public
# function files in the correct order. Manages module-scoped session state.
# Dependencies: All .ps1 files in Classes/, Private/, and Public/ directories.

#Requires -Version 5.1

Set-StrictMode -Version Latest

# Ensure TLS 1.2 is available. PowerShell 5.1 on older Windows may default to
# TLS 1.0/1.1, which Infisical's API (and most modern services) will reject.
if ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') {
    [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
}

# Module-scoped session variable — stores the current InfisicalSession.
# Accessed via $script:InfisicalSession from within module functions.
$script:InfisicalSession = $null

# Determine module root
$moduleRoot = $PSScriptRoot

# --- Class definitions (inline) ---
# Classes MUST be defined directly in the RootModule (not dot-sourced from
# separate files) so that 'using module PSInfisical' exports the types to
# the caller's scope. Dot-sourced classes only exist inside the module scope
# and are invisible to 'using module'. ScriptsToProcess would also work but
# breaks SecretManagement's Register-SecretVault internal module loading.

class InfisicalSession {
    [string] $ApiUrl
    [string] $OrganizationId
    [string] $ProjectId
    [string] $DefaultEnvironment
    [System.Security.SecureString] $AccessToken
    [Nullable[datetime]] $TokenExpiry
    [string] $AuthMethod          # UniversalAuth, Token, AccessToken
    [string] $ClientId            # Stored for re-auth (UniversalAuth only)
    [System.Security.SecureString] $ClientSecret  # Stored for re-auth (UniversalAuth only)

    [bool] $Connected
    [hashtable] $ApiCapabilities     # Tracks which API versions are available

    InfisicalSession() {
        $this.DefaultEnvironment = 'prod'
        $this.Connected = $false
        $this.ApiCapabilities = @{}
    }

    # Update Connected based on token state
    [void] UpdateConnectionStatus() {
        if ($null -eq $this.AccessToken) {
            $this.Connected = $false
            return
        }
        if ($null -ne $this.TokenExpiry -and $this.TokenExpiry -le [datetime]::UtcNow) {
            $this.Connected = $false
            return
        }
        $this.Connected = $true
    }

    [bool] IsTokenExpiringSoon() {
        if ($null -eq $this.TokenExpiry) {
            return $false
        }
        return ($this.TokenExpiry -le [datetime]::UtcNow.AddSeconds(60))
    }

    [bool] CanReauthenticate() {
        return ($this.AuthMethod -eq 'UniversalAuth' -and
                -not [string]::IsNullOrEmpty($this.ClientId) -and
                $null -ne $this.ClientSecret)
    }

    [string] GetAccessTokenPlainText() {
        if ($null -eq $this.AccessToken) {
            return $null
        }
        return [System.Net.NetworkCredential]::new('', $this.AccessToken).Password
    }

    [string] ToString() {
        $this.UpdateConnectionStatus()
        return "InfisicalSession: ApiUrl=$($this.ApiUrl), OrgId=$($this.OrganizationId), ProjectId=$($this.ProjectId), AuthMethod=$($this.AuthMethod), Connected=$($this.Connected)"
    }
}

class InfisicalSecret {
    [string] $Name
    [System.Security.SecureString] $Value
    [string] $Environment
    [string] $Path
    [string] $ProjectId
    [int] $Version
    [string] $Comment
    [datetime] $CreatedAt
    [datetime] $UpdatedAt
    [string] $Id
    [string[]] $TagIds
    [hashtable] $Metadata
    [int] $ReminderRepeatDays
    [string] $ReminderNote
    [string] $Type              # 'shared' or 'personal'

    InfisicalSecret() {
        $this.TagIds = @()
        $this.Metadata = @{}
        $this.Type = 'shared'
    }

    # Decrypts and returns the plaintext value. Use with care — the plaintext
    # string will remain in managed memory until garbage collected.
    [string] GetValue() {
        if ($null -eq $this.Value) {
            return $null
        }
        return [System.Net.NetworkCredential]::new('', $this.Value).Password
    }

    # Safe display that never leaks the secret value.
    [string] ToString() {
        return "$($this.Name)=<value hidden>"
    }
}

class InfisicalFolder {
    [string] $Id
    [string] $Name
    [string] $Environment
    [string] $Path              # Parent path (e.g., "/" means folder is at root)
    [string] $ProjectId
    [string] $Description
    [datetime] $CreatedAt
    [datetime] $UpdatedAt

    InfisicalFolder() { }

    [string] GetFullPath() {
        $parentPath = $this.Path.TrimEnd('/')
        return "$parentPath/$($this.Name)"
    }

    [string] ToString() {
        return "InfisicalFolder: $($this.GetFullPath()) (Environment=$($this.Environment))"
    }
}

class InfisicalTag {
    [string] $Id
    [string] $Name
    [string] $Slug
    [string] $Color
    [string] $ProjectId
    [datetime] $CreatedAt
    [datetime] $UpdatedAt

    InfisicalTag() { }

    [string] ToString() {
        return "InfisicalTag: $($this.Slug) ($($this.Color))"
    }
}

class InfisicalSecretImport {
    [string] $Id
    [string] $SourceEnvironment
    [string] $SourcePath
    [string] $Environment          # Destination environment
    [string] $Path                 # Destination path
    [string] $ProjectId
    [bool] $IsReplication
    [int] $Position
    [datetime] $CreatedAt
    [datetime] $UpdatedAt

    InfisicalSecretImport() { }

    [string] ToString() {
        $repl = if ($this.IsReplication) { ' [replication]' } else { '' }
        return "InfisicalSecretImport: $($this.SourceEnvironment):$($this.SourcePath) -> $($this.Environment):$($this.Path)$repl"
    }
}

class InfisicalIdentity {
    [string] $Id
    [string] $Name
    [string] $OrganizationId
    [string] $Role
    [string[]] $AuthMethods
    [bool] $HasDeleteProtection
    [hashtable] $Metadata
    [datetime] $CreatedAt
    [datetime] $UpdatedAt

    InfisicalIdentity() {
        $this.AuthMethods = @()
        $this.Metadata = @{}
    }

    [string] ToString() {
        return "InfisicalIdentity: $($this.Name) (Role=$($this.Role))"
    }
}

# --- Register type accelerators ---
# PowerShell classes defined in a module are NOT visible as type literals
# (e.g. [InfisicalSession]) when dot-sourcing function files within the same
# module. Type accelerators make the types globally resolvable at parse time,
# which is required for [OutputType()], parameter types, and ::new() calls
# in dot-sourced function files.
$script:TypeAcceleratorsType = [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
@(
    @{ Name = 'InfisicalSession'; Type = [InfisicalSession] }
    @{ Name = 'InfisicalSecret';  Type = [InfisicalSecret] }
    @{ Name = 'InfisicalFolder';  Type = [InfisicalFolder] }
    @{ Name = 'InfisicalTag';     Type = [InfisicalTag] }
    @{ Name = 'InfisicalSecretImport'; Type = [InfisicalSecretImport] }
    @{ Name = 'InfisicalIdentity';     Type = [InfisicalIdentity] }
) | ForEach-Object {
    if (-not $script:TypeAcceleratorsType::Get.ContainsKey($_.Name)) {
        $script:TypeAcceleratorsType::Add($_.Name, $_.Type)
    }
}

# Clean up type accelerators when the module is removed to avoid leaking
# into the session after Remove-Module.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    @('InfisicalSession', 'InfisicalSecret', 'InfisicalFolder', 'InfisicalTag', 'InfisicalSecretImport', 'InfisicalIdentity') | ForEach-Object {
        if ($script:TypeAcceleratorsType::Get.ContainsKey($_)) {
            $script:TypeAcceleratorsType::Remove($_)
        }
    }
}

# --- Dot-source Private functions ---
$privatePath = Join-Path -Path $moduleRoot -ChildPath 'Private'
if (Test-Path -Path $privatePath) {
    $privateFiles = Get-ChildItem -Path $privatePath -Filter '*.ps1' -File
    foreach ($file in $privateFiles) {
        . $file.FullName
    }
}

# --- Dot-source Public functions ---
$publicPath = Join-Path -Path $moduleRoot -ChildPath 'Public'
if (Test-Path -Path $publicPath) {
    $publicFiles = Get-ChildItem -Path $publicPath -Filter '*.ps1' -File
    foreach ($file in $publicFiles) {
        . $file.FullName
    }
}

# Note: FunctionsToExport in the manifest controls which functions are exported.
# Private functions are NOT listed there and therefore not accessible to module consumers.