Private/Cache/Save-MgcTokenCache.ps1

function Save-MgcTokenCache {
    <#
    .SYNOPSIS
        Stores tokens for a given cache key. In-memory by default; opt-in to disk.

    .PARAMETER Key
        Cache key, typically "{authority}|{clientId}|{tenantId}".

    .PARAMETER Tokens
        Token response object (must include refresh_token to be worth caching).

    .PARAMETER Persist
        If set, additionally write the entry to disk (DPAPI on Windows, chmod 600 elsewhere).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Key,
        [Parameter(Mandatory)][object]$Tokens,
        [switch]$Persist
    )

    # In-memory store (always)
    $script:MgcMemoryCache[$Key] = $Tokens

    if (-not $Persist) { return }
    if (-not $Tokens.refresh_token) { return }

    $cacheDir  = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'MgGraphCommunity'
    $cacheFile = Join-Path $cacheDir 'tokens.json'

    if (-not (Test-Path $cacheDir)) {
        New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
    }

    # Persist the refresh token + minimal metadata only. The access token is
    # short-lived and never needed across sessions, so it stays off disk.
    $persistable = [pscustomobject]@{
        refresh_token = $Tokens.refresh_token
        token_type    = $Tokens.token_type
        scope         = $Tokens.scope
    }
    $payload = $persistable | ConvertTo-Json -Compress -Depth 5

    # Cross-version safe: Test-MgcIsWindows replaces $IsWindows (PS 6+ only).
    $onWindows = Test-MgcIsWindows
    if ($onWindows) {
        $secure    = ConvertTo-SecureString -String $payload -AsPlainText -Force
        $encrypted = ConvertFrom-SecureString -SecureString $secure
        $entry     = [pscustomobject]@{ encrypted = $true;  data = $encrypted; saved = (Get-Date).ToString('o') }
    } else {
        $entry     = [pscustomobject]@{ encrypted = $false; data = $payload;   saved = (Get-Date).ToString('o') }
    }

    $cache = @{}
    if (Test-Path $cacheFile) {
        try {
            $existing = Get-Content $cacheFile -Raw -Encoding UTF8 | ConvertFrom-Json
            foreach ($prop in $existing.PSObject.Properties) { $cache[$prop.Name] = $prop.Value }
        } catch {
            Write-Verbose "Existing cache unreadable, starting fresh: $_"
        }
    }
    $cache[$Key] = $entry

    # On non-Windows the file holds the refresh token in plaintext, so restrict
    # permissions BEFORE the payload is written (a write-then-chmod leaves a
    # window where the default umask applies). Warn loudly if that fails.
    if (-not $onWindows) {
        $permsOk = $true
        try {
            & chmod 700 $cacheDir
            if ($LASTEXITCODE -ne 0) { $permsOk = $false }
            if (-not (Test-Path $cacheFile)) {
                New-Item -ItemType File -Path $cacheFile -Force | Out-Null
            }
            & chmod 600 $cacheFile
            if ($LASTEXITCODE -ne 0) { $permsOk = $false }
        } catch {
            $permsOk = $false
        }
        if (-not $permsOk) {
            Write-Warning "Could not restrict permissions on '$cacheFile'. The persisted refresh token may be readable by other local users."
        }
    }

    $cache | ConvertTo-Json -Depth 6 | Set-Content -Path $cacheFile -NoNewline -Encoding UTF8
}