src/_Connect-CciBlobStore.ps1

function _Connect-CciBlobStore {
<#
.SYNOPSIS
    Establishes an Entra-authenticated context for the tenant distribution store.
.DESCRIPTION
    Machine builders hold `Storage Blob Data Reader` on the tenant store via an
    Entra group and need no Azure DevOps entitlement.
 
    Auth order:
      1. A live Azure CLI session, if present AND signed in to the tenant that
         owns the store (no prompt).
      2. Device-code sign-in over plain REST (_Get-CciEntraToken).
 
    No Az PowerShell modules are used - see _Get-CciEntraToken for why.
    Returns a context object (Account/Token/TenantId) or $null.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]$Feed
    )

    $accountProp = $Feed.PSObject.Properties['blobAccount']
    $account = if ($accountProp) { $accountProp.Value }
    if ([string]::IsNullOrWhiteSpace($account)) {
        Write-Verbose "cciget: tenant '$($Feed.name)' has no blobAccount configured."
        return $null
    }
    # Read through PSObject.Properties throughout: the module loads under
    # Set-StrictMode -Version Latest, where touching an absent property throws.
    $tenantProp = $Feed.PSObject.Properties['tenantId']
    $tenantId = if ($tenantProp) { $tenantProp.Value }

    if ($Script:CciGetBlobContext -and $Script:CciGetBlobContext.Account -eq $account) {
        return $Script:CciGetBlobContext
    }

    $token = $null

    # 1. existing Azure CLI session - but ONLY when it is signed in to the
    # tenant that owns the store. az mints a storage token from whatever
    # tenant the session happens to be in; the store then rejects it, and
    # before 0.6.2 that arrived as a false "this store has no module index
    # yet" against a store that was healthy and full. A technician whose
    # default az context is some other tenant hit it every time, while a
    # machine with no az session at all worked - because the device-code
    # path below always signs in against $Feed.tenantId explicitly.
    # A feed with no tenantId keeps the old take-what-az-gives behaviour;
    # there is nothing to compare against.
    if (Get-Command az -ErrorAction SilentlyContinue) {
        $azTenant = $null
        try { $azTenant = az account show --query tenantId -o tsv 2>$null } catch { }
        # -o tsv hands back a string[] if anything ever emits more than one line.
        if ($azTenant -is [System.Array]) { $azTenant = @($azTenant)[0] }
        $azTenant = "$azTenant".Trim()

        if (-not $azTenant) {
            Write-Verbose 'cciget: no Azure CLI session to borrow a storage token from.'
        }
        elseif ($tenantId -and $azTenant -ne $tenantId) {
            Write-Verbose ("cciget: the Azure CLI session is signed in to tenant '$azTenant' but the " +
                "'$($Feed.name)' distribution store is in '$tenantId'; ignoring that session and signing in separately.")
        }
        else {
            try {
                $token = az account get-access-token --resource 'https://storage.azure.com' --query accessToken -o tsv 2>$null
                if ($token) { Write-Verbose 'cciget: storage token from the Azure CLI session.' }
            } catch { }
        }
    }

    # 2. device code over REST
    if (-not $token) {
        if (-not $tenantId) {
            Write-Warning "cciget: tenant '$($Feed.name)' has no tenantId; cannot sign in to the distribution store."
            return $null
        }
        $clientProp = $Feed.PSObject.Properties['clientId']
        $clientId = if ($clientProp -and $clientProp.Value) { $clientProp.Value } else { '04b07795-8ddb-461a-bbee-02f9e1bf7b46' }

        Write-Host 'cciget: signing in to the tenant distribution store...'
        $token = _Get-CciEntraToken -TenantId $tenantId -Scope 'https://storage.azure.com/.default' -ClientId $clientId
        if (-not $token) { return $null }
    }

    $ctx = [pscustomobject]@{
        Account  = $account
        Token    = $token
        TenantId = $tenantId
    }
    $Script:CciGetBlobContext = $ctx
    return $ctx
}