bootstrap($app); (new LoadConfiguration)->bootstrap($app); $c = config('database.connections.'.config('database.default')); throw_unless($c['driver'] === 'pgsql' && empty($c['url']) && (int) $c['port'] === 5432 && $c['database'] === 'investemoz', RuntimeException::class, 'Explicit active source required; no provider bootstrap.'); $source = new PDO('pgsql:host='.$c['host'].';port='.$c['port'].';dbname='.$c['database'], $c['username'], $c['password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); $source->exec('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'); $identity = $source->query("SELECT current_database() AS database,inet_server_port() AS port,current_setting('transaction_read_only') AS read_only")->fetch(PDO::FETCH_ASSOC); throw_unless($identity['database'] === 'investemoz' && (int) $identity['port'] === 5432 && $identity['read_only'] === 'on', RuntimeException::class, 'Active source identity refused.'); $hash = (new OrganizationCountryInstallation)->fingerprint(); $stamp = gmdate('Ymd_His'); $clone = 'country_candidate_'.$stamp.'_test'; $dir = $root.'/storage/app/private/investmoz-v2/organization-country-installation/'.$clone; mkdir($dir, 0700, true); $bin = 'C:/Program Files/PostgreSQL/17/bin/'; $run = static function (array $args, string $password) use ($dir): void { $p = new Process($args, null, ['PGPASSWORD' => $password]); $p->setTimeout(300); $p->run(); if (! $p->isSuccessful()) { file_put_contents($dir.'/command-failure.json', json_encode(['tool' => basename($args[0]), 'exit' => $p->getExitCode(), 'diagnostic' => str_replace($password ?: '__NO_PASSWORD__', '[REDACTED]', $p->getErrorOutput())])); throw new RuntimeException('Backup/restore failed; private diagnostic retained.'); } }; $snapshot = $source->query('SELECT pg_export_snapshot()')->fetchColumn(); $dump = $dir.'/database.dump'; $run([$bin.'pg_dump.exe', '-h', $c['host'], '-p', '5432', '-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('/^country_candidate_\d{8}_\d{6}_test$/', $clone), RuntimeException::class, 'Restore target refused.'); $maintenance->exec('CREATE DATABASE "'.$clone.'"'); $run([$bin.'pg_restore.exe', '-h', '127.0.0.1', '-p', '55441', '-U', 'postgres', '-d', $clone, '--exit-on-error', '--no-owner', '--no-privileges', $dump], ''); $restored = new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$clone, 'postgres', '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); $restored->exec('BEGIN READ ONLY'); $tables = []; 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 $t) { $name = '"'.str_replace('"', '""', $t['schemaname']).'"."'.str_replace('"', '""', $t['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 differs.'); $tables[$name] = ['count' => count($a), 'sha256' => hash('sha256', implode("\n", $a))]; } $sequenceSql = "SELECT * FROM pg_sequences WHERE schemaname NOT IN ('pg_catalog','information_schema') ORDER BY schemaname,sequencename"; $sequences = $source->query($sequenceSql)->fetchAll(PDO::FETCH_ASSOC); throw_unless($sequences === $restored->query($sequenceSql)->fetchAll(PDO::FETCH_ASSOC), RuntimeException::class, 'Restored sequences differ.'); $originalMfaIds = $source->query('SELECT id FROM users WHERE two_factor_secret IS NOT NULL ORDER BY id')->fetchAll(PDO::FETCH_COLUMN); $documents = $source->query('SELECT DISTINCT storage_key FROM documents WHERE storage_key IS NOT NULL')->fetchAll(PDO::FETCH_COLUMN); $installationState = ['migrations' => $source->query("SELECT migration FROM migrations WHERE migration IN ('2026_10_09_030000_extend_opportunity_resources','2026_10_10_040000_create_portal_profile_authorization','2026_10_09_190000_prepare_investor_path','2026_10_10_050000_extend_organization_country') ORDER BY migration")->fetchAll(PDO::FETCH_COLUMN), 'country_columns' => $source->query("SELECT column_name FROM information_schema.columns WHERE table_schema='public' AND table_name='organizations' AND column_name IN ('incorporation_country_code','incorporation_country_provenance') ORDER BY column_name")->fetchAll(PDO::FETCH_COLUMN), 'organization_count' => (int) $source->query('SELECT count(*) FROM organizations')->fetchColumn(), 'membership_count' => (int) $source->query('SELECT count(*) FROM memberships WHERE deleted_at IS NULL')->fetchColumn(), 'country_flag' => (bool) config('portal.organization_country_enabled')]; $eligible = ['verified_managers' => 0, 'verified_managers_with_mfa' => 0]; $managerEmails = array_map('strtolower', config('portal.managers', [])); foreach ($source->query('SELECT email,email_verified_at,two_factor_secret,two_factor_confirmed_at FROM users')->fetchAll(PDO::FETCH_ASSOC) as $u) { if ($u['email_verified_at'] !== null && in_array(strtolower($u['email']), $managerEmails, true)) { $eligible['verified_managers']++; if ($u['two_factor_secret'] !== null && $u['two_factor_confirmed_at'] !== null) { $eligible['verified_managers_with_mfa']++; } } } $source->exec('ROLLBACK'); $restored->exec('ROLLBACK'); $files = $dir.'/private-files.zip'; $zip = new ZipArchive; throw_unless($zip->open($files, ZipArchive::CREATE | ZipArchive::EXCL) === true, RuntimeException::class, 'Archive unavailable.'); $fileHashes = []; $missing = []; $privateRoot = realpath($root.'/storage/app/private'); foreach ($documents as $relative) { $path = realpath($privateRoot.'/'.$relative); if (! $path || ! str_starts_with(strtolower($path), strtolower($privateRoot.DIRECTORY_SEPARATOR))) { $missing[] = $relative; continue; } $name = 'private/'.str_replace('\\', '/', $relative); $zip->addFile($path, $name); $fileHashes[$name] = hash_file('sha256', $path); } // Original stable configuration is kept only in the private restore archive. $effectiveRuntime = $dir.'/effective-runtime.json'; file_put_contents($effectiveRuntime, json_encode(['app_key' => config('app.key'), 'cipher' => config('app.cipher'), 'config_cached' => $app->configurationIsCached()], JSON_THROW_ON_ERROR)); $zip->addFile($effectiveRuntime, 'runtime/effective-runtime.json'); $fileHashes['runtime/effective-runtime.json'] = hash_file('sha256', $effectiveRuntime); $zip->addFile($root.'/.env', 'runtime/.env'); $fileHashes['runtime/.env'] = hash_file('sha256', $root.'/.env'); // Preserve the actually loaded active bundle for reversal; no active build is changed. foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root.'/public/build', FilesystemIterator::SKIP_DOTS)) as $asset) { if (! $asset->isFile()) { continue; } $relative = substr(str_replace('\\', '/', $asset->getPathname()), strlen(str_replace('\\', '/', $root.'/public/build')) + 1); $name = 'runtime/active-build/'.$relative; $zip->addFile($asset->getPathname(), $name); $fileHashes[$name] = hash_file('sha256', $asset->getPathname()); } $zip->close(); $read = new ZipArchive; $read->open($files); $restoreFiles = $dir.'/files-restored'; mkdir($restoreFiles, 0700, true); foreach ($fileHashes as $name => $expected) { throw_unless(! str_contains($name, ':') && ! str_starts_with($name, '/') && ! array_intersect(explode('/', $name), ['.', '..']), RuntimeException::class, 'Member path refused.'); $bytes = $read->getFromName($name); throw_unless(is_string($bytes) && hash('sha256', $bytes) === $expected, RuntimeException::class, 'Archive member differs.'); $dest = $restoreFiles.'/'.str_replace('/', DIRECTORY_SEPARATOR, $name); if (! is_dir(dirname($dest))) { mkdir(dirname($dest), 0700, true); } throw_unless(str_starts_with(strtolower(realpath(dirname($dest))), strtolower(realpath($restoreFiles).DIRECTORY_SEPARATOR)), RuntimeException::class, 'Resolved restore path refused.'); file_put_contents($dest, $bytes); throw_unless(hash_file('sha256', $dest) === $expected, RuntimeException::class, 'Restored file differs.'); } $read->close(); $proof = ['verified_at' => gmdate('c'), 'code_hash' => $hash, 'target' => $identity, 'restored_target' => ['database' => $clone, 'port' => 55441], 'dump_path' => $dump, 'dump_sha256' => hash_file('sha256', $dump), 'files_path' => $files, 'files_sha256' => hash_file('sha256', $files), 'restore_verified' => true, 'files_verified' => $missing === [], 'tables' => $tables, 'sequences' => $sequences, 'sequences_verified' => true, 'original_mfa_user_ids' => $originalMfaIds, 'file_count' => count($fileHashes), 'missing_files' => $missing, 'source_installations' => $installationState, 'installer_eligibility_counts' => $eligible, 'runtime' => ['config_cached' => $app->configurationIsCached(), 'cipher' => config('app.cipher'), 'session_driver' => config('session.driver'), 'app_name' => config('app.name'), 'organization_country_enabled' => config('portal.organization_country_enabled'), 'opportunity_dashboard_access_enabled' => config('portal.opportunity_dashboard_access_enabled'), 'profile_access_enabled' => config('portal.profile_access_enabled'), 'opportunity_specialized_drafts_enabled' => config('portal.opportunity_specialized_drafts_enabled'), 'opportunity_modular_profiles_qa_enabled' => config('portal.opportunity_modular_profiles_qa_enabled'), 'legacy_content_qa_enabled' => config('portal.legacy_content_qa_enabled'), 'investor_path_enabled' => config('investor-path.enabled')], 'candidate_qa_manifest_sha256' => hash_file('sha256', $root.'/public/build-qa-opportunities/manifest.json'), 'active_manifest_sha256' => hash_file('sha256', $root.'/public/build/manifest.json'), 'compatibility_verified' => false, 'reversal_verified' => false, 'qa_passed' => false, 'active_writes' => 0]; $stagePath = $root.'/storage/app/private/investmoz-v2/portal-audit/country-production-stage-latest.json'; if (is_file($stagePath)) { $stage = json_decode(file_get_contents($stagePath), true, 512, JSON_THROW_ON_ERROR); throw_unless(hash_file('sha256', $stage['build_directory'].'/manifest.json') === $stage['build_manifest_sha256'], RuntimeException::class, 'Staged production manifest changed.'); $proof['production_manifest_sha256'] = $stage['build_manifest_sha256']; $proof['production_input_hash'] = $stage['input_hash']; } file_put_contents($dir.'/backup-proof.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); file_put_contents($root.'/storage/app/private/investmoz-v2/portal-audit/country-candidate-backup-latest.json', json_encode(['path' => $dir.'/backup-proof.json', 'directory' => $dir, 'restored_target' => $proof['restored_target']], JSON_THROW_ON_ERROR)); echo json_encode(['result' => $missing === [] ? 'restored_files_verified' : 'restore_partial_missing_files', 'tables' => count($tables), 'files' => count($fileHashes), 'missing_files' => count($missing), 'clone' => $clone, 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL;