entropy-auth 2026.7.31

Authentication and authorization for Entropy Softworks server and API projects
#!/usr/bin/env pwsh

# ======================================================================================
# common.ps1 — Shared infrastructure for entropy-auth CI/CD scripts
# ======================================================================================
#
# Provides logging, operation timing, and tool verification functions used across
# build.ps1, test.ps1, and promote.ps1.
#
# Usage: Dot-source this file after any param() block but before function calls:
#
#     . "$PSScriptRoot/common.ps1"
#
# Prerequisites (must be set by the calling script BEFORE first function call):
#
#     $Script:LOG_LEVEL      — Logging verbosity: DEBUG, INFO, WARNING, ERROR
#     $Script:LOG_FILE_NAME  — Log file basename (e.g. "entropy_auth_build.log")
#
# ======================================================================================

# Runtime performance tracking arrays — initialised once per script invocation.
# Because this file is dot-sourced, $Script: refers to the caller's scope.
# Set-StrictMode -Version Latest makes reading an unset variable throw, so probe
# for existence with Get-Variable rather than dereferencing the variable.
if (-not (Get-Variable -Name 'TRACKED_OP_NAMES' -Scope 'Script' -ErrorAction SilentlyContinue))
{
    $Script:TRACKED_OP_NAMES = @()
    $Script:TRACKED_OP_STARTS = @()
    $Script:TRACKED_OP_ELAPSED = @()
}

# ======================
# Logging
# ======================

function Get-LogPriority
{
    param (
        [string]$level
    )

    switch ($level)
    {
        "ERROR"
        { 0
        }
        "WARNING"
        { 1
        }
        "INFO"
        { 2
        }
        "DEBUG"
        { 3
        }
        default
        { -1
        }
    }
}

# Structured logging with timestamps and verbosity filtering
function Write-Log
{
    param (
        [string]$LogLevel,
        [string]$LogMessage,
        [string]$OperationName = ""
    )

    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

    # Construct the formatted log line
    $outputLine = "[$LogLevel] [$timestamp] $LogMessage"

    # Append elapsed time when an operation name is supplied on INFO messages
    if ($LogLevel -eq "INFO" -and $OperationName -ne "")
    {
        $elapsed = Get-OperationTime -Operation $OperationName -Type "duration"
        if ($null -ne $elapsed -and $elapsed -ne "")
        {
            $readableDuration = Format-Duration -Seconds $elapsed
            $outputLine = "$outputLine (Duration: $readableDuration)"
        }
    }

    # Resolve the user home directory (cross-platform)
    $userHome = if ($IsWindows)
    {
        $env:USERPROFILE
    } else
    {
        $env:HOME
    }

    # Platform-appropriate log directory
    $logDirectory = if ($IsWindows)
    {
        Join-Path -Path $userHome -ChildPath "AppData\Local\Logs"
    } elseif ($IsMacOS)
    {
        Join-Path -Path $userHome -ChildPath "Library/Logs"
    } else
    {
        Join-Path -Path $userHome -ChildPath ".local/share/logs"
    }

    # Use the script-specific log file name, falling back to a generic default
    $logFileName = if (-not [string]::IsNullOrEmpty($Script:LOG_FILE_NAME))
    {
        $Script:LOG_FILE_NAME
    } else
    {
        "entropy_auth.log"
    }

    $logFilePath = Join-Path -Path $logDirectory -ChildPath $logFileName

    # Ensure the log directory exists
    if (-not (Test-Path -Path $logDirectory))
    {
        New-Item -Path $logDirectory -ItemType Directory -Force | Out-Null
    }

    # Compare message priority against the configured threshold
    $msgPriority = Get-LogPriority -Level $LogLevel
    $thresholdPriority = Get-LogPriority -Level $Script:LOG_LEVEL

    # Emit only when the message severity meets or exceeds the threshold
    if ($msgPriority -ge 0 -and $thresholdPriority -ge 0 -and $msgPriority -le $thresholdPriority)
    {
        Write-Host $outputLine
        Add-Content -Path $logFilePath -Value $outputLine
    }
}

# ======================
# Operation Timing
# ======================

