make(Kernel::class)->bootstrap(); $identity = portalQaAssertTarget($target); $directory = $root.'/storage/app/private/investmoz-v2/portal-audit'; $private = $directory.'/profiles-v1-actors-private.json'; $baselinePath = $directory.'/profiles-v1-before-qa.json'; function snapshot(array $tables, ?array $baseline = null): array { $result = []; foreach ($tables as $table) { $columns = $baseline[$table]['columns'] ?? Schema::getColumnListing($table); $projection = implode(',', array_map(fn ($c) => '"'.$c.'"', $columns)); $identifier = in_array('id', $columns, true) ? 'id::text' : 'NULL::text'; $rows = DB::select('SELECT '.$identifier.' AS id, row_to_json(t)::text AS row FROM (SELECT '.$projection.' FROM "'.$table.'") t ORDER BY row_to_json(t)::text'); $result[$table] = ['columns' => $columns, 'rows' => []]; foreach ($rows as $row) { $digest = hash('sha256', $row->row); $result[$table]['rows'][$row->id ?? $digest] = $digest; } } return $result; } $tables = ['users', 'opportunities', 'organizations', 'memberships', 'documents', 'opportunity_projects', 'opportunity_clusters', 'opportunity_images', 'opportunity_documents', 'opportunity_locations', 'geo_locations', 'geo_geometries', 'desk_cases', 'desk_requests', 'investment_dossiers', 'investor_journeys', 'journey_steps', 'audit_events']; $tables = array_values(array_filter($tables, fn ($t) => Schema::hasTable($t))); $mode = $argv[1] ?? 'inspect'; if ($mode === 'prepare') { if (! is_file($baselinePath)) { file_put_contents($baselinePath, json_encode(['target' => $target, 'tables' => snapshot($tables), 'active_manifest_sha256' => hash_file('sha256', $root.'/public/build/manifest.json')], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); } if (! Schema::hasTable('portal_role_assignments')) { config(['portal_profiles.authenticated_installation' => true]); throw_unless(Artisan::call('migrate', ['--path' => 'database/migrations/2026_10_10_040000_create_portal_profile_authorization.php', '--force' => true]) === 0, RuntimeException::class, 'QA profile migration failed'); } $actors = is_file($private) ? json_decode(file_get_contents($private), true, 512, JSON_THROW_ON_ERROR) : []; foreach (['public', 'promoter', 'colleague', 'outsider', 'validator', 'publisher', 'auditor', 'super_first', 'super_second', 'bootstrap', 'replacement', 'multiple'] as $profile) { if (isset($actors[$profile])) { $user = User::findOrFail($actors[$profile]['id']); throw_unless($user->email === $actors[$profile]['email'] && str_ends_with($user->email, '@example.invalid'), RuntimeException::class, 'Synthetic actor identity mismatch'); continue; } $email = 'profiles-v1-'.$profile.'@example.invalid'; throw_if(User::where('email', $email)->exists(), RuntimeException::class, 'Existing actor has no private QA receipt; do not rotate it implicitly'); $password = Str::password(40); $user = User::create(['name' => 'QA profiles v1 '.$profile, 'email' => $email, 'password' => Hash::make($password)]); $user->forceFill(['email_verified_at' => now()])->save(); $actors[$profile] = ['id' => $user->id, 'email' => $email, 'password' => $password]; file_put_contents($private, json_encode($actors, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); } foreach (['organization' => ['promoter', 'colleague', 'multiple'], 'other_organization' => ['outsider']] as $label => $members) { $orgPath = $directory.'/profiles-v1-organizations-qa.json'; $organizations = is_file($orgPath) ? json_decode(file_get_contents($orgPath), true, 512, JSON_THROW_ON_ERROR) : []; if (! isset($organizations[$label])) { $organizations[$label] = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $organizations[$label], 'source_system' => 'profiles-v1-qa', 'name' => 'Synthetic QA '.$label, 'organization_type' => 'private']); file_put_contents($orgPath, json_encode($organizations, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); } foreach ($members as $member) { if (! DB::table('memberships')->where('organization_id', $organizations[$label])->where('user_id', $actors[$member]['id'])->exists()) { DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'source_system' => 'profiles-v1-qa', 'organization_id' => $organizations[$label], 'user_id' => $actors[$member]['id'], 'role' => 'officer']); } } } echo json_encode(['target' => $identity, 'migration_installed_in_qa' => true, 'synthetic_actors' => array_map(fn ($a) => $a['id'], $actors), 'active_writes' => 0, 'mfa' => 'Enroll through authenticated Fortify; no direct MFA injection']).PHP_EOL; exit; } if ($mode === 'verify') { $baseline = json_decode(file_get_contents($baselinePath), true, 512, JSON_THROW_ON_ERROR); $current = snapshot(array_keys($baseline['tables']), $baseline['tables']); $preserved = []; foreach ($baseline['tables'] as $table => $proof) { $preserved[$table] = count(array_diff_assoc($proof['rows'], $current[$table]['rows'])) === 0; } $proof = ['target' => $identity, 'previous_rows_preserved_by_id_and_original_columns' => $preserved, 'captured_at' => gmdate('c'), 'schema_installed' => Schema::hasTable('portal_role_assignments'), 'fingerprints' => [ 'profiles' => hash('sha256', json_encode(array_map(fn ($path) => hash_file('sha256', $path), PortalProfileAccess::fingerprintFiles()), JSON_THROW_ON_ERROR)), 'management' => app(OpportunityManagementInstallation::class)->fingerprint(), 'resources' => app(OpportunityResourceInstallation::class)->fingerprint(), 'desk_path' => app(VirtualDeskInstallation::class)->fingerprint(), 'geometry' => app(TerritoryGeometryImport::class)->fingerprint(), ], 'active_manifest_preserved' => hash_file('sha256', $root.'/public/build/manifest.json') === $baseline['active_manifest_sha256'], 'mfa18_unchanged' => $preserved['users'], 'pair' => Schema::hasTable('portal_superuser_pair') ? DB::table('portal_superuser_pair')->first() : null, 'active_writes' => 0, 'browser_visual_observed' => false]; throw_unless(! in_array(false, $preserved, true) && $proof['active_manifest_preserved'], RuntimeException::class, 'Baseline preservation failed'); file_put_contents($directory.'/profiles-v1-preservation-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($proof, JSON_THROW_ON_ERROR).PHP_EOL; exit; } DB::beginTransaction(); DB::statement('SET TRANSACTION READ ONLY'); echo json_encode(['target' => $identity, 'schema_tables' => Schema::getTableListing(), 'opportunity_columns' => Schema::getColumnListing('opportunities'), 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL; DB::rollBack();