Public/Initialize-WindowsDeviceLink.ps1

function Initialize-WindowsDeviceLink {
    <#
    .SYNOPSIS
    Safely ensures that the local DeviceLink has a tenant-side preassociation and can optionally complete device-side association.
 
    .DESCRIPTION
    Orchestrates existing WindowsDeviceLink operations without performing destructive repair.
    The cmdlet obtains the local DeviceLink, checks the tenant-side Device Association, classifies
    health, and creates a preassociation only when the validated state is LocalOnly.
 
    By default, existing preassociated or associated records are left unchanged. Specify
    -Associate to opt in to association after the state has been verified as
    Preassociated.
 
    Direct mode uses the selected Microsoft Graph authentication method. Backend mode uses
    the WindowsDeviceLink Azure Function App for authoritative multitenant cloud lookup and
    pre-association, while native association still runs locally on the device.
 
    Backend mode never performs an implicit tenant move. If the current association is found
    in a different tenant than -TargetTenantId, initialization is blocked and an explicit
    tenant-move workflow is required. Completion uses the guarded Complete-WindowsDeviceLinkAssociation cmdlet and
    therefore performs at most one ConfigureDeviceLinkAsync call, with no retry, reset, cleanup,
    cloud deletion, or reboot.
 
    Unexpected, incomplete, unsupported, or unknown states are blocked rather than repaired
    automatically.
 
    For DeviceCode authentication, one token is acquired at the start and reused for lookup,
    registration, and verification so one initialization run requires only one device-code sign-in.
 
    This cmdlet never resets firmware state, removes an association, or reboots the device.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Direct', SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Direct')]
        [ValidateSet('DeviceCode','Interactive','ClientSecret','AccessToken','Certificate','CertificateThumbprint','CertificateSubjectName','EnvironmentVariable','ManagedIdentity')]
        [string]$Method,
        [Parameter(ParameterSetName = 'Direct')][ValidateNotNullOrEmpty()][string]$TenantId,
        [Parameter(ParameterSetName = 'Direct')][ValidateNotNullOrEmpty()][string]$ClientId,
        [Parameter(ParameterSetName = 'Direct')][securestring]$AccessToken,
        [Parameter(ParameterSetName = 'Direct')][System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
        [Parameter(ParameterSetName = 'Direct')][ValidateNotNullOrEmpty()][string]$CertificateThumbprint,
        [Parameter(ParameterSetName = 'Direct')][ValidateNotNullOrEmpty()][string]$CertificateSubjectName,
        [Parameter(ParameterSetName = 'Direct')][bool]$SendCertificateChain = $false,
        [Parameter(ParameterSetName = 'Direct')][securestring]$ClientSecret,

        [Parameter(Mandatory, ParameterSetName = 'Backend')]
        [ValidateNotNull()]
        [uri]$BackendUri,

        [Parameter(Mandatory, ParameterSetName = 'Backend')]
        [ValidateNotNullOrEmpty()]
        [string]$BackendApiKey,

        [Parameter(Mandatory, ParameterSetName = 'Backend')]
        [guid]$TargetTenantId,
        [Parameter(ParameterSetName = 'Direct')][ValidateNotNullOrEmpty()][string]$Environment = 'Global',
        [Parameter(ParameterSetName = 'Direct')][ValidateRange(1, 600)][double]$ClientTimeout = 100,
        [ValidateNotNullOrEmpty()][string]$WindowsManagementServicePath,
        [ValidateRange(5, 600)][int]$TimeoutSeconds = 120,
        [switch]$Associate
    )

    if ($PSCmdlet.ParameterSetName -eq 'Backend') {
        $targetTenant = $TargetTenantId.ToString().ToLowerInvariant()
        $statusParameters = @{
            BackendUri = $BackendUri
            BackendApiKey = $BackendApiKey
            TimeoutSeconds = $TimeoutSeconds
        }
        if ($PSBoundParameters.ContainsKey('WindowsManagementServicePath')) {
            $statusParameters.WindowsManagementServicePath = $WindowsManagementServicePath
        }

        $beforeStatus = Get-WindowsDeviceLinkBackendStatus @statusParameters
        $beforeHealth = $beforeStatus | Test-WindowsDeviceLinkHealth

        if ($beforeStatus.AssociationPresent -and
            -not [string]::IsNullOrWhiteSpace([string]$beforeStatus.TenantId) -and
            ([string]$beforeStatus.TenantId).Trim().ToLowerInvariant() -ne $targetTenant) {

            return [pscustomobject]@{
                PSTypeName='Windows.DeviceLink.InitializationResult'
                Action='Blocked'
                Changed=$false
                AssociationRequested=[bool]$Associate
                AssociationResult=$null
                AssociationDetails=$null
                BeforeState='AssociationInDifferentTenant'
                AfterState='AssociationInDifferentTenant'
                Severity='Warning'
                Message="Initialization was blocked because the Device Association exists in another tenant. Use an explicit tenant-move workflow before targeting tenant '$targetTenant'."
                SerialNumber=$beforeStatus.SerialNumber
                LinkId=$beforeStatus.LinkId
                AssociationId=$beforeStatus.AssociationId
                AssociationState=$beforeStatus.AssociationState
                FirmwareVariables=$beforeStatus.FirmwareVariablesPresent
                RegistrationResult=$null
                BeforeStatus=$beforeStatus
                AfterStatus=$beforeStatus
            }
        }

        $action = Resolve-WindowsDeviceLinkInitializationAction -HealthState $beforeHealth.State
        $changed = $false
        $registration = $null
        $completion = $null
        $associationResult = $null
        $afterStatus = $beforeStatus
        $afterHealth = $beforeHealth
        $message = $beforeHealth.Summary

        switch ($action) {
            'None' {
                if ($beforeHealth.State -eq 'Preassociated') {
                    $message = 'The DeviceLink is already preassociated in the requested target tenant. No cloud change was made.'
                }
                else {
                    $message = 'The DeviceLink is already associated in the requested target tenant. No cloud change was made.'
                }
            }
            'Register' {
                $deviceLinkParameters = @{ TimeoutSeconds = $TimeoutSeconds }
                if ($PSBoundParameters.ContainsKey('WindowsManagementServicePath')) {
                    $deviceLinkParameters.WindowsManagementServicePath = $WindowsManagementServicePath
                }
                $deviceLink = Get-WindowsDeviceLink @deviceLinkParameters

                if ($PSCmdlet.ShouldProcess($deviceLink.SerialNumber, "Create the missing DeviceLink pre-association in target tenant $targetTenant through the Azure Function backend")) {
                    $preassociateUri = Resolve-WindowsDeviceLinkBackendEndpoint -BackendUri $BackendUri -Route preassociate
                    $registration = Invoke-WindowsDeviceLinkWebhook -InputObject $deviceLink -WebhookUri $preassociateUri -WebhookApiKey $BackendApiKey -TenantId $targetTenant
                    $changed = $true

                    $afterStatus = Get-WindowsDeviceLinkBackendStatus @statusParameters
                    $afterHealth = $afterStatus | Test-WindowsDeviceLinkHealth
                    if ($afterHealth.State -notin @('Preassociated','Associated')) {
                        throw "Function pre-association returned, but verification did not reach Preassociated or Associated. Verified state: $($afterHealth.State)."
                    }
                    if (([string]$afterStatus.TenantId).Trim().ToLowerInvariant() -ne $targetTenant) {
                        throw 'Function pre-association verification returned an unexpected target tenant.'
                    }
                    $message = "DeviceLink pre-association completed through the Azure Function backend and was verified as $($afterHealth.State)."
                }
                else {
                    $message = 'The DeviceLink is locally valid and not associated; Function-backed pre-association would be performed.'
                    if ($Associate) { $associationResult = 'WouldPreassociateAndComplete' }
                }
            }
            default {
                $message = "Initialization was blocked because the current health state is '$($beforeHealth.State)'. $($beforeHealth.RecommendedAction)"
            }
        }

        if ($Associate -and $afterHealth.State -eq 'Preassociated') {
            $target = if ($afterStatus.SerialNumber) { $afterStatus.SerialNumber } else { 'DeviceLink' }
            if ($PSCmdlet.ShouldProcess($target, 'Associate this device with the tenant')) {
                $completeParameters = @{ TimeoutSeconds = $TimeoutSeconds; Confirm = $false }
                if ($PSBoundParameters.ContainsKey('WindowsManagementServicePath')) {
                    $completeParameters.WindowsManagementServicePath = $WindowsManagementServicePath
                }

                $completion = Complete-WindowsDeviceLinkAssociation @completeParameters
                $associationResult = [string]$completion.Result
                if ($completion.Changed) { $changed = $true }

                $afterStatus = Get-WindowsDeviceLinkBackendStatus @statusParameters
                $afterHealth = $afterStatus | Test-WindowsDeviceLinkHealth
                if ($afterHealth.State -ne 'Associated') {
                    throw "DeviceLink association returned, but Function-backed verification did not reach Associated. Verified state: $($afterHealth.State)."
                }
                if (([string]$afterStatus.TenantId).Trim().ToLowerInvariant() -ne $targetTenant) {
                    throw 'Association verification returned an unexpected target tenant.'
                }
                $message = 'DeviceLink association succeeded and the final Function-backed cloud state was verified as Associated.'
            }
            elseif (-not $associationResult) {
                $associationResult = 'WouldComplete'
                $message = 'The DeviceLink is preassociated; association would be performed.'
            }
        }
        elseif ($Associate -and $afterHealth.State -eq 'Associated') {
            $associationResult = 'AlreadyAssociated'
            $message = 'The DeviceLink is already associated in the requested target tenant. No association change was required.'
        }

        return [pscustomobject]@{
            PSTypeName='Windows.DeviceLink.InitializationResult'
            Action=$action
            Changed=$changed
            AssociationRequested=[bool]$Associate
            AssociationResult=$associationResult
            AssociationDetails=$completion
            BeforeState=$beforeHealth.State
            AfterState=$afterHealth.State
            Severity=$afterHealth.Severity
            Message=$message
            SerialNumber=$beforeStatus.SerialNumber
            LinkId=$beforeStatus.LinkId
            AssociationId=$afterStatus.AssociationId
            AssociationState=$afterStatus.AssociationState
            FirmwareVariables=$afterStatus.FirmwareVariablesPresent
            RegistrationResult=$registration
            BeforeStatus=$beforeStatus
            AfterStatus=$afterStatus
        }
    }

    $effectiveAccessToken = $null
    try {
        $effectiveMethod = $Method
        if ($Method -eq 'DeviceCode') {
            if ($Environment -ne 'Global') { throw 'Native DeviceCode initialization currently supports the Global Microsoft cloud only.' }
            $tokenParameters = @{}
            if ($TenantId) { $tokenParameters.TenantId = $TenantId }
            if ($ClientId) { $tokenParameters.ClientId = $ClientId }
            Write-Information -InformationAction Continue -MessageData 'Using native OAuth device-code authentication for DeviceLink initialization.'
            $token = Get-WindowsDeviceLinkDeviceCodeToken @tokenParameters
            $effectiveAccessToken = ConvertTo-SecureString $token.AccessToken -AsPlainText -Force
            $TenantId = $token.TenantId
            $token = $null
            $effectiveMethod = 'AccessToken'
        }

        $commonOnline = @{ Method = $effectiveMethod; Environment = $Environment; ClientTimeout = $ClientTimeout }
        if ($effectiveAccessToken) {
            $commonOnline.TenantId = $TenantId
            $commonOnline.AccessToken = $effectiveAccessToken
        }
        else {
            foreach ($name in @('TenantId','ClientId','AccessToken','Certificate','CertificateThumbprint','CertificateSubjectName','SendCertificateChain','ClientSecret')) {
                if ($PSBoundParameters.ContainsKey($name)) { $commonOnline[$name] = $PSBoundParameters[$name] }
            }
        }

        $statusParameters = @{ Online = $true; Method = $effectiveMethod; Environment = $Environment; ClientTimeout = $ClientTimeout; TimeoutSeconds = $TimeoutSeconds }
        foreach ($key in $commonOnline.Keys) {
            if ($key -notin @('Method','Environment','ClientTimeout')) { $statusParameters[$key] = $commonOnline[$key] }
        }
        if ($PSBoundParameters.ContainsKey('WindowsManagementServicePath')) { $statusParameters.WindowsManagementServicePath = $WindowsManagementServicePath }

        $beforeStatus = Get-WindowsDeviceLinkStatus @statusParameters
        $beforeHealth = $beforeStatus | Test-WindowsDeviceLinkHealth
        $action = Resolve-WindowsDeviceLinkInitializationAction -HealthState $beforeHealth.State
        $changed = $false
        $registration = $null
        $completion = $null
        $associationResult = $null
        $afterStatus = $beforeStatus
        $afterHealth = $beforeHealth
        $message = $beforeHealth.Summary

        switch ($action) {
            'None' {
                if ($beforeHealth.State -eq 'Preassociated') {
                    $message = 'The DeviceLink is already preassociated. No change was made.'
                }
                else {
                    $message = 'The DeviceLink is already associated. No change was made.'
                }
            }
            'Register' {
                $deviceLinkParameters = @{ TimeoutSeconds = $TimeoutSeconds }
                if ($PSBoundParameters.ContainsKey('WindowsManagementServicePath')) { $deviceLinkParameters.WindowsManagementServicePath = $WindowsManagementServicePath }
                $deviceLink = Get-WindowsDeviceLink @deviceLinkParameters

                if ($PSCmdlet.ShouldProcess($deviceLink.SerialNumber, 'Create the missing tenant-side DeviceLink preassociation')) {
                    $registerParameters = @{}
                    foreach ($key in $commonOnline.Keys) { $registerParameters[$key] = $commonOnline[$key] }
                    $registerParameters.InputObject = $deviceLink
                    $registerParameters.Confirm = $false
                    $registration = Register-WindowsDeviceLink @registerParameters
                    $changed = $true
                    $afterStatus = Get-WindowsDeviceLinkStatus @statusParameters
                    $afterHealth = $afterStatus | Test-WindowsDeviceLinkHealth
                    if ($afterHealth.State -notin @('Preassociated','Associated')) {
                        throw "DeviceLink registration returned, but verification did not reach Preassociated or Associated. Verified state: $($afterHealth.State)."
                    }
                    $message = "DeviceLink registration completed and was verified as $($afterHealth.State)."
                }
                else {
                    $message = 'The DeviceLink is locally valid and not associated; registration would be performed.'
                }
            }
            default {
                $message = "Initialization was blocked because the current health state is '$($beforeHealth.State)'. $($beforeHealth.RecommendedAction)"
            }
        }

        if ($Associate -and $afterHealth.State -eq 'Preassociated') {
            $target = if ($afterStatus.SerialNumber) { $afterStatus.SerialNumber } else { 'DeviceLink' }
            if ($PSCmdlet.ShouldProcess($target, 'Complete the tenant DeviceLink association on this device')) {
                $completeParameters = @{ TimeoutSeconds = $TimeoutSeconds; Confirm = $false }
                if ($PSBoundParameters.ContainsKey('WindowsManagementServicePath')) {
                    $completeParameters.WindowsManagementServicePath = $WindowsManagementServicePath
                }
                $completion = Complete-WindowsDeviceLinkAssociation @completeParameters
                $associationResult = [string]$completion.Result
                if ($completion.Changed) { $changed = $true }

                $afterStatus = Get-WindowsDeviceLinkStatus @statusParameters
                $afterHealth = $afterStatus | Test-WindowsDeviceLinkHealth
                if ($afterHealth.State -ne 'Associated') {
                    throw "DeviceLink completion returned, but verification did not reach Associated. Verified state: $($afterHealth.State)."
                }
                $message = 'DeviceLink association succeeded and the final state was verified as Associated.'
            }
            else {
                $associationResult = 'WouldComplete'
                $message = 'The DeviceLink is preassociated; device-side association would be performed.'
            }
        }
        elseif ($Associate -and $afterHealth.State -eq 'Associated') {
            if ($completion) {
                $associationResult = [string]$completion.Result
                $message = 'DeviceLink association succeeded and the final state was verified as Associated.'
            }
            else {
                $associationResult = 'AlreadyAssociated'
                $message = 'The DeviceLink is already associated. No completion change was required.'
            }
        }

        [pscustomobject]@{
            PSTypeName='Windows.DeviceLink.InitializationResult'
            Action=$action
            Changed=$changed
            AssociationRequested=[bool]$Associate
            AssociationResult=$associationResult
            AssociationDetails=$completion
            BeforeState=$beforeHealth.State
            AfterState=$afterHealth.State
            Severity=$afterHealth.Severity
            Message=$message
            SerialNumber=$beforeStatus.SerialNumber
            LinkId=$beforeStatus.LinkId
            AssociationId=if ($afterStatus.AssociationId) { $afterStatus.AssociationId } elseif ($registration) { $registration.Id } else { $null }
            AssociationState=$afterStatus.AssociationState
            FirmwareVariables=$afterStatus.FirmwareVariablesPresent
            RegistrationResult=$registration
            BeforeStatus=$beforeStatus
            AfterStatus=$afterStatus
        }
    }
    finally {
        $effectiveAccessToken = $null
    }
}