# Persist operation timing entries
function Set-OperationTime
{
    param (
        [string]$Operation,
        $Value,
        [string]$Type  # "start" or "duration"
    )

    $index = -1

    # Locate an existing entry or allocate a new one
    for ($i = 0; $i -lt $Script:TRACKED_OP_NAMES.Count; $i++)
    {
        if ($Script:TRACKED_OP_NAMES[$i] -eq $Operation)
        {
            $index = $i
            break
        }
    }

    if ($index -eq -1)
    {
        $index = $Script:TRACKED_OP_NAMES.Count
        $Script:TRACKED_OP_NAMES += $Operation
        $Script:TRACKED_OP_STARTS += ""
        $Script:TRACKED_OP_ELAPSED += ""
    }

    if ($Type -eq "start")
    {
        $Script:TRACKED_OP_STARTS[$index] = $Value
    } else
    {
        $Script:TRACKED_OP_ELAPSED[$index] = $Value
    }
}

# Retrieve operation timing entries
function Get-OperationTime
{
    param (
        [string]$Operation,
        [string]$Type  # "start" or "duration"
    )

    for ($i = 0; $i -lt $Script:TRACKED_OP_NAMES.Count; $i++)
    {
        if ($Script:TRACKED_OP_NAMES[$i] -eq $Operation)
        {
            if ($Type -eq "start")
            {
                return $Script:TRACKED_OP_STARTS[$i]
            } else
            {
                return $Script:TRACKED_OP_ELAPSED[$i]
            }
        }
    }
    return ""
}

# Begin timing a named operation
function Start-OperationTiming
{
    param (
        [string]$Operation
    )

    $epochSeconds = [int](Get-Date -UFormat %s)
    Set-OperationTime -Operation $Operation -Value $epochSeconds -Type "start"
}

# End timing a named operation and compute elapsed seconds
function Stop-OperationTiming
{
    param (
        [string]$Operation
    )

    $epochSeconds = [int](Get-Date -UFormat %s)
    $started = Get-OperationTime -Operation $Operation -Type "start"

    if ($started -ne "")
    {
        $delta = $epochSeconds - $started
        Set-OperationTime -Operation $Operation -Value $delta -Type "duration"
    }
}

# Format seconds into a human-readable duration string
function Format-Duration
{
    param (
        [int]$Seconds
    )

    $hours = [math]::Floor($Seconds / 3600)
    $minutes = [math]::Floor(($Seconds % 3600) / 60)
    $remainingSeconds = $Seconds % 60

    if ($hours -gt 0)
    {
        return "{0}h {1}m {2}s" -f $hours, $minutes, $remainingSeconds
    } elseif ($minutes -gt 0)
    {
        return "{0}m {1}s" -f $minutes, $remainingSeconds
    } else
    {
        return "{0}s" -f $remainingSeconds
    }
}

# Print a summary table of all tracked operation timings
function Show-TimingSummary
{
    Write-Host "`nOperation Timing Summary:"
    Write-Host "------------------------"

    for ($i = 0; $i -lt $Script:TRACKED_OP_NAMES.Count; $i++)
    {
        $opName = $Script:TRACKED_OP_NAMES[$i]
        $opDuration = $Script:TRACKED_OP_ELAPSED[$i]

        if (-not [string]::IsNullOrEmpty($opDuration))
        {
            $formatted = Format-Duration -Seconds $opDuration
            "{0,-25}: {1}" -f $opName, $formatted | Write-Host
        }
    }
}

# ======================
# Tool Verification
# ======================

# Verify that a tool is installed and functional
function Test-ToolInstallation
{
    param (
        [string]$ToolName,
        [string]$VersionFlag = "--version"
    )

    Write-Log -LogLevel "DEBUG" -LogMessage "Checking $ToolName installation..."

    if (-not (Get-Command $ToolName -ErrorAction SilentlyContinue))
    {
        Write-Log -LogLevel "ERROR" -LogMessage "$ToolName is not installed or not in PATH"
        return $false
    }

    try
    {
        $null = & $ToolName $VersionFlag 2>$null
        if ($LASTEXITCODE -ne 0)
        {
            throw "$ToolName version check failed"
        }
    } catch
    {
        Write-Log -LogLevel "ERROR" -LogMessage "$ToolName is installed but not functioning properly"
        return $false
    }

    Write-Log -LogLevel "INFO" -LogMessage "$ToolName installation verified"
    return $true
}