Public/Set-NSPSecret.ps1

function Set-NSPSecret {
    <#
    .SYNOPSIS
        Stores (or replaces) a named secret.

    .DESCRIPTION
        Writes to the SecretManagement vault (default 'NSP') unless -Scope File is given, which
        writes the DPAPI fallback file instead.

        If -Secret is omitted the value is read with Read-Host -AsSecureString, so it never lands
        in shell history, a script file, or a transcript. Prefer that interactively.

        That prompt is the wrong default for CI or a scheduled script, though - it would hang
        waiting on a console that isn't there. Pass -NonInteractive to turn a missing -Secret into
        an immediate, clear throw instead of a prompt.

    .PARAMETER Name
        Secret name, e.g. 'CW.Control.ApiKey'.

    .PARAMETER Secret
        The value: a [string], a [SecureString], or omitted (prompt, unless -NonInteractive). A
        plain string is accepted for scripted migration but is the least safe path.

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

    .PARAMETER Scope
        'Vault' (default) or 'File' (DPAPI fallback, current user + machine only).

    .PARAMETER NonInteractive
        Throw instead of prompting when -Secret is omitted. For CI/scheduled use, where a
        Read-Host prompt would just hang.

    .EXAMPLE
        Set-NSPSecret -Name 'CW.Control.ApiKey'
        # prompts, stores in the NSP vault

    .EXAMPLE
        Set-NSPSecret -Name 'CW.Control.ApiKey' -Scope File
        # prompts, stores in %LOCALAPPDATA%\NSP\Secrets\CW.Control.ApiKey.sec

    .EXAMPLE
        Set-NSPSecret -Name 'CI.Deploy.Token' -Secret $token -NonInteractive
        # scripted: fails fast if $token is somehow empty/unset, instead of hanging on a prompt
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '',
        Justification = 'Migration path: a plaintext value handed in by a tech or read from a legacy file must be converted to a SecureString to be stored securely.')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, Position = 0)][string]$Name,
        [Parameter(Position = 1)][object]$Secret,
        [string]$Vault = $script:NSPVaultName,
        [ValidateSet('Vault', 'File')][string]$Scope = 'Vault',
        [switch]$NonInteractive
    )

    if (-not $PSBoundParameters.ContainsKey('Secret') -or $null -eq $Secret) {
        if ($NonInteractive) {
            throw "Set-NSPSecret: -NonInteractive requires -Secret - no value was supplied for '$Name' and prompting is disabled."
        }
        $secure = Read-Host -Prompt "Enter value for secret '$Name'" -AsSecureString
    } elseif ($Secret -is [System.Security.SecureString]) {
        $secure = $Secret
    } else {
        $secure = ConvertTo-SecureString -String ([string]$Secret) -AsPlainText -Force
    }

    if (-not $secure -or $secure.Length -eq 0) {
        throw "Set-NSPSecret: empty value for '$Name' - nothing stored."
    }

    if ($Scope -eq 'File') {
        $path = Get-NSPSecretFallbackPath -Name $Name
        $dir  = Split-Path -Parent $path
        if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
        if ($PSCmdlet.ShouldProcess($path, 'Write DPAPI-encrypted secret file')) {
            ConvertFrom-SecureString -SecureString $secure | Set-Content -LiteralPath $path -NoNewline -Encoding ascii
            Write-Host "Stored '$Name' at $path (DPAPI: current user + this machine only)." -ForegroundColor Green
        }
        return
    }

    if (-not (Get-Module -ListAvailable -Name 'Microsoft.PowerShell.SecretManagement')) {
        throw "SecretManagement is not installed. Run Initialize-NSPSecretStore first, or use -Scope File."
    }
    Import-Module Microsoft.PowerShell.SecretManagement -ErrorAction Stop
    if (-not (Get-SecretVault -Name $Vault -ErrorAction SilentlyContinue)) {
        throw "Vault '$Vault' is not registered. Run Initialize-NSPSecretStore first."
    }

    if ($PSCmdlet.ShouldProcess("$Vault\$Name", 'Set-Secret')) {
        Set-Secret -Name $Name -SecureString $secure -Vault $Vault -ErrorAction Stop
        Write-Host "Stored '$Name' in vault '$Vault'." -ForegroundColor Green
    }
}