internal/sdk/token-preload.ps1

<#
    Internal runtime for background access token preloading.
 
    Warms the Azure token cache on module import so the first cmdlet call
    does not pay for sequential token minting. A throttled background job
    mints the static resource set once every 45 minutes; the refreshed
    timestamp is tracked in a user-scoped cache file.
 
    All failures are Verbose only and never throw, mirroring the
    provisioning discovery refresh in internal/sdk/provisioning.ps1.
#>


$script:BapTokenPreloadResources = @(
    'https://service.powerapps.com/'
    'https://api.powerplatform.com'
    'https://graph.microsoft.com/'
    '065d9450-1e87-434e-ac2f-69af271549ed'
)

function Get-BapTokenPreloadCachePath {
    [CmdletBinding()]
    param ()

    $base = if ($env:APPDATA) { $env:APPDATA } else { Join-Path $HOME '.config' }
    Join-Path (Join-Path $base 'd365bap.tools') 'token-preload.json'
}

function Start-BapTokenPreload {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseUsingScopeModifierInNewRunspaces", "", Justification = "Start-Job binds values through ArgumentList, the using scope modifier is not supported there.")]
    [CmdletBinding()]
    param (
        [int] $MaxAgeMinutes = 45
    )

    try {
        $azContext = Get-AzContext -ErrorAction SilentlyContinue
        if (-not $azContext) {
            $azCommand = Get-Command az -ErrorAction SilentlyContinue
            $azLoggedIn = $false
            if ($null -ne $azCommand) {
                $null = & az account show -o json 2>$null
                if ($LASTEXITCODE -eq 0) { $azLoggedIn = $true }
            }
            if (-not $azLoggedIn) {
                Write-PSFMessage -Level Verbose -Message 'Token preload skipped: no Az login detected.'
                return
            }
        }

        $userPath = Get-BapTokenPreloadCachePath
        if (Test-Path -LiteralPath $userPath) {
            try {
                $state = Get-Content -LiteralPath $userPath -Raw -Encoding utf8 |
                    ConvertFrom-Json -AsHashtable -ErrorAction Stop
                if ($state.ContainsKey('refreshedUtc')) {
                    $refreshed = [datetime]$state['refreshedUtc']
                    if (([datetime]::UtcNow - $refreshed.ToUniversalTime()).TotalMinutes -lt $MaxAgeMinutes) { return }
                }
            }
            catch {
                Write-PSFMessage -Level Verbose -Message "Ignoring unreadable token preload state at $userPath : $($_.Exception.Message)"
            }
        }

        if (Get-Job -Name 'd365bap.tools.token.preload' -State Running -ErrorAction SilentlyContinue) { return }
        Get-Job -Name 'd365bap.tools.token.preload' -ErrorAction SilentlyContinue | Remove-Job -Force -ErrorAction SilentlyContinue

        # Provider choice cannot be read inside the job: PSFConfig and
        # $script: module state are not shared with the job process, so
        # pass it explicitly and let the job mint fresh tokens.
        $authProvider = Get-PSFConfigValue -FullName 'd365bap.tools.auth.provider' -Fallback 'AzAccount'

        $null = Start-Job -Name 'd365bap.tools.token.preload' -ScriptBlock {
            param ([string] $CachePath, [string] $AuthProvider, [string] $ModuleRoot, [string[]] $Resources)

            try {
                if ($AuthProvider -eq 'AzAccount') {
                    Import-Module Az.Accounts -ErrorAction Stop
                    if (-not (Get-AzContext -ErrorAction SilentlyContinue)) { return }
                }

                if (-not (Get-Command Get-BapAccessToken -ErrorAction SilentlyContinue)) {
                    $helperPath = Join-Path $ModuleRoot 'internal/functions/Get-BapAccessToken.ps1'
                    if (Test-Path -LiteralPath $helperPath) { . $helperPath }
                    else { return }
                }

                foreach ($resource in $Resources) {
                    try {
                        $null = Get-BapAccessToken -ResourceUrl $resource
                    }
                    catch { Write-PSFMessage -Level Verbose -Message "Token preload: skipping resource '$resource'." }
                }

                # $script:BapTokenCache updated inside Get-BapAccessToken is
                # best-effort here: job state is not shared with the parent
                # session, but minting still warms the provider token cache.
                $null = New-Item -ItemType Directory -Path (Split-Path -Parent $CachePath) -Force -ErrorAction Stop
                @{ refreshedUtc = [datetime]::UtcNow.ToString('o') } |
                    ConvertTo-Json -ErrorAction Stop |
                    Set-Content -LiteralPath $CachePath -Encoding utf8 -ErrorAction Stop
            }
            catch { Write-PSFMessage -Level Verbose -Message 'Token preload: background probe failed, keeping the previous state.' }
        } -ArgumentList @((Get-BapTokenPreloadCachePath), $authProvider, $script:ModuleRoot, $script:BapTokenPreloadResources)
    }
    catch { Write-PSFMessage -Level Verbose -Message "Token preload was skipped: $($_.Exception.Message)" }
}

Start-BapTokenPreload