[CmdletBinding()] param( [Parameter(Mandatory = $false, HelpMessage = "Show help.")] [switch]$Help ) if ($Help -or $args -contains '-help') { Write-Host "bbx install" -ForegroundColor Green Write-Host "Install BrowserBox (binary distribution)" -ForegroundColor Yellow Write-Host "Usage: bbx install" -ForegroundColor Cyan Write-Host "Options:" -ForegroundColor Cyan Write-Host " -Help Show this help message" -ForegroundColor White Write-Host "" Write-Host "Environment overrides:" -ForegroundColor Cyan Write-Host " BBX_RELEASE_REPO, BBX_RELEASE_TAG, BBX_RELEASE_ASSET_DIR, GH_TOKEN/GITHUB_TOKEN, BBX_NO_UPDATE" -ForegroundColor White Write-Host " BBX_INSTALL_HOSTNAME, BBX_INSTALL_EMAIL (preferred for full install)" -ForegroundColor White Write-Host " BBX_HOSTNAME, BBX_EMAIL, EMAIL (legacy full-install aliases)" -ForegroundColor White $global:LASTEXITCODE = 0 return } $ErrorActionPreference = "Stop" $PublicRepo = "BrowserBox/BrowserBox" $ReleaseRepo = if ($env:BBX_RELEASE_REPO) { $env:BBX_RELEASE_REPO } else { $PublicRepo } $Token = if ($env:GH_TOKEN) { $env:GH_TOKEN } elseif ($env:GITHUB_TOKEN) { $env:GITHUB_TOKEN } else { "" } $NoUpdate = $false if ($null -ne $env:BBX_NO_UPDATE -and $env:BBX_NO_UPDATE -ne "") { try { $NoUpdate = [System.Convert]::ToBoolean($env:BBX_NO_UPDATE) } catch { $NoUpdate = ($env:BBX_NO_UPDATE.ToLowerInvariant() -in @("1", "true", "yes", "y", "on")) } } function Get-FirstNonEmpty { param([string[]]$Values) foreach ($value in $Values) { if (-not [string]::IsNullOrWhiteSpace($value)) { return $value } } return $null } $InstallHostname = Get-FirstNonEmpty @($env:BBX_INSTALL_HOSTNAME, $env:BBX_HOSTNAME) $InstallEmail = Get-FirstNonEmpty @($env:BBX_INSTALL_EMAIL, $env:BBX_EMAIL, $env:EMAIL) if ($InstallHostname) { $env:BBX_INSTALL_HOSTNAME = $InstallHostname $env:BBX_HOSTNAME = $InstallHostname } if ($InstallEmail) { $env:BBX_INSTALL_EMAIL = $InstallEmail $env:BBX_EMAIL = $InstallEmail $env:EMAIL = $InstallEmail } $BinaryDir = "$env:LOCALAPPDATA\browserbox\bin" $BinaryName = "browserbox.exe" $RemoteAssetName = "browserbox-win-x64.exe" $BinaryPath = Join-Path $BinaryDir $BinaryName $PreseededReleaseAssetDir = if ($env:BBX_RELEASE_ASSET_DIR) { $env:BBX_RELEASE_ASSET_DIR } else { "" } function Ensure-BinaryDir { if (-not (Test-Path $BinaryDir)) { New-Item -ItemType Directory -Path $BinaryDir -Force | Out-Null } } function Get-LatestRelease { param([string]$Repo) if ($NoUpdate) { if ($env:BBX_RELEASE_TAG) { return $env:BBX_RELEASE_TAG } return $null } $headers = @{} if ($Token) { $headers["Authorization"] = "Bearer $Token" } try { $apiUrl = "https://api.github.com/repos/$Repo/releases/latest" $response = Invoke-RestMethod -Uri $apiUrl -TimeoutSec 10 -Headers $headers -ErrorAction Stop return $response.tag_name } catch { try { Write-Host "Latest release lookup failed (check for drafts), checking release list..." -ForegroundColor Gray $apiUrl = "https://api.github.com/repos/$Repo/releases?per_page=1" $response = Invoke-RestMethod -Uri $apiUrl -TimeoutSec 10 -Headers $headers -ErrorAction Stop if ($response -and $response.Count -gt 0) { return $response[0].tag_name } } catch { Write-Error "Failed to fetch latest release from $Repo : $_" exit 1 } } return $null } function Get-ReleaseByTag { param( [Parameter(Mandatory = $true)] [string]$Repo, [Parameter(Mandatory = $true)] [string]$Tag, [hashtable]$Headers = @{}, [int]$PerPage = 100, [int]$MaxPages = 20 ) try { return Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/tags/$Tag" -Headers $Headers -ErrorAction Stop } catch { Write-Host "Direct tag lookup failed (common for drafts), scanning release list pages..." -ForegroundColor Gray for ($page = 1; $page -le $MaxPages; $page++) { $uri = "https://api.github.com/repos/$Repo/releases?per_page=$PerPage&page=$page" $releases = @(Invoke-RestMethod -Uri $uri -Headers $Headers -ErrorAction Stop) if (-not $releases -or $releases.Count -eq 0) { break } $release = $releases | Where-Object { $_.tag_name -eq $Tag } | Select-Object -First 1 if ($release) { Write-Host "Resolved release $Tag on page $page." -ForegroundColor Gray return $release } if ($releases.Count -lt $PerPage) { break } } } $ghCmd = Get-Command gh -ErrorAction SilentlyContinue if ($ghCmd) { try { Write-Host "Release list scan missed $Tag, trying gh release view fallback..." -ForegroundColor Gray $releaseJson = & $ghCmd.Source release view $Tag --repo $Repo --json tagName,isDraft,isPrerelease,assets,id 2>&1 if ($LASTEXITCODE -eq 0 -and $releaseJson) { $release = $releaseJson | ConvertFrom-Json if ($release -and $release.tagName -eq $Tag) { Write-Host "Resolved release $Tag via gh release view." -ForegroundColor Gray return $release } } elseif ($LASTEXITCODE -ne 0) { Write-Host "gh release view fallback exited ${LASTEXITCODE}: $releaseJson" -ForegroundColor Gray } } catch { Write-Host "gh release view fallback failed for $Tag." -ForegroundColor Gray } } return $null } function Download-Binary { param( [string]$Tag ) Ensure-BinaryDir $tempFile = "$BinaryPath.tmp" Write-Host "Downloading BrowserBox $Tag for Windows..." -ForegroundColor Cyan $headers = @{} if ($Token) { $headers["Authorization"] = "Bearer $Token" } $useAssetApi = $Token -or ($ReleaseRepo -ne $PublicRepo) if ($ReleaseRepo -ne $PublicRepo -and -not $Token) { Write-Error "GH_TOKEN/GITHUB_TOKEN is required to download from private/internal repo $ReleaseRepo." exit 1 } try { if ($PreseededReleaseAssetDir) { Write-Host "Using preseeded release assets from $PreseededReleaseAssetDir" -ForegroundColor Gray $preseededBinary = Join-Path $PreseededReleaseAssetDir $RemoteAssetName if (-not (Test-Path $preseededBinary)) { $preseededBinary = Join-Path $PreseededReleaseAssetDir $BinaryName } if (-not (Test-Path $preseededBinary)) { Write-Error "Preseeded release assets do not contain $RemoteAssetName or $BinaryName." exit 1 } Copy-Item -Path $preseededBinary -Destination $tempFile -Force } elseif ($useAssetApi) { if ($Tag) { $release = Get-ReleaseByTag -Repo $ReleaseRepo -Tag $Tag -Headers $headers } else { $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/$ReleaseRepo/releases" -Headers $headers -ErrorAction Stop if (-not $releases -or $releases.Count -eq 0) { Write-Error "No releases found in $ReleaseRepo" exit 1 } $release = $releases[0] $Tag = $release.tag_name } $asset = $release.assets | Where-Object { $_.name -eq $RemoteAssetName } | Select-Object -First 1 if (-not $asset) { Write-Error "Release $Tag not found in $ReleaseRepo" exit 1 } $assetUrl = "https://api.github.com/repos/$ReleaseRepo/releases/assets/$($asset.id)" Invoke-WebRequest -Uri $assetUrl -Headers @{ Authorization = "Bearer $Token"; Accept = "application/octet-stream" } -OutFile $tempFile -ErrorAction Stop } else { $downloadUrl = if ($Tag) { "https://github.com/$ReleaseRepo/releases/download/$Tag/$RemoteAssetName" } else { "https://github.com/$ReleaseRepo/releases/latest/download/$RemoteAssetName" } Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile -ErrorAction Stop } if (Test-Path $BinaryPath) { Remove-Item $BinaryPath -Force } Move-Item $tempFile $BinaryPath -Force } catch { Write-Error "Failed to download binary: $_" exit 1 } } $IntegrityPublicKeyPem = @' -----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnqKI++Z5x+cHF1je6Ww9 r3hNRuefjZzlJGPD56IQTbVIDXZT45uGNHelg+BjlZezdGH86y29zKgx2g3pt8cC Yp8KMSgg69uo9EVFlDw8HQ1Sf7rciiU89neb48lkm5GfzXtAyIFWQj83AHDblQUq UJoXuu7YQLskHiRa0YPOkPf5KUHS8Yv1OJwXldsmd/+NGCrZki1o6xEt55B5qo3J 89jUiVnSafUhZXuQiwYfRT5MVoBBFl6TK/kg3qTF4oVBvz0r4HO/C1uAEytaDEI4 CFy2XO6i64DgSbkjzXCsomlHU0ywPbLxXPUst5AZwX62f/caGKGZs7IrZDBYNI2k bBZ5fCAFhExwI0HUVIFC31YFpFRZB3UnVQdE0q8UuZyCstubPk7gdkEljnCXDnMB bvgk5+5y8WgCrbu3mndlbb4K9NqxFq3tJppM8Gq8Rip94DghUBlRMXCBwaZ+EsBZ ZwkpTdoWvsJcO+NwHscRvHNRcDRUrDwMrTpSs/cfCRMUo0ze0ZxpenCQuQpae7ei Rs4+aW0rrwZBFo+o5GNWDOADAoD4JEPBNuSJyOw4mjdTgf8O9pIJfDF7HtX7pHr7 e8u3jamSWvZSZA+50fI6iL05JUDA4cQ529voRTxiLALgLkSnlGY2EQrDr9A8lH4/ hYdYq1pXWapoaFZTuPK4ln8CAwEAAQ== -----END PUBLIC KEY----- '@ function Convert-HexToBytes { param([string]$Hex) $Hex = $Hex.Trim() if ($Hex.Length % 2 -ne 0) { throw "Invalid hex string length." } $bytes = New-Object byte[] ($Hex.Length / 2) for ($i = 0; $i -lt $Hex.Length; $i += 2) { $bytes[$i / 2] = [Convert]::ToByte($Hex.Substring($i, 2), 16) } return $bytes } function Read-AsnLength { param([System.IO.BinaryReader]$Reader) $length = $Reader.ReadByte() if ($length -lt 0x80) { return $length } $byteCount = $length -band 0x7F $length = 0 for ($i = 0; $i -lt $byteCount; $i++) { $length = ($length -shl 8) -bor $Reader.ReadByte() } return $length } function Read-AsnSequence { param([System.IO.BinaryReader]$Reader) $tag = $Reader.ReadByte() if ($tag -ne 0x30) { throw "Invalid ASN.1 sequence tag: $tag" } return Read-AsnLength -Reader $Reader } function Read-AsnIntegerBytes { param([System.IO.BinaryReader]$Reader) $tag = $Reader.ReadByte() if ($tag -ne 0x02) { throw "Invalid ASN.1 integer tag: $tag" } $length = Read-AsnLength -Reader $Reader $bytes = $Reader.ReadBytes($length) if ($bytes.Length -gt 0 -and $bytes[0] -eq 0x00) { return $bytes[1..($bytes.Length - 1)] } return $bytes } function Get-RsaPublicKeyFromPem { param([string]$Pem) $base64 = $Pem -replace '-----BEGIN PUBLIC KEY-----','' -replace '-----END PUBLIC KEY-----','' -replace '\s','' $der = [Convert]::FromBase64String($base64) $ms = New-Object System.IO.MemoryStream(,$der) $reader = New-Object System.IO.BinaryReader($ms) $null = Read-AsnSequence -Reader $reader $algLen = Read-AsnSequence -Reader $reader $null = $reader.ReadBytes($algLen) $bitTag = $reader.ReadByte() if ($bitTag -ne 0x03) { throw "Invalid ASN.1 bit string tag: $bitTag" } $bitLen = Read-AsnLength -Reader $reader $null = $reader.ReadByte() $bitBytes = $reader.ReadBytes($bitLen - 1) $inner = New-Object System.IO.BinaryReader((New-Object System.IO.MemoryStream(,$bitBytes))) $null = Read-AsnSequence -Reader $inner $modulus = Read-AsnIntegerBytes -Reader $inner $exponent = Read-AsnIntegerBytes -Reader $inner $params = New-Object System.Security.Cryptography.RSAParameters $params.Modulus = $modulus $params.Exponent = $exponent $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider $rsa.ImportParameters($params) return $rsa } function Verify-ManifestSignature { param( [string]$ManifestPath, [string]$SignaturePath ) if (-not (Test-Path $ManifestPath)) { throw "Manifest not found at $ManifestPath" } if (-not (Test-Path $SignaturePath)) { throw "Signature not found at $SignaturePath" } $manifestBytes = [System.IO.File]::ReadAllBytes($ManifestPath) $domain = [System.Text.Encoding]::UTF8.GetBytes("INTEGRITY/RELEASE_MANIFEST/v1`0") $payload = New-Object byte[] ($domain.Length + $manifestBytes.Length) [System.Array]::Copy($domain, 0, $payload, 0, $domain.Length) [System.Array]::Copy($manifestBytes, 0, $payload, $domain.Length, $manifestBytes.Length) $sigHex = (Get-Content $SignaturePath -Raw).Trim() if (-not $sigHex) { throw "Signature file is empty." } $sigBytes = Convert-HexToBytes -Hex $sigHex $rsa = [System.Security.Cryptography.RSA]::Create() if ($rsa -and ($rsa | Get-Member -Name ImportFromPem -MemberType Method)) { $rsa.ImportFromPem($IntegrityPublicKeyPem) } else { $rsa = Get-RsaPublicKeyFromPem -Pem $IntegrityPublicKeyPem } $ok = $rsa.VerifyData($payload, $sigBytes, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) if (-not $ok) { throw "Release manifest signature verification failed." } } function Get-FileSha256 { param([string]$Path) if (-not (Test-Path $Path)) { throw "File not found: $Path" } return (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLowerInvariant() } $tag = if ($env:BBX_RELEASE_TAG) { $env:BBX_RELEASE_TAG } else { Get-LatestRelease -Repo $ReleaseRepo } if (-not $tag) { Write-Error "Could not determine release tag." exit 1 } Download-Binary -Tag $tag # Try global location first (C:\ProgramData), fallback to user dir $globalDir = Join-Path $env:PROGRAMDATA "dosaygo\bbpro" $userConfigDir = "$env:USERPROFILE\.config\dosaygo\bbpro" # Check if we can write to ProgramData (admin privileges) $useGlobal = $false try { if (-not (Test-Path $globalDir)) { New-Item -ItemType Directory -Path $globalDir -Force -ErrorAction Stop | Out-Null } $useGlobal = $true } catch { # No admin access, use user config dir if (-not (Test-Path $userConfigDir)) { New-Item -ItemType Directory -Path $userConfigDir -Force | Out-Null } } $targetDir = if ($useGlobal) { $globalDir } else { $userConfigDir } $locationDesc = if ($useGlobal) { "global location" } else { "user config" } $tempBase = $env:TEMP if (-not $tempBase) { $tempBase = "C:\Windows\Temp" } $tempDir = Join-Path $tempBase "bbx-installer" New-Item -ItemType Directory -Path $tempDir -Force | Out-Null $manifestPath = Join-Path $tempDir "release.manifest.json" $sigPath = Join-Path $tempDir "release.manifest.json.sig" Write-Host "Downloading release manifest to $locationDesc..." -ForegroundColor Yellow try { $publicRepo = if ($PublicRepo) { $PublicRepo } else { "BrowserBox/BrowserBox" } if ($PreseededReleaseAssetDir) { $preseededManifest = Join-Path $PreseededReleaseAssetDir "release.manifest.json" $preseededSig = Join-Path $PreseededReleaseAssetDir "release.manifest.json.sig" if (-not (Test-Path $preseededManifest) -or -not (Test-Path $preseededSig)) { throw "Preseeded release assets are missing release.manifest.json or release.manifest.json.sig." } Copy-Item $preseededManifest $manifestPath -Force Copy-Item $preseededSig $sigPath -Force } elseif ($Token -or $ReleaseRepo -ne $publicRepo) { if (-not $Token) { throw "GH_TOKEN/GITHUB_TOKEN is required to download manifests from $ReleaseRepo." } $headers = @{ Authorization = "Bearer $Token" } $release = Get-ReleaseByTag -Repo $ReleaseRepo -Tag $tag -Headers $headers $manifestAsset = $release.assets | Where-Object { $_.name -eq "release.manifest.json" } | Select-Object -First 1 $sigAsset = $release.assets | Where-Object { $_.name -eq "release.manifest.json.sig" } | Select-Object -First 1 if (-not $manifestAsset -or -not $sigAsset) { throw "Manifest assets not found on release $tag." } Invoke-WebRequest -Uri "https://api.github.com/repos/$ReleaseRepo/releases/assets/$($manifestAsset.id)" -Headers @{ Authorization = "Bearer $Token"; Accept = "application/octet-stream" } -OutFile $manifestPath -ErrorAction Stop Invoke-WebRequest -Uri "https://api.github.com/repos/$ReleaseRepo/releases/assets/$($sigAsset.id)" -Headers @{ Authorization = "Bearer $Token"; Accept = "application/octet-stream" } -OutFile $sigPath -ErrorAction Stop } else { $manifestUrl = "https://github.com/$ReleaseRepo/releases/download/$tag/release.manifest.json" $sigUrl = "https://github.com/$ReleaseRepo/releases/download/$tag/release.manifest.json.sig" Invoke-WebRequest -Uri $manifestUrl -OutFile $manifestPath -ErrorAction Stop Invoke-WebRequest -Uri $sigUrl -OutFile $sigPath -ErrorAction Stop } Verify-ManifestSignature -ManifestPath $manifestPath -SignaturePath $sigPath $manifestJson = Get-Content $manifestPath -Raw | ConvertFrom-Json $artifactKey = "win32-x64" $entry = $manifestJson.artifacts.$artifactKey if (-not $entry) { throw "Manifest missing entry for $artifactKey." } if (-not $entry.sha256) { throw "Manifest missing sha256 for $artifactKey." } $actualSha = Get-FileSha256 -Path $BinaryPath if ($actualSha -ne $entry.sha256.ToLowerInvariant()) { throw "SHA-256 mismatch for downloaded binary." } Copy-Item $manifestPath (Join-Path $targetDir "release.manifest.json") -Force Copy-Item $sigPath (Join-Path $targetDir "release.manifest.json.sig") -Force } catch { Write-Error "Failed to download or verify release manifest: $_" exit 1 } # Ensure cp_commands_only.ps1 copies the binary we just downloaded. $env:BBX_BINARY_SOURCE_PATH = $BinaryPath $existingBrowserBox = Get-Command browserbox -ErrorAction SilentlyContinue $forceFullInstall = $env:BBX_FULL_INSTALL -and $env:BBX_FULL_INSTALL -ne "" -and $env:BBX_FULL_INSTALL -ne "0" -and $env:BBX_FULL_INSTALL.ToLowerInvariant() -ne "false" $installArgs = @("--install") if ($forceFullInstall -or -not $existingBrowserBox) { $installArgs = @("--full-install") if ($InstallHostname -and $InstallEmail) { $installArgs += @($InstallHostname, $InstallEmail) } } if ($forceFullInstall) { Write-Host "BBX_FULL_INSTALL requested. Running full setup (--full-install)." -ForegroundColor Yellow } elseif ($existingBrowserBox) { Write-Host "BrowserBox detected in PATH. Running update setup (--install)." -ForegroundColor Yellow } else { Write-Host "BrowserBox not found in PATH. Running full setup (--full-install)." -ForegroundColor Yellow } Write-Host "Running BrowserBox installer: $BinaryPath $($installArgs -join ' ')" -ForegroundColor Yellow & $BinaryPath @installArgs $installExit = $LASTEXITCODE if ($installExit -ne 0) { Write-Error "BrowserBox installer exited with code $installExit" exit $installExit } Write-Host "Installation complete. Type 'bbx' to get started. Get a license key at https://browserbox.io and install your license key with bbx certify." -ForegroundColor Green exit 0