# Foxora Studio recovery; run as the affected desktop user, not another admin. # Default is read-only. No project/task database, pack, or OS credential is deleted. [CmdletBinding()] param( [ValidateSet('Check','SignIn','View')][string]$Mode = 'Check', [string]$DataHome = $(if ($env:FOXORA_APP_DATA) { $env:FOXORA_APP_DATA } else { Join-Path $env:USERPROFILE '.foxora' }) ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest if ($env:OS -ne 'Windows_NT') { throw 'Use repair.sh on macOS/Linux.' } if (-not [IO.Path]::IsPathRooted($DataHome)) { throw 'DataHome must be absolute.' } if (-not (Test-Path -LiteralPath $DataHome -PathType Container)) { Write-Host 'No existing Foxora data. Start Studio normally.'; exit 0 } $DataHome = (Get-Item -LiteralPath $DataHome).FullName.TrimEnd('\') if ($DataHome -eq $env:USERPROFILE -or $DataHome -eq [IO.Path]::GetPathRoot($DataHome).TrimEnd('\')) { throw 'Unsafe data home.' } function Assert-PlainPath([string]$Path) { if ((Test-Path -LiteralPath $Path) -and ((Get-Item -Force -LiteralPath $Path).Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Recovery target is a junction/symlink. No reset performed.' } } Assert-PlainPath $DataHome $view = Join-Path $env:LOCALAPPDATA 'ai.foxora.desktop' Write-Host 'Foxora recovery check (Windows)' foreach ($item in @('studio.db','device-session.json','auth-storage','workspaces','browser/packs')) { if (Test-Path -LiteralPath (Join-Path $DataHome $item)) { Write-Host "Present: $item" } } if (Test-Path -LiteralPath $view) { Write-Host 'Present: app webview profile' } Write-Host 'Fox names are generated for new tasks. This tool cannot patch compiled OAuth handling or a server authorization bug.' if ($Mode -eq 'Check') { exit 0 } $running = @(Get-Process -Name 'foxora-desktop','foxora-engine','Foxora Studio' -ErrorAction SilentlyContinue) if ($running.Count -gt 0) { throw 'Quit Studio from its tray and wait for its engine to exit, then retry. No process was stopped.' } $recovery = Join-Path $DataHome 'recovery' Assert-PlainPath $recovery Assert-PlainPath (Join-Path $DataHome 'auth-storage') Assert-PlainPath (Join-Path $DataHome 'device-session.json') if ($Mode -eq 'View') { Assert-PlainPath $view if ($DataHome.StartsWith($view.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase) -or $DataHome -eq $view) { throw 'Data home must not be inside the webview profile.' } } [void][IO.Directory]::CreateDirectory($recovery) $lock = [IO.File]::Open((Join-Path $recovery '.repair-lock'),[IO.FileMode]::CreateNew,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None) try { $backup = Join-Path $recovery ('repair-' + [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssZ') + '-' + [Guid]::NewGuid().ToString('N')) [void][IO.Directory]::CreateDirectory($backup) # Restrict backup to this user; it can contain opaque login/session state. $acl = New-Object Security.AccessControl.DirectorySecurity $acl.SetAccessRuleProtection($true, $false) $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User $rule = New-Object Security.AccessControl.FileSystemAccessRule($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow') $acl.AddAccessRule($rule) Set-Acl -LiteralPath $backup -AclObject $acl Write-Host "Private recovery backup: $backup" $manifest = New-Object 'System.Collections.Generic.List[object]' $utf8 = New-Object Text.UTF8Encoding($false) function Move-Aside([string]$Source,[string]$Label) { Assert-PlainPath $Source if (Test-Path -LiteralPath $Source) { $target = Join-Path $backup $Label $manifest.Add([pscustomobject]@{Source=$Source;Backup=$target}) [IO.File]::WriteAllText((Join-Path $backup 'moves.json'), (ConvertTo-Json -InputObject @($manifest.ToArray()) -Depth 3), $utf8) Move-Item -LiteralPath $Source -Destination $target } } function Sha256([string]$Text) { $sha = [Security.Cryptography.SHA256]::Create() try { return ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text)))).Replace('-','').ToLowerInvariant() } finally { $sha.Dispose() } } if ($Mode -eq 'SignIn') { # Rust Path::canonicalize uses the verbatim Windows path from this API. Add-Type -TypeDefinition @' using System; using System.Text; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; public static class FoxoraRecoveryPath { [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern SafeFileHandle CreateFile(string p, uint a, uint s, IntPtr sa, uint c, uint f, IntPtr t); [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern uint GetFinalPathNameByHandle(SafeFileHandle h, StringBuilder b, uint n, uint f); public static string Canonical(string p) { using (var h=CreateFile(p,0,7,IntPtr.Zero,3,0x02000000,IntPtr.Zero)) { if(h.IsInvalid) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); var b=new StringBuilder(32768); var n=GetFinalPathNameByHandle(h,b,(uint)b.Capacity,0); if(n==0 || n>=b.Capacity) throw new InvalidOperationException("Could not resolve data home"); return b.ToString(); } } } '@ $canonical = [FoxoraRecoveryPath]::Canonical($DataHome) $profile = 'ai.foxora.desktop:' + (Sha256 $canonical) + ':production:lcskvopauvdhjxpjdhra' $authDir = Join-Path $DataHome 'auth-storage' [void][IO.Directory]::CreateDirectory($authDir) $marker = Join-Path $authDir ((Sha256 $profile) + '.generation') Move-Aside $marker 'auth-generation' [IO.File]::WriteAllText((Join-Path $backup 'new-marker.txt'),$marker,$utf8) $pending = $marker + '.' + [Guid]::NewGuid().ToString('N') + '.pending' $stream = [IO.File]::Open($pending,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None) try { $bytes = $utf8.GetBytes([Guid]::NewGuid().ToString()); $stream.Write($bytes,0,$bytes.Length) } finally { $stream.Dispose() } Move-Item -LiteralPath $pending -Destination $marker Move-Aside (Join-Path $DataHome 'device-session.json') 'device-session.json' } else { Move-Aside $view 'webview' } [IO.File]::WriteAllText((Join-Path $backup 'PRIVATE.txt'),'Private local backup. Never upload or send it to support.',$utf8) Write-Host 'Recovery prepared. Reopen Studio; close old login tabs and start ONE new sign-in.' Write-Host 'Projects, tasks, browser/model packs and old OS credentials were preserved.' Write-Host 'If PKCE recurs, report its exact text; the compiled app may require an update.' Write-Host 'Undo: quit Studio, preserve any newly created target state separately, then move backup entries to Source paths in moves.json. Remove only the new marker named in new-marker.txt before restoring auth-generation, if present.' } finally { $lock.Dispose(); Remove-Item -LiteralPath (Join-Path $recovery '.repair-lock') }