Private/SettingsIo.ps1

# JSON read/write helpers. Everything returns new objects; nothing mutates its input.

# Env vars that make Claude Code bypass account OAuth entirely. A profile launch
# is an explicit request for that profile's identity, so these are stripped from
# the child environment with a warning rather than silently honoured.
$script:AuthOverrideEnvVars = @(
    'ANTHROPIC_API_KEY'
    'ANTHROPIC_AUTH_TOKEN'
    'CLAUDE_CODE_OAUTH_TOKEN'
    'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR'
    'CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR'
)

# OTEL keys the company package requires. CCWB's newer credential process runs
# telemetry_enforcement_mode=block, so a profile missing any of these can fail
# to receive AWS credentials at all.
$script:RequiredTelemetryKeys = @(
    'CLAUDE_CODE_ENABLE_TELEMETRY'
    'OTEL_METRICS_EXPORTER'
    'OTEL_LOGS_EXPORTER'
    'OTEL_EXPORTER_OTLP_PROTOCOL'
    'OTEL_EXPORTER_OTLP_ENDPOINT'
    'OTEL_RESOURCE_ATTRIBUTES'
)

function Read-CcsJson {
    <#
    .SYNOPSIS
        Read a JSON file, returning $null when absent and throwing on malformed content.
    #>

    param([Parameter(Mandatory)][string]$Path)

    if (-not (Test-Path -LiteralPath $Path)) { return $null }
    $raw = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
    if ([string]::IsNullOrWhiteSpace($raw)) { return $null }
    try {
        return $raw | ConvertFrom-Json -ErrorAction Stop
    } catch {
        throw "Malformed JSON in $Path : $($_.Exception.Message)"
    }
}

function Write-CcsJson {
    <#
    .SYNOPSIS
        Write an object as UTF-8 JSON, creating parent directories as needed.
    #>

    param(
        [Parameter(Mandatory)][string]$Path,
        [Parameter(Mandatory)]$Value
    )

    $dir = Split-Path -Parent $Path
    if ($dir -and -not (Test-Path -LiteralPath $dir)) {
        New-Item -ItemType Directory -Path $dir -Force | Out-Null
    }
    $json = $Value | ConvertTo-Json -Depth 20
    Set-Content -LiteralPath $Path -Value $json -Encoding utf8NoBOM
}

function Backup-CcsFile {
    <#
    .SYNOPSIS
        Copy a file next to itself with a timestamp suffix. Returns the backup path,
        or $null when the source does not exist.
    #>

    param([Parameter(Mandatory)][string]$Path)

    if (-not (Test-Path -LiteralPath $Path)) { return $null }
    $stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
    $backup = "$Path.bak.$stamp"
    Copy-Item -LiteralPath $Path -Destination $backup -Force
    $backup
}

function Get-CcsSettingsEnv {
    <#
    .SYNOPSIS
        The 'env' block of a settings object as a hashtable (empty when absent).
    #>

    param($Settings)

    $result = @{}
    if (-not $Settings) { return $result }
    $envProp = $Settings.PSObject.Properties['env']
    if (-not $envProp -or -not $envProp.Value) { return $result }
    foreach ($p in $envProp.Value.PSObject.Properties) { $result[$p.Name] = $p.Value }
    $result
}

function Test-CcsBedrockSettings {
    <#
    .SYNOPSIS
        Whether a settings object routes Claude Code to Amazon Bedrock.
    #>

    param($Settings)

    $envMap = Get-CcsSettingsEnv -Settings $Settings
    $flag = $envMap['CLAUDE_CODE_USE_BEDROCK']
    if ($null -eq $flag) { return $false }
    "$flag" -in @('1', 'true', 'True')
}

function Remove-CcsProviderSettings {
    <#
    .SYNOPSIS
        Return a NEW settings object with every provider-specific key removed.
    .DESCRIPTION
        Used when deriving a subscription profile from a Bedrock one: the AWS,
        telemetry and model-pin keys would otherwise keep routing the new profile
        at Bedrock, and Bedrock model IDs are not valid against the Claude API.
        Personal preferences (permissions, plugins, statusline, ...) are kept.
    #>

    param($Settings)

    if (-not $Settings) { return [pscustomobject]@{} }

    $dropTopLevel = @('otelHeadersHelper', 'awsAuthRefresh', 'awsCredentialExport', 'modelOverrides', 'availableModels')
    $dropEnvPrefixes = @('AWS_', 'OTEL_', 'ANTHROPIC_DEFAULT_', 'ANTHROPIC_BEDROCK_')
    $dropEnvExact = @(
        'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_MANTLE', 'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
        'CLAUDE_CODE_SKIP_MANTLE_AUTH', 'CLAUDE_CODE_ENABLE_TELEMETRY',
        'ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL',
        'CLAUDE_CODE_SUBAGENT_MODEL'
    )

    $result = [pscustomobject]@{}
    foreach ($prop in $Settings.PSObject.Properties) {
        if ($prop.Name -in $dropTopLevel) { continue }

        if ($prop.Name -eq 'env') {
            $keptEnv = [pscustomobject]@{}
            foreach ($e in $prop.Value.PSObject.Properties) {
                if ($e.Name -in $dropEnvExact) { continue }
                if ($dropEnvPrefixes | Where-Object { $e.Name.StartsWith($_) }) { continue }
                Add-Member -InputObject $keptEnv -NotePropertyName $e.Name -NotePropertyValue $e.Value
            }
            if (@($keptEnv.PSObject.Properties).Count -gt 0) {
                Add-Member -InputObject $result -NotePropertyName 'env' -NotePropertyValue $keptEnv
            }
            continue
        }

        Add-Member -InputObject $result -NotePropertyName $prop.Name -NotePropertyValue $prop.Value
    }
    $result
}

function Get-CcsAwsProfileNames {
    <#
    .SYNOPSIS
        Profile names declared in ~/.aws/config ("default" plus "profile <name>" sections).
    #>

    param([string]$Path = (Get-CcsAwsConfigPath))

    if (-not (Test-Path -LiteralPath $Path)) { return @() }
    $names = foreach ($line in Get-Content -LiteralPath $Path) {
        if ($line -match '^\s*\[\s*profile\s+(?<n>[^\]]+?)\s*\]\s*$') { $Matches['n'] }
        elseif ($line -match '^\s*\[\s*default\s*\]\s*$') { 'default' }
    }
    @($names)
}