make(Kernel::class)->bootstrap(); portalQaAssertTarget($target); $directory = null; // Laravel's CLI exception renderer may otherwise leave an unsuccessful harness at exit 0. // Keep the last verified proof intact and invalidate only the QA readiness receipt. set_exception_handler(static function (Throwable $error) use (&$directory, $target): void { if (is_string($directory) && is_file($directory.'/command-failure.json')) { $readiness = config('portal_profiles.proof_directory').'/readiness.json'; if (is_file($readiness)) { copy($readiness, $directory.'/previous-readiness.json'); } $failure = ['verified_at' => gmdate('c'), 'target' => $target, 'restore_verified' => false, 'files_verified' => false, 'qa_passed' => false, 'compatibility_verified' => false, 'active_writes' => 0, 'reason' => 'Latest QA backup/restore command failed; previous dated verified backups remain historical.', 'diagnostic' => $directory.'/command-failure.json']; file_put_contents($readiness, json_encode($failure, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); file_put_contents(storage_path('app/private/investmoz-v2/portal-audit/profile-installation-backup-failure-qa.json'), json_encode($failure, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); } try { $pdo = DB::connection()->getPdo(); if ($pdo->inTransaction()) { $pdo->rollBack(); } } catch (Throwable) { // Preserve the original failure, without connection strings or exception bodies. } fwrite(STDERR, 'QA backup/restore not verified; diagnostic retained privately. Active environment untouched.'.PHP_EOL); exit(1); }); $code = app(PortalProfileInstallation::class)->fingerprint(); $tests = json_decode(file_get_contents(storage_path('app/private/investmoz-v2/portal-audit/profile-installation-checks-qa.json')), true, 512, JSON_THROW_ON_ERROR); throw_unless(($tests['result'] ?? null) === 'passed' && ($tests['code_hash'] ?? null) === $code, RuntimeException::class, 'Current installation QA checks required.'); $c = DB::connection()->getConfig(); throw_unless(in_array($c['url'] ?? null, [null, ''], true) && str_ends_with($c['database'], '_test') && (int) $c['port'] === 55441 && $c['host'] === '127.0.0.1', RuntimeException::class, 'QA connection refused.'); $source = DB::connection()->getPdo(); $source->exec('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'); $restoreName = 'profile_restore_'.gmdate('Ymd_His').'_test'; $directory = storage_path('app/private/investmoz-v2/portal-profile-installation/'.$restoreName); mkdir($directory, 0700, true); $dump = $directory.'/database.dump'; $run = function (array $args, string $password) use ($directory): void { $process = new Process($args, null, ['PGPASSWORD' => $password]); $process->setTimeout(300); $process->run(); if (! $process->isSuccessful()) { // Keep diagnostic output private; never expose connection secrets or dump content. $stderr = $process->getErrorOutput(); if ($password !== '') { $stderr = str_replace($password, '[REDACTED]', $stderr); } file_put_contents($directory.'/command-failure.json', json_encode([ 'tool' => basename($args[0]), 'exit_code' => $process->getExitCode(), 'stderr' => $stderr, 'target_port' => 55441, 'active_access' => false, ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); throw new RuntimeException('QA backup/restore command failed; diagnostic retained privately.'); } }; $bin = 'C:/Program Files/PostgreSQL/17/bin/'; $snapshot = $source->query('SELECT pg_export_snapshot()')->fetchColumn(); $run([$bin.'pg_dump.exe', '-h', $c['host'], '-p', '55441', '-U', $c['username'], '-d', $c['database'], '-Fc', '--snapshot='.$snapshot, '-f', $dump], $c['password'] ?? ''); $maintenance = new PDO('pgsql:host=127.0.0.1;port=55441;dbname=postgres', 'postgres', '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); throw_unless((int) $maintenance->query('SELECT inet_server_port()')->fetchColumn() === 55441 && preg_match('/^profile_restore_\d{8}_\d{6}_test$/', $restoreName), RuntimeException::class, 'Restore target refused.'); $maintenance->exec('CREATE DATABASE "'.$restoreName.'"'); $run([$bin.'pg_restore.exe', '-h', '127.0.0.1', '-p', '55441', '-U', 'postgres', '-d', $restoreName, '--exit-on-error', '--no-owner', '--no-privileges', $dump], ''); $restored = new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$restoreName, 'postgres', '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); $identity = $restored->query('SELECT current_database() AS database,inet_server_port() AS port')->fetch(PDO::FETCH_ASSOC); throw_unless($identity['database'] === $restoreName && (int) $identity['port'] === 55441, RuntimeException::class, 'Actual restore identity refused.'); $restored->exec('BEGIN READ ONLY'); $tableProof = []; foreach ($source->query("SELECT schemaname,tablename FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema') ORDER BY schemaname,tablename")->fetchAll(PDO::FETCH_ASSOC) as $table) { $name = '"'.str_replace('"', '""', $table['schemaname']).'"."'.str_replace('"', '""', $table['tablename']).'"'; $sql = 'SELECT row_to_json(t)::text FROM '.$name.' t ORDER BY row_to_json(t)::text'; $a = $source->query($sql)->fetchAll(PDO::FETCH_COLUMN); $b = $restored->query($sql)->fetchAll(PDO::FETCH_COLUMN); throw_unless($a === $b, RuntimeException::class, 'Restored table contents differ.'); $tableProof[$name] = ['count' => count($a), 'sha256' => hash('sha256', implode("\n", $a))]; } $documents = $source->query('SELECT storage_key FROM documents WHERE storage_key IS NOT NULL')->fetchAll(PDO::FETCH_COLUMN); $source->exec('ROLLBACK'); $restored->exec('ROLLBACK'); $files = $directory.'/private-files.zip'; $zip = new ZipArchive; throw_unless($zip->open($files, ZipArchive::CREATE | ZipArchive::EXCL) === true, RuntimeException::class, 'Private archive unavailable.'); $root = realpath(storage_path('app/private')); $manifest = []; $missing = []; foreach (array_unique($documents) as $relative) { $path = realpath(storage_path('app/private/'.$relative)); if ($path === false) { $missing[] = $relative; continue; } throw_unless(str_starts_with(strtolower($path), strtolower($root.DIRECTORY_SEPARATOR)), RuntimeException::class, 'Document outside private storage.'); $name = 'private/'.str_replace('\\', '/', $relative); $zip->addFile($path, $name); $manifest[$name] = hash_file('sha256', $path); } // Preserve the QA runtime/key, never the active .env. Archive remains private. $runtime = base_path('tools/portal-qa-environment.php'); $zip->addFile($runtime, 'runtime/portal-qa-environment.php'); $manifest['runtime/portal-qa-environment.php'] = hash_file('sha256', $runtime); $zip->close(); $read = new ZipArchive; $read->open($files); $restoreFiles = $directory.'/files-restored'; mkdir($restoreFiles, 0700, true); $restoreRoot = realpath($restoreFiles); throw_unless(str_starts_with(strtolower($restoreRoot), strtolower(realpath(storage_path('app/private/investmoz-v2/portal-profile-installation')).DIRECTORY_SEPARATOR)), RuntimeException::class, 'File restore root refused.'); foreach ($manifest as $name => $hash) { throw_unless(! str_contains($name, ':') && ! str_starts_with($name, '/') && ! array_intersect(explode('/', $name), ['.', '..']), RuntimeException::class, 'Archive member path refused.'); $bytes = $read->getFromName($name); throw_unless(is_string($bytes) && hash('sha256', $bytes) === $hash, RuntimeException::class, 'Archived bytes differ.'); $destination = $restoreRoot.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $name); if (! is_dir(dirname($destination))) { mkdir(dirname($destination), 0700, true); } throw_unless(str_starts_with(strtolower(realpath(dirname($destination))), strtolower($restoreRoot.DIRECTORY_SEPARATOR)), RuntimeException::class, 'Resolved restore destination refused.'); file_put_contents($destination, $bytes); throw_unless(hash_file('sha256', $destination) === $hash, RuntimeException::class, 'Restored file differs.'); } $read->close(); $proof = ['verified_at' => gmdate('c'), 'target' => $target, 'restored_target' => $identity, 'code_hash' => $code, 'restore_verified' => true, 'files_verified' => $missing === [], 'files_restore_verified' => true, 'files_restore_root' => $restoreRoot, 'compatibility_verified' => true, 'qa_passed' => true, 'dump_path' => $dump, 'dump_sha256' => hash_file('sha256', $dump), 'files_path' => $files, 'files_sha256' => hash_file('sha256', $files), 'tables' => $tableProof, 'files' => $manifest, 'missing_document_files' => $missing, 'active_writes' => 0, 'scope' => 'QA schema/data restore and archived byte recovery; not active backup, visual acceptance or recovery of lost MFA.', 'checks' => $tests]; file_put_contents($directory.'/proof.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); file_put_contents(config('portal_profiles.proof_directory').'/readiness.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode(['target' => $target, 'restored_target' => $identity, 'restore_verified' => true, 'tables' => count($tableProof), 'files' => count($manifest), 'missing_files' => count($missing), 'ready_files' => $missing === [], 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL;