Public/Test-NSPSecret.ps1

function Test-NSPSecret {
    <#
    .SYNOPSIS
        Preflight check for one named secret: can it actually be read right now, which source
        would win, and why not if it can't. Never returns the value.

    .DESCRIPTION
        Test-NSPSecretStore answers "is the store healthy" in general. This answers the narrower,
        more common startup question: "can THIS toolkit read the specific secrets it needs, right
        now, before it does any work." A script can loop its required secret names through this at
        startup and report something actionable - "vault is locked", "secret missing", "vault not
        registered" - instead of failing deep into a run on whatever Get-NSPSecret call happens
        first.

        Walks the same three sources as Get-NSPSecret, in the same order, but never calls
        Get-Secret / reads a DPAPI file's contents - only existence/reachability checks, so the
        value itself is never touched:

          1. Environment variable NSP_SECRET_<NAME>.
          2. SecretManagement vault (default 'NSP'). Distinguishes "vault not registered" from
             "vault registered but not reachable (locked / misconfigured)" from "reachable but
             this name isn't in it" - those are three different fixes.
          3. DPAPI fallback file %LOCALAPPDATA%\NSP\Secrets\<name>.sec (existence only).

        Reason is $null when Available is $true.

    .PARAMETER Name
        One or more secret names to check, e.g. 'CW.Control.ApiKey'. Accepts pipeline input so a
        toolkit can check a whole credential set in one call:
        'A','B','C' | Test-NSPSecret

    .PARAMETER Vault
        SecretManagement vault name. Defaults to 'NSP'.

    .EXAMPLE
        Test-NSPSecret -Name 'CW.Automate.ClientId'

    .EXAMPLE
        # startup preflight for a toolkit that needs four Automate credentials
        $required = 'CW.Automate.ClientId', 'CW.Automate.User', 'CW.Automate.Password', 'CW.Automate.TotpSeed'
        $missing = $required | Test-NSPSecret | Where-Object { -not $_.Available }
        if ($missing) {
            $missing | ForEach-Object { Write-Warning "$($_.Name): $($_.Reason)" }
            throw "Missing required secrets - see warnings above."
        }
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string[]]$Name,

        [string]$Vault = $script:NSPVaultName
    )

    begin {
        # Vault reachability doesn't depend on which secret name we're asked about, so it's
        # resolved once here rather than once per -Name when several are piped in.
        $vaultModuleAvailable = [bool](Get-Module -ListAvailable -Name 'Microsoft.PowerShell.SecretManagement')
        $vaultReachable  = $false
        $vaultStateReason = $null

        if ($vaultModuleAvailable) {
            try { Import-Module Microsoft.PowerShell.SecretManagement -ErrorAction Stop } catch {
                $vaultStateReason = "SecretManagement is installed but failed to import: $($_.Exception.Message)"
            }

            if (-not $vaultStateReason) {
                $vaultInfo = $null
                try { $vaultInfo = Get-SecretVault -Name $Vault -ErrorAction Stop } catch { $vaultInfo = $null }

                if (-not $vaultInfo) {
                    $vaultStateReason = "vault '$Vault' is not registered (run Initialize-NSPSecretStore)"
                } else {
                    try {
                        # Existence probe only - result discarded, matches Test-NSPSecretStore's
                        # own reachability check. This is what surfaces "vault is locked" as
                        # distinct from "secret missing".
                        $null = Get-SecretInfo -Vault $Vault -ErrorAction Stop
                        $vaultReachable = $true
                    } catch {
                        $vaultStateReason = "vault '$Vault' is registered but not reachable right now - locked or misconfigured: $($_.Exception.Message)"
                    }
                }
            }
        } else {
            $vaultStateReason = 'SecretManagement module is not installed'
        }
    }

    process {
        foreach ($oneName in $Name) {
            $envName = 'NSP_SECRET_' + (($oneName -replace '[^A-Za-z0-9]', '_').ToUpperInvariant())
            if (-not [string]::IsNullOrEmpty([Environment]::GetEnvironmentVariable($envName))) {
                [pscustomobject]@{
                    Name      = $oneName
                    Available = $true
                    Source    = 'EnvironmentVariable'
                    Detail    = $envName
                    Reason    = $null
                }
                continue
            }

            if ($vaultReachable -and (Get-SecretInfo -Name $oneName -Vault $Vault -ErrorAction SilentlyContinue)) {
                [pscustomobject]@{
                    Name      = $oneName
                    Available = $true
                    Source    = 'Vault'
                    Detail    = $Vault
                    Reason    = $null
                }
                continue
            }

            $path = Get-NSPSecretFallbackPath -Name $oneName
            if (Test-Path -LiteralPath $path) {
                [pscustomobject]@{
                    Name      = $oneName
                    Available = $true
                    Source    = 'File'
                    Detail    = $path
                    Reason    = $null
                }
                continue
            }

            $vaultPart = if ($vaultReachable) { "not present in vault '$Vault'" } else { $vaultStateReason }
            [pscustomobject]@{
                Name      = $oneName
                Available = $false
                Source    = 'None'
                Detail    = $null
                Reason    = "Not found: $vaultPart; no DPAPI fallback file either. Store it: Set-NSPSecret -Name '$oneName'."
            }
        }
    }
}