#!/usr/bin/env bash set -euo pipefail set +x required=(DEPLOY_PATH EXPECTED_DEPLOY_SHA RESET_EMAIL RESET_CONFIRMATION) for key in "${required[@]}"; do if [ -z "${!key:-}" ]; then echo "required staging account-reset input is missing: $key" >&2 exit 1 fi done [[ "$EXPECTED_DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "invalid expected deployment SHA" >&2 exit 1 } [[ "$RESET_EMAIL" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,63}$ ]] || { echo "invalid reset email" >&2 exit 1 } [ "$RESET_CONFIRMATION" = "RESET $RESET_EMAIL" ] || { echo "account reset confirmation does not match" >&2 exit 1 } [ "$DEPLOY_PATH" = "/opt/jyotisha-staging" ] || { echo "refusing non-staging deployment path" >&2 exit 1 } state_directory="$DEPLOY_PATH/.state" [ -f "$state_directory/deployed-revision" ] || { echo "staging deployed revision is unavailable" >&2 exit 1 } [ "$(<"$state_directory/deployed-revision")" = "$EXPECTED_DEPLOY_SHA" ] || { echo "deployed staging revision does not match the approved reset SHA" >&2 exit 1 } install -d -m 700 "$state_directory" exec 9>"$state_directory/mutation.lock" flock -n 9 || { echo "another staging mutation holds the host lock" >&2 exit 75 } cd "$DEPLOY_PATH" compose=(docker compose -p jyotisha-staging -f deploy/docker-compose.postgres.yml) "${compose[@]}" ps --status running postgres --quiet | grep -q . || { echo "staging postgres container is not running" >&2 exit 1 } run_psql() { "${compose[@]}" exec -T -e RESET_EMAIL="$RESET_EMAIL" postgres sh -ceu ' exec psql -X -v ON_ERROR_STOP=1 -v target_email="$RESET_EMAIL" \ -U "$POSTGRES_USER" -d "$POSTGRES_DB" ' } run_psql <<'SQL' begin; create temporary table reset_snapshot on commit drop as select identity_user.id, identity_user.email, profile.email as profile_email, profile.credits, (select count(*) from identity.accounts value where value.user_id = identity_user.id) as identity_accounts, (select count(*) from identity.sessions value where value.user_id = identity_user.id) as identity_sessions, (select count(*) from public.credit_transactions value where value.user_id = identity_user.id) as credit_transactions, (select count(*) from public.credit_request_cancellations value where value.user_id = identity_user.id) as credit_cancellations, (select count(*) from public.consultation_requests value where value.user_id = identity_user.id) as consultation_requests, (select count(*) from public.birth_time_rectification_billing value where value.user_id = identity_user.id) as rectification_billing, (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = identity_user.id) as action_receipts, (select count(*) from public.redemption_codes value where value.redeemed_by = identity_user.id) as redeemed_codes, (select count(*) from audit.admin_audit_logs value where value.actor_user_id = identity_user.id) as admin_audit_logs from identity.users identity_user join auth.users auth_user on auth_user.id = identity_user.id join public.profiles profile on profile.id = identity_user.id where lower(btrim(identity_user.email)) = lower(btrim(:'target_email')) and lower(btrim(auth_user.email)) = lower(btrim(:'target_email')) for update of identity_user, auth_user, profile; do $$ begin if (select count(*) from reset_snapshot) <> 1 then raise exception 'account_not_found_or_identity_bridge_mismatch'; end if; end $$; select jsonb_build_object( 'stage', 'preflight', 'email', snapshot.email, 'credits', snapshot.credits, 'identityAccounts', snapshot.identity_accounts, 'identitySessions', snapshot.identity_sessions, 'creditTransactions', snapshot.credit_transactions, 'creditCancellations', snapshot.credit_cancellations, 'consultationRequests', snapshot.consultation_requests, 'rectificationBilling', snapshot.rectification_billing, 'actionReceipts', snapshot.action_receipts, 'redeemedCodes', snapshot.redeemed_codes, 'adminAuditLogs', snapshot.admin_audit_logs, 'chatSessions', (select count(*) from public.chat_sessions value where value.user_id = snapshot.id), 'chartProfiles', (select count(*) from public.chart_profiles value where value.user_id = snapshot.id), 'synastryReports', (select count(*) from public.synastry_reports value where value.user_id = snapshot.id), 'legacyRectificationCases', (select count(*) from public.birth_time_rectification_cases value where value.user_id = snapshot.id), 'v5RectificationCases', (select count(*) from public.birth_time_rectification_v4_cases value where value.user_id = snapshot.id), 'v5AgentRuns', (select count(*) from public.birth_time_rectification_agent_runs value where value.user_id = snapshot.id), 'v5Diagnostics', (select count(*) from public.birth_time_rectification_diagnostics value where value.user_id = snapshot.id), 'v5Jobs', (select count(*) from public.birth_time_rectification_v4_jobs value where value.user_id = snapshot.id) ) from reset_snapshot snapshot; update public.profiles profile set name = null, birth_date = null, birth_time = null, country_code = null, province_code = null, city_code = null, district_code = null, onboarding_payload = null, onboarding_version = null, onboarding_generated_at = null, latitude = null, longitude = null, timezone_offset = null, reported_birth_time = null, active_birth_time = null, birth_time_source = null, birth_time_period = null, birth_time_clue = null, uncertainty_before_minutes = null, uncertainty_after_minutes = null, birth_time_status = null, rectification_confidence = null, rectification_case_id = null, birth_place_label = null, birth_place_type = null, birth_place_provider = null, birth_place_provider_id = null, timezone_id = null, timezone_source = null, updated_at = pg_catalog.now() from reset_snapshot snapshot where profile.id = snapshot.id; delete from public.chat_sessions value using reset_snapshot snapshot where value.user_id = snapshot.id; delete from public.chart_profiles value using reset_snapshot snapshot where value.user_id = snapshot.id; delete from public.synastry_reports value using reset_snapshot snapshot where value.user_id = snapshot.id; delete from public.birth_time_rectification_v4_cases value using reset_snapshot snapshot where value.user_id = snapshot.id; delete from public.birth_time_rectification_cases value using reset_snapshot snapshot where value.user_id = snapshot.id; do $$ begin if exists ( select 1 from reset_snapshot snapshot join identity.users identity_user on identity_user.id = snapshot.id join auth.users auth_user on auth_user.id = snapshot.id join public.profiles profile on profile.id = snapshot.id where identity_user.email is distinct from snapshot.email or auth_user.email is distinct from snapshot.email or profile.email is distinct from snapshot.profile_email or profile.credits is distinct from snapshot.credits or (select count(*) from identity.accounts value where value.user_id = snapshot.id) <> snapshot.identity_accounts or (select count(*) from identity.sessions value where value.user_id = snapshot.id) <> snapshot.identity_sessions or (select count(*) from public.credit_transactions value where value.user_id = snapshot.id) <> snapshot.credit_transactions or (select count(*) from public.credit_request_cancellations value where value.user_id = snapshot.id) <> snapshot.credit_cancellations or (select count(*) from public.consultation_requests value where value.user_id = snapshot.id) <> snapshot.consultation_requests or (select count(*) from public.birth_time_rectification_billing value where value.user_id = snapshot.id) <> snapshot.rectification_billing or (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = snapshot.id) <> snapshot.action_receipts or (select count(*) from public.redemption_codes value where value.redeemed_by = snapshot.id) <> snapshot.redeemed_codes or (select count(*) from audit.admin_audit_logs value where value.actor_user_id = snapshot.id) <> snapshot.admin_audit_logs ) then raise exception 'preserved_state_changed'; end if; if exists (select 1 from public.chat_sessions value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists (select 1 from public.chart_profiles value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists (select 1 from public.synastry_reports value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists (select 1 from public.birth_time_rectification_cases value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists (select 1 from public.birth_time_rectification_v4_cases value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists (select 1 from public.birth_time_rectification_v4_jobs value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists (select 1 from public.birth_time_rectification_agent_runs value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists (select 1 from public.birth_time_rectification_diagnostics value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists (select 1 from public.birth_time_rectification_candidate_feature_snapshots value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists (select 1 from public.birth_time_rectification_public_messages value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists (select 1 from public.birth_time_rectification_pending_evidence value join reset_snapshot snapshot on value.user_id = snapshot.id) or exists ( select 1 from public.profiles profile join reset_snapshot snapshot on profile.id = snapshot.id where profile.name is not null or profile.birth_date is not null or profile.birth_time is not null or profile.country_code is not null or profile.province_code is not null or profile.city_code is not null or profile.district_code is not null or profile.onboarding_payload is not null or profile.onboarding_version is not null or profile.onboarding_generated_at is not null or profile.latitude is not null or profile.longitude is not null or profile.timezone_offset is not null or profile.reported_birth_time is not null or profile.active_birth_time is not null or profile.birth_time_source is not null or profile.birth_time_period is not null or profile.birth_time_clue is not null or profile.uncertainty_before_minutes is not null or profile.uncertainty_after_minutes is not null or profile.birth_time_status is not null or profile.rectification_confidence is not null or profile.rectification_case_id is not null or profile.birth_place_label is not null or profile.birth_place_type is not null or profile.birth_place_provider is not null or profile.birth_place_provider_id is not null or profile.timezone_id is not null or profile.timezone_source is not null ) then raise exception 'reset_state_not_empty'; end if; end $$; commit; SQL run_psql <<'SQL' begin; create temporary table postflight_target on commit drop as select identity_user.id, identity_user.email, profile.credits, not ( profile.name is null and profile.birth_date is null and profile.birth_time is null and profile.country_code is null and profile.province_code is null and profile.city_code is null and profile.district_code is null and profile.onboarding_payload is null and profile.onboarding_version is null and profile.onboarding_generated_at is null and profile.latitude is null and profile.longitude is null and profile.timezone_offset is null and profile.reported_birth_time is null and profile.active_birth_time is null and profile.birth_time_source is null and profile.birth_time_period is null and profile.birth_time_clue is null and profile.uncertainty_before_minutes is null and profile.uncertainty_after_minutes is null and profile.birth_time_status is null and profile.rectification_confidence is null and profile.rectification_case_id is null and profile.birth_place_label is null and profile.birth_place_type is null and profile.birth_place_provider is null and profile.birth_place_provider_id is null and profile.timezone_id is null and profile.timezone_source is null ) as profile_not_reset, lower(btrim(profile.email)) = lower(btrim(identity_user.email)) as profile_email_matches from identity.users identity_user join auth.users auth_user on auth_user.id = identity_user.id and lower(btrim(auth_user.email)) = lower(btrim(identity_user.email)) join public.profiles profile on profile.id = identity_user.id where lower(btrim(identity_user.email)) = lower(btrim(:'target_email')); do $$ declare target_id uuid; begin if (select count(*) from postflight_target) <> 1 then raise exception 'postflight_account_not_found_or_identity_bridge_mismatch'; end if; select id into target_id from postflight_target; if exists (select 1 from public.chat_sessions value where value.user_id = target_id) or exists (select 1 from public.chart_profiles value where value.user_id = target_id) or exists (select 1 from public.synastry_reports value where value.user_id = target_id) or exists (select 1 from public.birth_time_rectification_cases value where value.user_id = target_id) or exists (select 1 from public.birth_time_rectification_v4_cases value where value.user_id = target_id) or exists (select 1 from public.birth_time_rectification_v4_jobs value where value.user_id = target_id) or exists (select 1 from public.birth_time_rectification_agent_runs value where value.user_id = target_id) or exists (select 1 from public.birth_time_rectification_diagnostics value where value.user_id = target_id) or exists (select 1 from public.birth_time_rectification_candidate_feature_snapshots value where value.user_id = target_id) or exists (select 1 from public.birth_time_rectification_public_messages value where value.user_id = target_id) or exists (select 1 from public.birth_time_rectification_pending_evidence value where value.user_id = target_id) or exists (select 1 from postflight_target where profile_not_reset) then raise exception 'postflight_reset_state_not_empty'; end if; end $$; select jsonb_build_object( 'stage', 'postflight', 'matchedAccounts', (select count(*) from postflight_target), 'email', (select email from postflight_target), 'credits', (select credits from postflight_target), 'profileEmailMatches', (select profile_email_matches from postflight_target), 'profileNotReset', (select profile_not_reset from postflight_target), 'chatSessions', (select count(*) from public.chat_sessions value where value.user_id = (select id from postflight_target)), 'chartProfiles', (select count(*) from public.chart_profiles value where value.user_id = (select id from postflight_target)), 'synastryReports', (select count(*) from public.synastry_reports value where value.user_id = (select id from postflight_target)), 'legacyRectificationCases', (select count(*) from public.birth_time_rectification_cases value where value.user_id = (select id from postflight_target)), 'v5RectificationCases', (select count(*) from public.birth_time_rectification_v4_cases value where value.user_id = (select id from postflight_target)), 'v5Jobs', (select count(*) from public.birth_time_rectification_v4_jobs value where value.user_id = (select id from postflight_target)), 'v5AgentRuns', (select count(*) from public.birth_time_rectification_agent_runs value where value.user_id = (select id from postflight_target)), 'v5Diagnostics', (select count(*) from public.birth_time_rectification_diagnostics value where value.user_id = (select id from postflight_target)), 'v5FeatureSnapshots', (select count(*) from public.birth_time_rectification_candidate_feature_snapshots value where value.user_id = (select id from postflight_target)), 'v5PublicMessages', (select count(*) from public.birth_time_rectification_public_messages value where value.user_id = (select id from postflight_target)), 'v5PendingEvidence', (select count(*) from public.birth_time_rectification_pending_evidence value where value.user_id = (select id from postflight_target)), 'identityAccounts', (select count(*) from identity.accounts value where value.user_id = (select id from postflight_target)), 'identitySessions', (select count(*) from identity.sessions value where value.user_id = (select id from postflight_target)), 'creditTransactions', (select count(*) from public.credit_transactions value where value.user_id = (select id from postflight_target)), 'creditCancellations', (select count(*) from public.credit_request_cancellations value where value.user_id = (select id from postflight_target)), 'consultationRequests', (select count(*) from public.consultation_requests value where value.user_id = (select id from postflight_target)), 'rectificationBilling', (select count(*) from public.birth_time_rectification_billing value where value.user_id = (select id from postflight_target)), 'actionReceipts', (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = (select id from postflight_target)) ); commit; SQL