#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'