Public/Complete-WindowsDeviceLinkAssociation.ps1

function Complete-WindowsDeviceLinkAssociation {
    <#
    .SYNOPSIS
    Completes device-side DeviceLink association after tenant preassociation.
 
    .DESCRIPTION
    Performs a guarded native DeviceLink Configure operation on full Windows. The cmdlet requires
    a healthy preflight and a clean local preassociation firmware state (DeviceLinkId plus
    DeviceLinkCreationTimeUtc only), invokes ConfigureDeviceLinkAsync exactly once, and then
    verifies that all four known DeviceLink firmware variables are present and the resulting
    association JWT is structurally/temporally valid and bound to the current device identity.
 
    Progress information is emitted for each major phase. While the native Windows configure
    operation remains in progress, a heartbeat is emitted every 15 seconds with elapsed time.
    This is observational only; no percentage-complete is invented and progress reporting cannot
    trigger retries or alter the native operation.
 
    No retry, firmware reset, cloud deletion or reboot is performed automatically.
    #>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')]
    param(
        [ValidateRange(5,600)]
        [int]$TimeoutSeconds = 180
    )

    $totalTimer=[Diagnostics.Stopwatch]::StartNew()

    Write-Information -InformationAction Continue -MessageData 'Starting device-side DeviceLink association completion...'
    Write-Information -InformationAction Continue -MessageData 'Validating DeviceLink runtime and prerequisites...'

    $support = Test-WindowsDeviceLinkSupport
    if (-not $support.Supported) {
        throw "DeviceLink isn't supported: $($support.Reason)"
    }
    if ($support.Environment -ne 'Windows' -or $support.ActivationMode -ne 'RegisteredWinRT') {
        throw 'DeviceLink association completion currently requires full Windows with the registered DeviceLink WinRT runtime.'
    }

    $preflight = Test-WindowsDeviceLinkPreflight
    if ($preflight.BlockingCount -gt 0) {
        $blocked=@($preflight.Checks|Where-Object State -eq 'Blocked'|ForEach-Object{"$($_.Name): $($_.Summary)"})
        throw "DeviceLink preflight is blocked: $($blocked -join '; ')"
    }
    Write-Information -InformationAction Continue -MessageData "DeviceLink preflight passed. Warnings: $($preflight.WarningCount); blockers: $($preflight.BlockingCount)."

    Write-Information -InformationAction Continue -MessageData 'Inspecting local DeviceLink firmware state...'
    $beforeFirmware=@(Get-WindowsDeviceLinkFirmwareState)
    $beforePresent=@($beforeFirmware|Where-Object Present|Select-Object -ExpandProperty Name)
    $expectedBefore=@('DeviceLinkId','DeviceLinkCreationTimeUtc')

    $alreadyComplete=(@($beforePresent).Count -eq 4 -and
        'DeviceLinkId' -in $beforePresent -and
        'DeviceLinkJwtCompressed' -in $beforePresent -and
        'DeviceLinkJwtLastWrite' -in $beforePresent -and
        'DeviceLinkCreationTimeUtc' -in $beforePresent)

    if ($alreadyComplete) {
        Write-Information -InformationAction Continue -MessageData 'Local DeviceLink firmware state is already complete (4/4). Validating the existing association JWT...'
        $existingJwt=Test-WindowsDeviceLinkAssociationJwt
        $totalTimer.Stop()
        Write-Information -InformationAction Continue -MessageData "Existing DeviceLink association is complete. JWT state: $($existingJwt.State); identity match: $($existingJwt.IdentityMatch)."
        return [pscustomobject]@{
            PSTypeName='Windows.DeviceLink.AssociationCompletion'
            Changed=$false
            AlreadyComplete=$true
            Result='AlreadyComplete'
            ConfigureOperationResult=$null
            ConfigureAsyncStatus=$null
            ConfigureAsyncError=$null
            FirmwareVariablesPresent='4/4'
            JwtState=$existingJwt.State
            IdentityMatch=$existingJwt.IdentityMatch
            DiscoveryDurationSeconds=$null
            ConfigureDurationSeconds=$null
            VerificationDurationSeconds=[math]::Round($totalTimer.Elapsed.TotalSeconds,1)
            TotalDurationSeconds=[math]::Round($totalTimer.Elapsed.TotalSeconds,1)
            RetryCount=0
            CleanupInvoked=$false
            RebootInvoked=$false
        }
    }

    $cleanPreassociation=(@($beforePresent).Count -eq 2 -and
        ($expectedBefore|Where-Object{$_ -notin $beforePresent}).Count -eq 0)
    if (-not $cleanPreassociation) {
        throw "Refusing DeviceLink association completion because local firmware state is not the expected clean preassociation state. Present: $($beforePresent -join ', ')."
    }
    Write-Information -InformationAction Continue -MessageData 'Local DeviceLink preassociation state verified (2/4).'

    Write-Information -InformationAction Continue -MessageData 'Obtaining the current local DeviceLink identity...'
    $identity = Get-WindowsDeviceLink -TimeoutSeconds $TimeoutSeconds
    $target = if($identity.LinkId){$identity.LinkId}else{'current device'}
    if(-not $PSCmdlet.ShouldProcess($target,'Complete DeviceLink association using native ConfigureDeviceLinkAsync')){
        return
    }

    $progressCallback=[Action[string]]{
        param([string]$message)
        Write-Information -InformationAction Continue -MessageData $message
    }

    $native=[WinPEDeviceLink.Native.DeviceLinkManagerClient]::ConfigureRegistered(
        $identity.DeviceLink,
        $TimeoutSeconds,
        $progressCallback
    )

    $verificationTimer=[Diagnostics.Stopwatch]::StartNew()
    Write-Information -InformationAction Continue -MessageData 'Verifying DeviceLink firmware state after native configuration...'
    $afterFirmware=@(Get-WindowsDeviceLinkFirmwareState)
    $afterPresent=@($afterFirmware|Where-Object Present|Select-Object -ExpandProperty Name)
    $firmwareComplete=(@($afterPresent).Count -eq 4 -and
        'DeviceLinkId' -in $afterPresent -and
        'DeviceLinkJwtCompressed' -in $afterPresent -and
        'DeviceLinkJwtLastWrite' -in $afterPresent -and
        'DeviceLinkCreationTimeUtc' -in $afterPresent)
    Write-Information -InformationAction Continue -MessageData "DeviceLink firmware variables present: $(@($afterPresent).Count)/4."

    Write-Information -InformationAction Continue -MessageData 'Validating the resulting DeviceLink association JWT...'
    $jwt=Test-WindowsDeviceLinkAssociationJwt
    $jwtValid=($jwt.State -eq 'Valid' -and $jwt.Present -and $jwt.IdentityMatch -ne $false)
    Write-Information -InformationAction Continue -MessageData "Association JWT validation completed. State: $($jwt.State); identity match: $($jwt.IdentityMatch); signature validation: $($jwt.SignatureValidation)."

    $nativeSucceeded=($native.ConfigureAsyncStatus -eq 1 -and $native.ConfigureAsyncError -eq 0 -and $native.ConfigureOperationResult -eq 1)
    $verificationTimer.Stop()
    $totalTimer.Stop()

    if(-not $nativeSucceeded -or -not $firmwareComplete -or -not $jwtValid){
        $hr='0x{0:X8}' -f ([BitConverter]::ToUInt32([BitConverter]::GetBytes([int]$native.ConfigureAsyncError),0))
        throw "DeviceLink Configure completed without the expected verified post-state. NativeSuccess=$nativeSucceeded; AsyncStatus=$($native.ConfigureAsyncStatus); AsyncError=$hr; OperationResult=$($native.ConfigureOperationResult); FirmwareComplete=$firmwareComplete; JwtState=$($jwt.State); IdentityMatch=$($jwt.IdentityMatch). No cleanup or retry was performed."
    }

    Write-Information -InformationAction Continue -MessageData "DeviceLink association completion verified successfully in $([math]::Round($totalTimer.Elapsed.TotalSeconds,1)) seconds."

    $errorHex='0x{0:X8}' -f ([BitConverter]::ToUInt32([BitConverter]::GetBytes([int]$native.ConfigureAsyncError),0))
    [pscustomobject]@{
        PSTypeName='Windows.DeviceLink.AssociationCompletion'
        Changed=$true
        AlreadyComplete=$false
        Result='AssociatedLocally'
        LinkId=$identity.LinkId
        TenantId=$native.TenantId
        DiscoveryResult=$native.DiscoveryResult
        DiscoveryUrl=$native.DiscoveryUrl
        ConfigureResultBefore=$native.ConfigureResultBefore
        ConfigureResultAfter=$native.ConfigureResultAfter
        ConfigureOperationResult=$native.ConfigureOperationResult
        ConfigureAsyncStatus=$native.ConfigureAsyncStatus
        ConfigureAsyncError=$errorHex
        FirmwareVariablesPresent='4/4'
        JwtState=$jwt.State
        IdentityMatch=$jwt.IdentityMatch
        SignatureValidation=$jwt.SignatureValidation
        DiscoveryDurationSeconds=[math]::Round([double]$native.DiscoveryDurationSeconds,1)
        ConfigureDurationSeconds=[math]::Round([double]$native.ConfigureDurationSeconds,1)
        VerificationDurationSeconds=[math]::Round($verificationTimer.Elapsed.TotalSeconds,1)
        TotalDurationSeconds=[math]::Round($totalTimer.Elapsed.TotalSeconds,1)
        RetryCount=0
        CleanupInvoked=$false
        RebootInvoked=$false
    }
}