plugins/vault/public/Set-JaxVaultToken.ps1

function Set-JaxVaultToken {
    [CmdletBinding()]
    [Alias('Connect-JaxVault', 'Set-VaultToken')]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string] $VaultToken,

        [Parameter(Mandatory = $false)]
        [string] $VaultAddress,

        [Parameter(Mandatory = $false)]
        [ValidateSet('token', 'github', 'ldap', 'userpass')]
        [string] $Method
    )

    $commonParams = Get-JaxCommonParameters -BoundParameters $PSBoundParameters

    $repoRoot = Get-JaxRepoRoot @commonParams
    $config = Get-JaxConfig -RepoRoot $repoRoot @commonParams

    $vaultCfg = $null
    try { $vaultCfg = Get-JaxVaultPluginConfig -RepoRoot $repoRoot -Config $config } catch {}

    $configHasAddress = $vaultCfg -and $vaultCfg.Contains('address') -and $vaultCfg['address'] -is [string] -and -not [string]::IsNullOrWhiteSpace($vaultCfg['address'])
    $configHasTokenDir = $vaultCfg -and $vaultCfg.Contains('tokenDir') -and $vaultCfg['tokenDir'] -is [string] -and -not [string]::IsNullOrWhiteSpace($vaultCfg['tokenDir'])
    $configHasPolicy = $vaultCfg -and $vaultCfg.Contains('policy') -and $vaultCfg['policy'] -is [string] -and -not [string]::IsNullOrWhiteSpace($vaultCfg['policy'])
    $configHasTokenTtl = $vaultCfg -and $vaultCfg.Contains('tokenTtl') -and $vaultCfg['tokenTtl'] -is [string] -and -not [string]::IsNullOrWhiteSpace($vaultCfg['tokenTtl'])
    $authMount = 'github'
    if ($vaultCfg -and $vaultCfg.Contains('authMount') -and $vaultCfg['authMount'] -is [string] -and -not [string]::IsNullOrWhiteSpace($vaultCfg['authMount'])) {
        $authMount = ([string]$vaultCfg['authMount']).Trim().Trim('/')
    }
    if ($authMount -notmatch '^[A-Za-z0-9][A-Za-z0-9_.\-/]*$' -or $authMount -match '(^|/)\.\.($|/)') {
        throw "Invalid Vault GitHub auth mount '$authMount'."
    }

    function ConvertTo-JaxVaultTokenTtlValue {
        param([string] $value)
        if ([string]::IsNullOrWhiteSpace($value)) { return '8h' }
        $v = $value.Trim()
        if ($v -match '^\d+$') { return "${v}h" }
        return $v
    }

    # Resolve address:
    # - explicit argument wins
    # - then repo config (.jax/jax.config.yml / legacy jax.config.yml)
    # - then env var (fallback only)
    if ([string]::IsNullOrWhiteSpace($VaultAddress)) {
        try {
            if ($configHasAddress) {
                $VaultAddress = $vaultCfg['address']
            }
        } catch {
            # best-effort
        }
    }
    if ([string]::IsNullOrWhiteSpace($VaultAddress)) {
        if (Test-Path Env:\VAULT_ADDR) {
            $VaultAddress = (Get-Item Env:\VAULT_ADDR).Value
        }
    }

    # Config is source of truth: if config doesn't have address yet, ask once (defaulting to env) and save it.
    if (-not $configHasAddress) {
        $addrDefault = $VaultAddress
        if ([string]::IsNullOrWhiteSpace($addrDefault)) { $addrDefault = 'http://127.0.0.1:8200' }
        $VaultAddress = Read-JaxPromptString -Prompt 'Vault address' -Default $addrDefault
    }

    if (-not [string]::IsNullOrWhiteSpace($VaultAddress)) {
        $env:VAULT_ADDR = $VaultAddress
        Write-Host "Vault address set to: $VaultAddress" -ForegroundColor Cyan
    }

    # Config is source of truth: ask tokenDir once (when missing) and save it.
    $tokenDir = $null
    if ($configHasTokenDir) {
        $tokenDir = $vaultCfg['tokenDir']
    }
    if (-not $configHasTokenDir) {
        $tokenDirDefault = '.jax/vault'
        $tokenDir = Read-JaxPromptString -Prompt "Token directory (relative to HOME or absolute; shortcut: enter a project key)" -Default $tokenDirDefault
        if ([string]::IsNullOrWhiteSpace($tokenDir)) {
            $tokenDir = $tokenDirDefault
        }
    }

    # Config is source of truth: ask policy once (when missing) and save it.
    $policy = $null
    if ($configHasPolicy) {
        $policy = $vaultCfg['policy']
    }
    if (-not $configHasPolicy) {
        $policy = Read-JaxPromptString -Prompt 'Vault policy name (optional; blank keeps the login token)' -Default ''
    }

    # Config is source of truth: token TTL for optional child-token creation.
    $tokenTtl = $null
    if ($configHasTokenTtl) {
        $tokenTtl = [string]$vaultCfg['tokenTtl']
    }

    $shouldUpdateTokenTtl = $false
    if ($configHasTokenTtl) {
        $ttlPrompt = "Change Vault token TTL? (current: $tokenTtl)"
        $shouldUpdateTokenTtl = Read-JaxPromptBool -Prompt $ttlPrompt -Default $false
    }
    if ((-not $configHasTokenTtl) -or $shouldUpdateTokenTtl) {
        $ttlDefault = if ($configHasTokenTtl) { $tokenTtl } else { '8h' }
        $tokenTtl = Read-JaxPromptString -Prompt 'Vault token TTL (hours number or duration like 6h, 30m)' -Default $ttlDefault
        if ([string]::IsNullOrWhiteSpace($tokenTtl)) {
            $tokenTtl = $ttlDefault
        }
    }
    $tokenTtl = ConvertTo-JaxVaultTokenTtlValue $tokenTtl

    if ((-not $configHasAddress) -or (-not $configHasTokenDir) -or (-not $configHasPolicy) -or (-not $configHasTokenTtl) -or $shouldUpdateTokenTtl) {
        Update-JaxVaultRepoConfig -RepoRoot $repoRoot -VaultAddress $VaultAddress -TokenDir $tokenDir -Policy $policy -TokenTtl $tokenTtl @commonParams | Out-Null
        # Reload config so downstream path resolution uses the updated config.
        $config = Get-JaxConfig -RepoRoot $repoRoot @commonParams
        try { $vaultCfg = Get-JaxVaultPluginConfig -RepoRoot $repoRoot -Config $config } catch {}
    }

    $tokenPath = Get-JaxVaultTokenPath -RepoRoot $repoRoot -Config $config @commonParams

    $vaultTokenIsNonEmpty = -not [string]::IsNullOrWhiteSpace($VaultToken)
    $methodWasExplicit = $PSBoundParameters.ContainsKey('Method') -and -not [string]::IsNullOrWhiteSpace($Method)
    $tokenWasExplicitAndNonEmpty = $PSBoundParameters.ContainsKey('VaultToken') -and $vaultTokenIsNonEmpty

    # UX: env vars should NOT suppress the auth method prompt (token can be stale/expired).
    # Only skip prompting when user explicitly provided a non-empty token or explicitly specified method.
    if (-not $tokenWasExplicitAndNonEmpty -and -not $methodWasExplicit) {
        $allowedMethods = @('github', 'token')
        $Method = Read-JaxPromptString -Prompt "Select authentication method (github/token)" -Default 'github'
        if (-not ($allowedMethods -contains $Method.ToLowerInvariant())) {
            Write-Warning "Unknown auth method '$Method'. Falling back to 'token'."
            $Method = 'token'
        }
    }
    if ([string]::IsNullOrWhiteSpace($Method)) {
        $Method = 'token'
    }

    $Method = $Method.ToLowerInvariant()
    Write-Verbose "Selected method: $Method"
    if ($Method -eq 'github') {
        Write-Host "Authenticating via GitHub at auth/$authMount..." -ForegroundColor Cyan
        try {
            # Check if we have a GH token in env, else prompt
            $ghToken = if (Test-Path Env:\GITHUB_TOKEN) { $env:GITHUB_TOKEN } else { $null }

            if ([string]::IsNullOrWhiteSpace($ghToken)) {
                $ghToken = Read-Host -Prompt "Enter GitHub Personal Access Token (hidden)" -AsSecureString | ConvertFrom-SecureString -AsPlainText
            }

            if ([string]::IsNullOrWhiteSpace($ghToken)) {
                Write-Error "GitHub token is required."
                return
            }

            # Send the GitHub token over stdin so it is not exposed in the
            # Vault process argument list. The configured mount supports
            # non-default paths such as auth/github-production.
            $json = $ghToken | vault write -format=json "auth/$authMount/login" 'token=-' 2>$null
            if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($json)) {
                Write-Error "Vault login with GitHub failed at auth/$authMount."
                return
            }

            $loginData = $json | ConvertFrom-Json
            $VaultToken = $loginData.auth.client_token
            Write-Host "Successfully authenticated with Vault via GitHub." -ForegroundColor Green

        } catch {
            Write-Error "Error during GitHub authentication: $_"
            return
        }
    } elseif ($Method -eq 'token') {
        if ([string]::IsNullOrWhiteSpace($VaultToken)) {
            $VaultToken = Read-Host -Prompt "Enter Vault Token (hidden)" -AsSecureString | ConvertFrom-SecureString -AsPlainText
        }
    } else {
        Write-Warning "Method '$Method' is not yet fully implemented. Falling back to token prompt."
        if ([string]::IsNullOrWhiteSpace($VaultToken)) {
            $VaultToken = Read-Host -Prompt "Enter Vault Token (hidden)" -AsSecureString | ConvertFrom-SecureString -AsPlainText
        }
    }

    if ([string]::IsNullOrWhiteSpace($VaultToken)) {
        Write-Error "Token cannot be empty."
        return
    }

    if (-not [string]::IsNullOrWhiteSpace($policy)) {
        # Keep a reference to the current token (typically the GitHub login token) so warnings are not misleading.
        $loginToken = $VaultToken

        $env:VAULT_TOKEN = $VaultToken

        # Capture stderr so we can explain failures (do not swallow Vault errors).
        # Use an explicit args array to avoid any accidental literal `$tokenTtl` being passed through.
        $ttlArg = "-ttl=$tokenTtl"
        $formatArg = '-format=json'
        $policyArg = "-policy=$policy"
        $tokenCreateOutput = & vault token create $ttlArg $formatArg $policyArg 2>&1
        $tokenCreateExitCode = $LASTEXITCODE

        if ($tokenCreateExitCode -eq 0 -and $tokenCreateOutput) {
            try {
                $tokenData = $tokenCreateOutput | ConvertFrom-Json -ErrorAction Stop
                if ($tokenData -and $tokenData.auth -and $tokenData.auth.client_token) {
                    $VaultToken = [string]$tokenData.auth.client_token
                    Write-Host "Created token with TTL '$tokenTtl' and policy '$policy'." -ForegroundColor Green
                } else {
                    Write-Warning "Vault token create succeeded but returned no token; keeping current login token."
                }
            } catch {
                Write-Warning "Failed to parse token create response; keeping current login token."
            }
        } else {
            $details = $null
            if ($tokenCreateOutput) {
                $details = ([string]$tokenCreateOutput).Trim()
            }
            if ([string]::IsNullOrWhiteSpace($details)) {
                Write-Warning "Vault token create failed for policy '$policy' (ttl: '$tokenTtl', exit: $tokenCreateExitCode); keeping current login token."
            } else {
                Write-Warning "Vault token create failed for policy '$policy' (ttl: '$tokenTtl', exit: $tokenCreateExitCode). Vault said: $details"
            }

            # Restore the login token explicitly (defensive) and continue.
            $VaultToken = $loginToken
        }
    }

    # Verify/Validate by making a simple call?
    # For now, just save it.

    $tokenDir = Split-Path -Parent $tokenPath
    if (-not (Test-Path $tokenDir)) {
        New-Item -ItemType Directory -Path $tokenDir -Force | Out-Null
    }
    Set-Content -Path $tokenPath -Value $VaultToken -Force
    Set-JaxVaultTokenPermissions -TokenDirectory $tokenDir -TokenPath $tokenPath

    # Also set in current session
    $env:VAULT_TOKEN = $VaultToken

    Write-Host "Vault token saved successfully." -ForegroundColor Green
}