#Requires -RunAsAdministrator [CmdletBinding()] param() $ErrorActionPreference = "Stop" $Target = "C:\MinerOpsAgent" $ConfigPath = Join-Path $Target "agent-config.json" $PendingConfigPath = Join-Path $Target "agent-config.json.pending" $RecoveryConfigPath = Join-Path $Target "agent-config.recovery.json" $EnrollmentPath = Join-Path $PSScriptRoot "enrollment.json" $Installer = Join-Path $PSScriptRoot "install.ps1" $RequiredFiles = @( "install.ps1", "MinerOpsAgentService.ps1", "MinerOpsAgentTray.ps1", "MinerOpsAgentGUI.ps1", "MinerOpsApi3EnrichmentPoller.ps1", "MinerOpsPoolPoller.ps1", "MinerOpsPoolPollerTask.ps1", "EnrichOneMiner.ps1", "MinerOpsControl.ps1", "VERSION", "enrollment.json" ) function Protect-MinerOpsCredentialFile { param( [Parameter(Mandatory = $true)] [string] $Path ) $acl = New-Object System.Security.AccessControl.FileSecurity $acl.SetAccessRuleProtection($true, $false) $identities = @( (New-Object System.Security.Principal.SecurityIdentifier("S-1-5-18")), (New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")), [System.Security.Principal.WindowsIdentity]::GetCurrent().User ) foreach ($identity in $identities) { $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( $identity, [System.Security.AccessControl.FileSystemRights]::FullControl, [System.Security.AccessControl.InheritanceFlags]::None, [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow ) $acl.AddAccessRule($rule) } Set-Acl -LiteralPath $Path -AclObject $acl } function ConvertFrom-MinerOpsSecureString { param( [Parameter(Mandatory = $true)] [Security.SecureString] $SecureValue ) $pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR( $SecureValue ) try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer) } } # Validate the complete package before modifying the Agent host or consuming # the single-use enrollment token. foreach ($name in $RequiredFiles) { $source = Join-Path $PSScriptRoot $name if (!(Test-Path -LiteralPath $source -PathType Leaf)) { throw "Enrollment package is missing $name" } } if (Test-Path -LiteralPath $ConfigPath) { throw @" C:\MinerOpsAgent\agent-config.json already exists. This enrollment bootstrapper is for a new Agent identity and will not overwrite an existing identity. Use the normal MinerOps upgrade procedure instead. "@ } foreach ($recoveryPath in @($PendingConfigPath, $RecoveryConfigPath)) { if (Test-Path -LiteralPath $recoveryPath) { throw @" A previous enrollment recovery file exists at: $recoveryPath Do not create or redeem another enrollment until that recovery state has been reviewed. "@ } } $enrollment = Get-Content -LiteralPath $EnrollmentPath -Raw | ConvertFrom-Json $HubUrl = ([string] $enrollment.hub_url).TrimEnd("/") $EnrollmentToken = ([string] $enrollment.token).Trim() $parsedHub = $null if ( -not [Uri]::TryCreate( $HubUrl, [UriKind]::Absolute, [ref] $parsedHub ) -or $parsedHub.Scheme -notin @("http", "https") ) { throw "enrollment.json contains an invalid MinerOps Hub URL." } if ( $EnrollmentToken.Length -lt 32 -or $EnrollmentToken.Length -gt 256 ) { throw "enrollment.json contains an invalid enrollment token." } Write-Host "" Write-Host "MinerOps Windows Agent enrollment" Write-Host "Hub: $HubUrl" Write-Host "" Write-Host "API3 credentials stay only on this Windows Agent." Write-Host "Leave the account blank if API3 features are not being configured yet." Write-Host "" $Api3Account = (Read-Host "WhatsMiner API3 account").Trim() $Api3PasswordPlain = "" if ($Api3Account) { $Api3PasswordSecure = Read-Host ` "WhatsMiner API3 password" ` -AsSecureString $Api3PasswordPlain = ConvertFrom-MinerOpsSecureString ` -SecureValue $Api3PasswordSecure } $Redeemed = $false $ConfigPersisted = $false try { New-Item -ItemType Directory -Force -Path $Target | Out-Null # Create protected empty files BEFORE token redemption. If persistence is # not possible, fail before consuming the single-use token. [IO.File]::WriteAllText($PendingConfigPath, "") Protect-MinerOpsCredentialFile -Path $PendingConfigPath [IO.File]::WriteAllText($RecoveryConfigPath, "") Protect-MinerOpsCredentialFile -Path $RecoveryConfigPath $requestBody = @{ token = $EnrollmentToken } | ConvertTo-Json -Compress Write-Host "" Write-Host "Redeeming one-time MinerOps enrollment..." $response = Invoke-RestMethod ` -Method Post ` -Uri "$HubUrl/api/agent/enroll" ` -ContentType "application/json" ` -Body $requestBody ` -TimeoutSec 30 $Redeemed = $true if (-not $response.data) { throw "MinerOps enrollment returned no Agent data." } $agent = $response.data foreach ($required in @( "agent_id", "agent_secret", "site_id", "site_name", "agent_zone_id", "agent_zone_name" )) { if (-not ([string] $agent.$required).Trim()) { throw "MinerOps enrollment response is missing $required." } } $ranges = @() foreach ($cidr in @($agent.ip_ranges)) { $network = ([string] $cidr).Trim() if (-not $network) { continue } $ranges += [pscustomobject]@{ name = "$($agent.agent_zone_name) - $network" cidr = $network enabled = $true api_port = 4028 api3_port = 4433 } } $config = [ordered]@{ hub_url = $HubUrl agent_id = [string] $agent.agent_id site_id = [string] $agent.site_id agent_zone_id = [string] $agent.agent_zone_id site_name = [string] $agent.site_name agent_secret = [string] $agent.agent_secret poll_interval_seconds = 120 ip_ranges = @($ranges) api3_account = $Api3Account api3_password = $Api3PasswordPlain api3_port = 4433 } $configJson = $config | ConvertTo-Json -Depth 20 $utf8NoBom = New-Object System.Text.UTF8Encoding($false) # Write a protected recovery copy first. If anything after redemption # fails, the permanent Agent credential is not lost with the consumed token. [IO.File]::WriteAllText( $RecoveryConfigPath, $configJson + [Environment]::NewLine, $utf8NoBom ) [IO.File]::WriteAllText( $PendingConfigPath, $configJson + [Environment]::NewLine, $utf8NoBom ) Move-Item ` -LiteralPath $PendingConfigPath ` -Destination $ConfigPath $ConfigPersisted = $true Remove-Item -LiteralPath $RecoveryConfigPath -Force # The extracted bootstrap token is no longer needed once permanent # credentials have been persisted locally. Remove-Item -LiteralPath $EnrollmentPath -Force $EnrollmentToken = $null $requestBody = $null $Api3PasswordPlain = $null Write-Host "Permanent Agent identity saved locally." Write-Host "Installing MinerOps Agent runtime..." & $Installer $mainTask = Get-ScheduledTask ` -TaskName "MinerOpsAgent" ` -ErrorAction Stop if ([string] $mainTask.State -ne "Running") { Start-ScheduledTask -TaskName "MinerOpsAgent" -ErrorAction Stop } Write-Host "" Write-Host "MinerOps Agent enrollment completed." Write-Host "Agent ID: $($agent.agent_id)" Write-Host "Site: $($agent.site_name)" Write-Host "Agent Zone: $($agent.agent_zone_name)" Write-Host "" Write-Host "Delete the downloaded ZIP and extracted setup folder after verification." } catch { if (-not $Redeemed) { foreach ($temporaryPath in @( $PendingConfigPath, $RecoveryConfigPath )) { if (Test-Path -LiteralPath $temporaryPath) { Remove-Item ` -LiteralPath $temporaryPath ` -Force ` -ErrorAction SilentlyContinue } } } elseif ($ConfigPersisted) { Write-Warning @" The enrollment token was consumed and the permanent Agent identity WAS saved to C:\MinerOpsAgent\agent-config.json. Do not create a replacement enrollment. Correct the installation problem and rerun install.ps1 from this package. "@ } else { Write-Warning @" The enrollment token was consumed. A protected credential recovery file may exist at: C:\MinerOpsAgent\agent-config.recovery.json Do not create or redeem another enrollment until that recovery file has been reviewed. "@ } throw }