#Requires -Version 5.1 <# Ormur — zero-config PC installer (FAZ4). Barbaros LLC. Casual user, ONE command (run in Windows PowerShell): irm https://ormur.app/install.ps1 | iex What it does, hands-off: 1. Self-elevates (single UAC prompt — needed to install a machine-wide agent). 2. Downloads the release bundle from https://ormur.app and lays it under %ProgramFiles%\Ormur. 3. Registers the 3 existing frontends (sessiond / serve / tsnet) to start at logon, HIDDEN, via the "Ormur" Scheduled Task -> ormur-startup.vbs -> ormur-startup.cmd -> the unchanged run_*.cmd. Starts them now too (no reboot needed). 4. Runs pairing (`agent pair`), shows the QR automatically (a local pair.html opens in the browser AND an ASCII QR + the 6-digit code print in this window). 5. Runs a "Test connection" health check so you see green before you rely on it. Nothing about the machine leaves it: the agent joins your OWN private mesh; the broker sees only control-plane metadata, never terminal content. Tool-agnostic, reboot-durable. Overrides (optional): -ReleaseUrl -InstallDir -NoPair -SkipTest -Uninstall #> [CmdletBinding()] param( [string]$ReleaseUrl = 'https://ormur.app', # base URL of the release assets (holds install.ps1 + ormur-pc.zip) [string]$InstallDir = (Join-Path $env:ProgramFiles 'Ormur'), [switch]$NoPair, [switch]$SkipTest, [switch]$Uninstall ) $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $TaskName = 'Ormur' function Test-Admin { $id = [Security.Principal.WindowsIdentity]::GetCurrent() (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } function Say([string]$m,[string]$c='Cyan'){ Write-Host " $m" -ForegroundColor $c } # env carries settings across the elevation boundary (irm|iex has no script file to re-pass args from) if ($env:ORMUR_RELEASE_URL) { $ReleaseUrl = $env:ORMUR_RELEASE_URL } if ($env:ORMUR_UNINSTALL -eq '1') { $Uninstall = $true } # ---- self-elevate (one UAC) ---- if (-not (Test-Admin)) { Say 'Requesting administrator rights (one-time)…' 'Yellow' $rel = $ReleaseUrl $inner = "`$env:ORMUR_RELEASE_URL='$rel'; " + $(if($Uninstall){"`$env:ORMUR_UNINSTALL='1'; "}else{''}) + "iwr $rel/install.ps1 -UseBasicParsing | iex" Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList @( '-NoProfile','-ExecutionPolicy','Bypass','-NoExit','-Command', $inner ) return } # machine-wide state (buyer): SAME pins used by run_*.cmd / repair.cmd so the paired node == served node. $State = Join-Path $env:ProgramData 'Ormur\state' # who is the interactive human? (the logon task + the terminals must run in THEIR session, not SYSTEM) $Interactive = try { (Get-CimInstance Win32_ComputerSystem).UserName } catch { $null } if (-not $Interactive) { $Interactive = "$env:USERDOMAIN\$env:USERNAME" } # ===================== UNINSTALL ===================== if ($Uninstall) { Say 'Uninstalling Ormur…' 'Yellow' try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue } catch {} cmd /c "taskkill /im agent.exe /f >nul 2>&1" Start-Sleep -Milliseconds 500 if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir -ErrorAction SilentlyContinue } Say 'Removed program files + autostart. Machine state kept at:' 'Green' Say " $State (delete manually for a full wipe)" 'DarkGray' Say 'Done.' 'Green' return } # ===================== INSTALL ===================== Say '' Say 'Ormur — installing your machine-in-your-pocket agent' 'White' Say '----------------------------------------------------' 'DarkGray' # ---- 1. download + stage the bundle ---- $bundleUrl = "$ReleaseUrl/ormur-pc.zip" $zip = Join-Path $env:TEMP 'ormur-pc.zip' Say "Downloading agent bundle…" try { Invoke-WebRequest -Uri $bundleUrl -OutFile $zip -UseBasicParsing } catch { Say "Could not download $bundleUrl" 'Red' Say " $($_.Exception.Message)" 'DarkGray' Say "Check your internet connection and that the release URL is correct, then retry." 'Yellow' return } New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null # stop any running stack so files aren't locked during extract cmd /c "taskkill /im agent.exe /f >nul 2>&1"; Start-Sleep -Milliseconds 400 Say "Installing to $InstallDir" # .NET extractor (overwrites; Expand-Archive on WinPS 5.1 can choke on existing files) Add-Type -AssemblyName System.IO.Compression.FileSystem try { $arch = [IO.Compression.ZipFile]::OpenRead($zip) foreach ($e in $arch.Entries) { if (-not $e.Name) { continue } # directory entry $dest = Join-Path $InstallDir $e.FullName New-Item -ItemType Directory -Force -Path (Split-Path $dest -Parent) | Out-Null [IO.Compression.ZipFileExtensions]::ExtractToFile($e, $dest, $true) } } finally { if ($arch) { $arch.Dispose() } } $Agent = Join-Path $InstallDir 'agent.exe' if (-not (Test-Path $Agent)) { Say 'Bundle did not contain agent.exe — aborting.' 'Red'; return } # ---- 2. machine-wide state dir; let the interactive user write node/pairing state there ---- New-Item -ItemType Directory -Force -Path (Join-Path $State 'tsnet') | Out-Null # BUILTIN\Users = Modify on the state tree (the logon task runs as the user, not SYSTEM). cmd /c "icacls `"$State`" /grant *S-1-5-32-545:(OI)(CI)M /T >nul 2>&1" # carry the broker abuse-gate signup key into the state dir too (agent looks in exe dir + state dir) $sk = Join-Path $InstallDir '.signup_key' if (Test-Path $sk) { Copy-Item $sk (Join-Path $State '.signup_key') -Force -ErrorAction SilentlyContinue } # ---- 3. register hidden logon autostart + start the stack now ---- Say 'Registering autostart (starts at every logon)…' $vbs = Join-Path $InstallDir 'ormur-startup.vbs' if (-not (Test-Path $vbs)) { Say 'ormur-startup.vbs missing from bundle — aborting.' 'Red'; return } $action = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument "`"$vbs`"" $trigger = New-ScheduledTaskTrigger -AtLogOn $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew try { $principal = New-ScheduledTaskPrincipal -UserId $Interactive -LogonType Interactive -RunLevel Limited Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null } catch { # fallback: register for the current interactive user without an explicit principal Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null } # start it NOW so the buyer doesn't have to reboot/relogin first try { Start-ScheduledTask -TaskName $TaskName } catch { Start-Process wscript.exe -ArgumentList "`"$vbs`"" -WindowStyle Hidden } Say 'Stack starting…' Start-Sleep -Seconds 4 # ---- 4. pairing + auto-show QR ---- if (-not $NoPair) { Say 'Generating your pairing code…' $env:ARGUS_BETA_PAIRING_DIR = $State $env:ARGUS_BETA_TSNET_DIR = Join-Path $State 'tsnet' $env:ARGUS_BETA_AGENT_KEY = Join-Path $State 'agent_ed25519' try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch {} $pairRaw = '' try { $pairRaw = (& $Agent pair 2>&1 | Out-String) } catch { $pairRaw = "$_" } $code = if ($pairRaw -match 'PAIR-CODE:\s*(\d{4,8})') { $Matches[1] } else { '' } $payload = if ($pairRaw -match 'QR-PAYLOAD:\s*(\S+)') { $Matches[1] } else { '' } # the agent prints its own scannable ASCII QR ABOVE the "PAIR-CODE:" line — capture it $ascii = '' $idx = $pairRaw.IndexOf('PAIR-CODE:') if ($idx -gt 0) { $ascii = $pairRaw.Substring(0, $idx).Trim("`r","`n") } if ($code) { # render the pairing page from the bundled template and open it in the default browser $tmpl = Join-Path $InstallDir 'pair.html.tmpl' $pubDir = Join-Path $env:PUBLIC 'Ormur'; New-Item -ItemType Directory -Force -Path $pubDir | Out-Null $htmlPath = Join-Path $pubDir 'pair.html' if (Test-Path $tmpl) { # minimal HTML-escape for the ASCII QR block (no external deps) $asciiHtml = ($ascii -replace '&','&' -replace '<','<' -replace '>','>') # literal token replacement (payload/QR text is not regex-safe) $html = (Get-Content $tmpl -Raw) $html = $html.Replace('{{PAIR_CODE}}', $code).Replace('{{QR_PAYLOAD}}', $payload).Replace('{{QR_ASCII}}', $asciiHtml) Set-Content -Path $htmlPath -Value $html -Encoding UTF8 try { Start-Process $htmlPath } catch {} } Say '' Say '====================================================' 'Green' Say ' PAIR YOUR PHONE' 'Green' if ($ascii) { Write-Host $ascii } Say " Code: $code (valid 10 minutes)" 'White' Say ' Open the Ormur app -> scan the QR, or type the code.' 'Gray' Say ' A pairing page also opened in your browser.' 'DarkGray' Say '====================================================' 'Green' } else { Say 'Installed, but a pairing code could not be generated (network/relay hiccup).' 'Yellow' Say "Double-click $InstallDir\repair.cmd to get a fresh 6-digit code." 'Yellow' } } # ---- 5. test connection (pre-payment confidence) ---- if (-not $SkipTest) { Say '' Say 'Running connection test…' 'Cyan' Start-Sleep -Seconds 3 $tc = Join-Path $InstallDir 'test-connection.ps1' if (Test-Path $tc) { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $tc -InstallDir $InstallDir } else { Say 'test-connection.ps1 not in bundle — skipping health check.' 'DarkGray' } } Say '' Say 'Ormur is installed and will start automatically at every logon.' 'Green' Say " Re-pair: $InstallDir\repair.cmd" 'DarkGray' Say " Re-test: powershell -File `"$InstallDir\test-connection.ps1`"" 'DarkGray' Say " Remove: irm $ReleaseUrl/install.ps1 | iex (with -Uninstall)" 'DarkGray'