# install.ps1 - one-line uploy installer for Windows (PowerShell 5.1+). # # irm https://uploy.app/releases/install.ps1 | iex # # or for a specific version / offline: # .\install.ps1 -Version 0.1.0 # .\install.ps1 -Manifest C:\path\install-manifest-windows-x64.json -Archive C:\path\uploy-0.1.0-windows-x64.zip # # or, the single-executable (SEA) build: # .\install.ps1 -Mode exe # # What it does: # 1. Detects arch (x64 / arm64). # 2. Requires Node.js (the portable launcher + the verifier need it). # 3. Online: fetches the signed install manifest + the archive + the # verifier (install-verify.mjs, pinned by sha256). Offline # (-Manifest + -Archive): uses local files, still verifies the # signature against the pinned public key - no network. # 4. Verifies sha256(archive) === manifest.sha256 AND the Ed25519 # signature over the manifest against the pinned release public key. # 5. Extracts to the install prefix (default: $env:LOCALAPPDATA\uploy). # 6. Prints the PATH update + the autostart command. # # Trust root: the pinned public key below (injected at release time). # A dev build leaves it empty - the script refuses to install (fail-closed). param( [string]$Mode = 'portable', [string]$Version = '', [string]$Manifest = '', [string]$Archive = '', [string]$Prefix = '', [string]$ReleaseUrl = '' ) # Disable the IE-engine progress bar (slow / can hang in PS 5.1). $ProgressPreference = 'SilentlyContinue' $ErrorActionPreference = 'Stop' if ($Mode -ne 'portable' -and $Mode -ne 'exe') { Write-Error "install.ps1: -Mode must be 'portable' or 'exe' (got '$Mode')." exit 1 } # SEA manifest name suffix (release pipeline emits # install-manifest-sea-windows-.json for the single-executable build). $ManifestSuffix = '' if ($Mode -eq 'exe') { $ManifestSuffix = '-sea' } if ($ReleaseUrl -eq '') { $ReleaseUrl = if ($env:UPLOY_RELEASE_URL) { $env:UPLOY_RELEASE_URL } else { 'https://uploy.app/releases' } } # --- Pinned trust anchors (injected at release build time) ------------- # The release pipeline (build-portable.mjs) replaces these empty literals # with the real base64 Ed25519 public key + the sha256 of install-verify.mjs. # Empty = dev build - refuse to install. $InstallPubkey = 'UhbNgXAB5VySmOR5KYixhiopiF98Apbh+cmQAktBMhE=' $VerifySha256 = '840ef2928c92ed4f5509bb8845979fb3950c8c77e5ec84b319013052c708ab7f' # ----------------------------------------------------------------------- if ($InstallPubkey -eq '') { Write-Error 'install.ps1: this is a dev build - the release public key is not injected. Refusing to install (fail-closed). Build via build-portable.mjs to inject it.' exit 2 } # --- Node is required --------------------------------------------------- $nodeCmd = Get-Command node -ErrorAction SilentlyContinue if ($null -eq $nodeCmd) { Write-Error 'install.ps1: Node.js is required (>= 18). Install it first: https://nodejs.org/' exit 3 } $nodeMajor = 0 try { $nodeMajor = [int]((node -v) -replace '^v', '').Split('.')[0] } catch { $nodeMajor = 0 } if ($nodeMajor -lt 18) { Write-Error "install.ps1: Node >= 18 required (found $(node -v))." exit 3 } # --- Platform detection ------------------------------------------------- $arch = 'x64' $pa = $env:PROCESSOR_ARCHITECTURE if ($pa -eq 'ARM64') { $arch = 'arm64' } elseif ($pa -eq 'AMD64') { $arch = 'x64' } $Platform = "windows-$arch" # --- Install prefix ----------------------------------------------------- if ($Prefix -eq '') { $Prefix = Join-Path $env:LOCALAPPDATA 'uploy' } # --- Temp working dir --------------------------------------------------- $Work = Join-Path ([System.IO.Path]::GetTempPath()) ("uploy-install-" + [System.Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $Work -Force | Out-Null $Cleanup = $true # --- Acquire manifest + archive + verifier ------------------------------ $Online = $true if ($Manifest -ne '' -and $Archive -ne '') { $Online = $false Write-Output 'install.ps1: offline mode - verifying local files (no network).' } elseif ($Manifest -ne '' -or $Archive -ne '') { Write-Error 'install.ps1: -Manifest and -Archive must be given together for offline mode.' exit 1 } if ($Online) { if ($Version -ne '') { $ManifestUrl = "$ReleaseUrl/v$Version/install-manifest$ManifestSuffix-$Platform.json" } else { $ManifestUrl = "$ReleaseUrl/latest/install-manifest$ManifestSuffix-$Platform.json" } $Manifest = Join-Path $Work 'install-manifest.json' Write-Output "install.ps1: fetching manifest $ManifestUrl" Invoke-WebRequest -Uri $ManifestUrl -OutFile $Manifest $man = Get-Content $Manifest -Raw | ConvertFrom-Json if ($null -eq $man.url) { Write-Error 'install.ps1: manifest has no archive url.' exit 6 } $ArchiveUrl = $man.url $ArchiveName = Split-Path $ArchiveUrl -Leaf $Archive = Join-Path $Work $ArchiveName Write-Output "install.ps1: fetching archive $ArchiveUrl" Invoke-WebRequest -Uri $ArchiveUrl -OutFile $Archive $InstallVerify = Join-Path $Work 'install-verify.mjs' Invoke-WebRequest -Uri "$ReleaseUrl/install-verify.mjs" -OutFile $InstallVerify $ActualHash = (Get-FileHash -Algorithm SHA256 -Path $InstallVerify).Hash.ToLower() if ($ActualHash -ne $VerifySha256.ToLower()) { Write-Error "install.ps1: verifier sha256 mismatch - downloaded verifier is not the pinned one.`nExpected: $VerifySha256`nGot: $ActualHash" exit 7 } } else { # Offline: the verifier sits next to the manifest. $cand = Join-Path (Split-Path $Manifest -Parent) 'install-verify.mjs' if (Test-Path $cand) { $InstallVerify = $cand } else { Write-Error "install.ps1: offline mode needs install-verify.mjs next to the manifest ($cand)." exit 7 } } # --- Verify the archive ------------------------------------------------- Write-Output 'install.ps1: verifying signature + sha256...' & node $InstallVerify --manifest $Manifest --archive $Archive --pubkey $InstallPubkey if ($LASTEXITCODE -ne 0) { Write-Error "install.ps1: verification failed (exit $LASTEXITCODE)." exit $LASTEXITCODE } # --- Extract ------------------------------------------------------------ if (-not (Test-Path $Prefix)) { New-Item -ItemType Directory -Path $Prefix -Force | Out-Null } Write-Output "install.ps1: extracting to $Prefix" if ($Archive -match '\.zip$') { Expand-Archive -Path $Archive -DestinationPath $Prefix -Force } else { # tar.gz / tgz - Windows 10+ ships tar.exe. & tar -xzf $Archive -C $Prefix if ($LASTEXITCODE -ne 0) { Write-Error "install.ps1: tar extraction failed."; exit 8 } } if ($Mode -eq 'exe') { # SEA layout: $Prefix\uploy\uploy.exe + $Prefix\uploy\uploy-native\ $BinDir = Join-Path $Prefix 'uploy' $UployExe = Join-Path $BinDir 'uploy.exe' if (-not (Test-Path $UployExe)) { Write-Error "install.ps1: extracted archive but $UployExe not found (unexpected exe layout)." exit 8 } $SidecarDir = Join-Path $BinDir 'uploy-native' if (-not (Test-Path $SidecarDir)) { Write-Output "install.ps1: warning - $SidecarDir not found; native addons (isolated-vm/argon2/keyring) will run degraded." } } else { $BinDir = Join-Path $Prefix 'uploy\bin' $UployExe = Join-Path $BinDir 'uploy.cmd' if (-not (Test-Path $UployExe)) { Write-Error "install.ps1: extracted archive but $UployExe not found (unexpected layout)." exit 8 } } # --- PATH guidance ------------------------------------------------------ $UserPath = [Environment]::GetEnvironmentVariable('Path', 'User') if ($UserPath -notlike "*$BinDir*") { [Environment]::SetEnvironmentVariable('Path', "$BinDir;$UserPath", 'User') Write-Output "install.ps1: added $BinDir to the User PATH (open a new terminal to use 'uploy')." } else { Write-Output "install.ps1: $BinDir already on the User PATH." } Write-Output '' Write-Output "install.ps1: start the daemon with: uploy start" Write-Output "install.ps1: survive reboots with: uploy autostart install" Write-Output "install.ps1: dashboard at: http://localhost:8765" # Cleanup Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue