Private/Test-GkConnection.ps1

function Test-GkConnection {
    <#
    .SYNOPSIS
        Pre-flight validation before a public function makes Graph calls: confirms an active
        session, the required scopes (by capability group), and delegated-only constraints.
    .DESCRIPTION
        Throws an actionable, terminating error when the caller is not connected, is missing a
        required scope, or is app-only against a delegated-only function. On success, returns
        the IAuthContext. Called at the top of every public function.

        Scope checking uses capability groups from $script:GkScopeMap: the caller must hold at
        least one scope from each group, so a broad scope (Directory.Read.All) satisfies narrower
        needs without a false failure.
    .OUTPUTS
        Microsoft.Graph.PowerShell.Authentication.IAuthContext
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $FunctionName
    )

    $ctx = Get-MgContext
    if (-not $ctx) {
        $connectHint = Get-GkConnectScopeHint -FunctionName $FunctionName
        $ex = [System.Exception]::new(
            "Not connected to Microsoft Graph. Run: Connect-MgGraph -Scopes $connectHint")
        $er = [System.Management.Automation.ErrorRecord]::new(
            $ex, 'GkNotConnected', [System.Management.Automation.ErrorCategory]::AuthenticationError, $FunctionName)
        $PSCmdlet.ThrowTerminatingError($er)
    }

    if (-not $script:GkScopeMap.ContainsKey($FunctionName)) {
        # Unknown function name — nothing to validate beyond an active session.
        return $ctx
    }
    $entry = $script:GkScopeMap[$FunctionName]

    # Auth-type constraint: delegated-only APIs cannot be served app-only.
    if ($entry.DelegatedOnly -and $ctx.AuthType -eq 'AppOnly') {
        $ex = [System.Exception]::new(
            "$FunctionName requires a delegated (interactive) session — it reads a Graph API with no application permission (e.g. licenseDetails). You are connected app-only. Reconnect with: Connect-MgGraph -Scopes $(Get-GkConnectScopeHint -FunctionName $FunctionName)")
        $er = [System.Management.Automation.ErrorRecord]::new(
            $ex, 'GkDelegatedOnly', [System.Management.Automation.ErrorCategory]::PermissionDenied, $FunctionName)
        $PSCmdlet.ThrowTerminatingError($er)
    }

    # Scope capability groups: need at least one scope from each group.
    $granted = @($ctx.Scopes)
    $missingGroups = foreach ($group in $entry.Groups) {
        $has = $false
        foreach ($s in $group.Any) {
            if ($granted -contains $s) { $has = $true; break }
        }
        if (-not $has) { $group }
    }

    if ($missingGroups) {
        $detail = ($missingGroups | ForEach-Object { "to $($_.For): one of [$($_.Any -join ', ')]" }) -join '; '
        $connectHint = Get-GkConnectScopeHint -FunctionName $FunctionName
        $ex = [System.Exception]::new(
            "Missing Graph scope(s) for ${FunctionName}: $detail. Run: Connect-MgGraph -Scopes $connectHint")
        $er = [System.Management.Automation.ErrorRecord]::new(
            $ex, 'GkMissingScope', [System.Management.Automation.ErrorCategory]::PermissionDenied, $FunctionName)
        $PSCmdlet.ThrowTerminatingError($er)
    }

    return $ctx
}

function Get-GkConnectScopeHint {
    <#
    .SYNOPSIS
        Build a comma-joined least-effort scope string for a Connect-MgGraph hint: the first
        (least-privileged, as ordered in the map) scope from each capability group.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [string] $FunctionName
    )
    if (-not $script:GkScopeMap.ContainsKey($FunctionName)) { return 'User.Read.All' }
    $entry = $script:GkScopeMap[$FunctionName]
    $scopes = foreach ($group in $entry.Groups) { @($group.Any)[0] }
    $scopes = @($scopes | Where-Object { $_ } | Select-Object -Unique)
    if (-not $scopes) { return 'User.Read' }
    return ($scopes -join ',')
}