diff --git a/Makefile b/Makefile index 390299120..3c3de9d1b 100644 --- a/Makefile +++ b/Makefile @@ -356,6 +356,9 @@ if ! ensure_dev_infra; then exit 1 fi +echo "=> 编译 aether-gateway..." +cargo build -p aether-gateway --bin aether-gateway + GATEWAY_PID="" GATEWAY_LOG_DIR="" GATEWAY_LOG_FILE="" @@ -364,8 +367,8 @@ create_gateway_log_file echo "=> 启动 aether-gateway (Rust frontdoor: 0.0.0.0:$${APP_PORT})..." echo "=> 日志过滤: $${RUST_LOG}" -echo "=> 执行命令: cargo run -p aether-gateway --bin aether-gateway -- --app-port $${APP_PORT}" -cargo run -p aether-gateway --bin aether-gateway -- --app-port "$${APP_PORT}" > >( +echo "=> 执行命令: target/debug/aether-gateway --app-port $${APP_PORT}" +target/debug/aether-gateway --app-port "$${APP_PORT}" > >( tee -a "$${GATEWAY_LOG_FILE}" ) 2>&1 & GATEWAY_PID=$$! @@ -456,7 +459,7 @@ if [ -f .env ]; then fi export APP_PORT="$${APP_PORT:-8084}" -echo "=> 启动后端: RUST_LOG=$${DEV_RUST_LOG} cargo run -p aether-gateway --bin aether-gateway -- --app-port $${APP_PORT:-8084}" +echo "=> 启动后端: 先编译 aether-gateway,再运行 target/debug/aether-gateway --app-port $${APP_PORT:-8084}" /bin/bash -euo pipefail -c "$$DEV_BACKEND_SCRIPT" & backend_pid=$$! diff --git a/README.md b/README.md index 3a44c1c6a..7f529b8e2 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ make dev `make dev` 会同时启动后端 `aether-gateway` 和前端 `frontend` 的 Vite dev server。需要单独启动时可使用 `make dev-backend` 或 `make dev-frontend`。 Postgres / Redis 本地依赖未就绪时,`make dev` 会自动执行 `docker compose up -d postgres redis`。 -数据库 schema 和历史数据准备也会在启动时自动完成;通常不需要手动区分 migration 与 backfill。排查或部署前预执行时可使用: +`make dev` 会先完成后端编译,再开始计算服务健康检查超时。数据库 schema 和必要的派生数据准备也会在启动时自动完成;通常不需要手动区分 migration 与 backfill。升级不会主动重写或清除已有业务历史记录,新写入会直接遵循当前的数据持久化策略。排查或部署前预执行时可使用: ```bash make db-status diff --git a/crates/aether-data/adapters/mysql/migrations/20260822000000_purge_request_candidate_sensitive_diagnostics.sql b/crates/aether-data/adapters/mysql/migrations/20260822000000_purge_request_candidate_sensitive_diagnostics.sql deleted file mode 100644 index a631c9686..000000000 --- a/crates/aether-data/adapters/mysql/migrations/20260822000000_purge_request_candidate_sensitive_diagnostics.sql +++ /dev/null @@ -1,16 +0,0 @@ -UPDATE request_candidates -SET - username = NULL, - api_key_name = NULL, - extra_data = NULL, - required_capabilities = NULL, - error_message = NULL, - error_type = NULL, - skip_reason = NULL -WHERE username IS NOT NULL - OR api_key_name IS NOT NULL - OR extra_data IS NOT NULL - OR required_capabilities IS NOT NULL - OR error_message IS NOT NULL - OR error_type IS NOT NULL - OR skip_reason IS NOT NULL; diff --git a/crates/aether-data/adapters/mysql/migrations/20260822010000_purge_usage_and_video_task_sensitive_diagnostics.sql b/crates/aether-data/adapters/mysql/migrations/20260822010000_purge_usage_and_video_task_sensitive_diagnostics.sql deleted file mode 100644 index 2b3df6921..000000000 --- a/crates/aether-data/adapters/mysql/migrations/20260822010000_purge_usage_and_video_task_sensitive_diagnostics.sql +++ /dev/null @@ -1,124 +0,0 @@ --- Historical HTTP captures predate the deny-by-default persistence policy and --- may contain credentials or request content. Keep the usage and projected --- billing facts, but remove every legacy copy of the raw exchange. -UPDATE `usage` AS usage_rows -LEFT JOIN usage_cost_reservations AS reservation - ON reservation.state = 'reserved' - AND REGEXP_LIKE( - reservation.reservation_token, - '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', - 'c' - ) - AND reservation.request_id = usage_rows.request_id - AND reservation.subject_id = usage_rows.user_id - AND reservation.reservation_token = JSON_UNQUOTE(JSON_EXTRACT( - CASE - WHEN JSON_VALID(usage_rows.request_metadata) = 1 - THEN usage_rows.request_metadata - ELSE JSON_OBJECT() - END, - '$.plan_usage_reservation_token' - )) -SET usage_rows.error_message = NULL, - usage_rows.error_category = CASE - WHEN usage_rows.error_category IS NULL - OR TRIM(usage_rows.error_category) = '' THEN NULL - WHEN LOWER(TRIM(usage_rows.error_category)) IN ( - 'auth', 'cancelled', 'client_error', 'http_error', - 'non_success_status', 'provider_error', 'rate_limit', 'redirect', - 'server_error', 'stream_missing_terminal_event', - 'stream_terminal_error', 'upstream_error' - ) THEN LOWER(TRIM(usage_rows.error_category)) - ELSE 'other_error' - END, - usage_rows.request_headers = NULL, - usage_rows.request_body = NULL, - usage_rows.provider_request_headers = NULL, - usage_rows.provider_request_body = NULL, - usage_rows.response_headers = NULL, - usage_rows.response_body = NULL, - usage_rows.client_response_headers = NULL, - usage_rows.client_response_body = NULL, - usage_rows.request_body_compressed = NULL, - usage_rows.provider_request_body_compressed = NULL, - usage_rows.response_body_compressed = NULL, - usage_rows.client_response_body_compressed = NULL, - usage_rows.request_metadata = CASE - WHEN reservation.reservation_token IS NULL THEN NULL - WHEN JSON_TYPE(JSON_EXTRACT( - CASE - WHEN JSON_VALID(usage_rows.request_metadata) = 1 - THEN usage_rows.request_metadata - ELSE JSON_OBJECT() - END, - '$.plan_usage_reservation_deferred' - )) = 'BOOLEAN' - AND JSON_UNQUOTE(JSON_EXTRACT( - CASE - WHEN JSON_VALID(usage_rows.request_metadata) = 1 - THEN usage_rows.request_metadata - ELSE JSON_OBJECT() - END, - '$.plan_usage_reservation_deferred' - )) = 'true' - THEN JSON_SET( - JSON_OBJECT( - 'plan_usage_reservation_token', reservation.reservation_token - ), - '$.plan_usage_reservation_deferred', - JSON_EXTRACT('true', '$') - ) - ELSE JSON_OBJECT( - 'plan_usage_reservation_token', reservation.reservation_token - ) - END -WHERE usage_rows.error_message IS NOT NULL - OR usage_rows.error_category IS NOT NULL - OR usage_rows.request_headers IS NOT NULL - OR usage_rows.request_body IS NOT NULL - OR usage_rows.provider_request_headers IS NOT NULL - OR usage_rows.provider_request_body IS NOT NULL - OR usage_rows.response_headers IS NOT NULL - OR usage_rows.response_body IS NOT NULL - OR usage_rows.client_response_headers IS NOT NULL - OR usage_rows.client_response_body IS NOT NULL - OR usage_rows.request_body_compressed IS NOT NULL - OR usage_rows.provider_request_body_compressed IS NOT NULL - OR usage_rows.response_body_compressed IS NOT NULL - OR usage_rows.client_response_body_compressed IS NOT NULL - OR usage_rows.request_metadata IS NOT NULL; - -DELETE FROM usage_http_audits; - -DELETE FROM usage_body_blobs; - --- All fields needed by billing reads are projected into typed columns on this --- table. The historical JSON documents include rule expressions, catalog --- snapshots, and arbitrary dimensions, so they are not retained. -UPDATE usage_settlement_snapshots -SET settlement_snapshot = NULL, - billing_dimensions = NULL -WHERE settlement_snapshot IS NOT NULL - OR billing_dimensions IS NOT NULL; - -UPDATE video_tasks -SET original_request_body = NULL, - converted_request_body = NULL, - progress_message = NULL, - error_message = NULL, - request_metadata = NULL, - video_url = NULL, - video_urls = NULL, - thumbnail_url = NULL, - stored_video_path = NULL, - webhook_url = NULL -WHERE original_request_body IS NOT NULL - OR converted_request_body IS NOT NULL - OR progress_message IS NOT NULL - OR error_message IS NOT NULL - OR request_metadata IS NOT NULL - OR video_url IS NOT NULL - OR video_urls IS NOT NULL - OR thumbnail_url IS NOT NULL - OR stored_video_path IS NOT NULL - OR webhook_url IS NOT NULL; diff --git a/crates/aether-data/adapters/mysql/migrations/20260822020000_purge_background_task_sensitive_diagnostics.sql b/crates/aether-data/adapters/mysql/migrations/20260822020000_purge_background_task_sensitive_diagnostics.sql deleted file mode 100644 index d3738a5e7..000000000 --- a/crates/aether-data/adapters/mysql/migrations/20260822020000_purge_background_task_sensitive_diagnostics.sql +++ /dev/null @@ -1,43 +0,0 @@ -UPDATE background_task_runs -SET owner_instance = NULL, - created_by = CASE - WHEN LOWER(TRIM(created_by)) IN ('admin', 'scheduler', 'system') - THEN LOWER(TRIM(created_by)) - ELSE NULL - END, - progress_message = NULL, - payload_json = NULL, - result_json = NULL, - error_message = CASE - WHEN status = 'failed' THEN 'background_task_failed' - ELSE NULL - END -WHERE owner_instance IS NOT NULL - OR created_by IS NOT NULL - OR progress_message IS NOT NULL - OR payload_json IS NOT NULL - OR result_json IS NOT NULL - OR error_message IS NOT NULL; - -UPDATE background_task_events -SET event_type = CASE - WHEN event_type IN ( - 'cancel_requested', 'failed', 'queued', 'running', - 'skipped', 'succeeded', 'worker_boot' - ) THEN event_type - ELSE 'unclassified_event' - END, - message = CASE - WHEN event_type IN ( - 'cancel_requested', 'failed', 'queued', 'running', - 'skipped', 'succeeded', 'worker_boot' - ) THEN event_type - ELSE 'unclassified_event' - END, - payload_json = NULL -WHERE message <> event_type - OR payload_json IS NOT NULL - OR event_type NOT IN ( - 'cancel_requested', 'failed', 'queued', 'running', - 'skipped', 'succeeded', 'worker_boot' - ); diff --git a/crates/aether-data/adapters/mysql/migrations/20260827000000_purge_plaintext_stripe_client_secrets.sql b/crates/aether-data/adapters/mysql/migrations/20260827000000_purge_plaintext_stripe_client_secrets.sql deleted file mode 100644 index 497adc2bb..000000000 --- a/crates/aether-data/adapters/mysql/migrations/20260827000000_purge_plaintext_stripe_client_secrets.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Stripe PaymentIntent client secrets are payment capabilities. New writes --- store only encrypted values; legacy plaintext cannot be migrated safely --- without the application encryption key, so remove it fail-closed. -UPDATE payment_orders -SET status = CASE - WHEN LOWER(TRIM(payment_method)) = 'stripe' AND status = 'pending' - THEN 'expired' - ELSE status - END, - gateway_response = JSON_REMOVE(gateway_response, '$.client_secret') -WHERE gateway_response IS NOT NULL - AND JSON_VALID(gateway_response) = 1 - AND JSON_CONTAINS_PATH(gateway_response, 'one', '$.client_secret') = 1; diff --git a/crates/aether-data/adapters/mysql/migrations/20260827010000_purge_legacy_payment_callback_payloads.sql b/crates/aether-data/adapters/mysql/migrations/20260827010000_purge_legacy_payment_callback_payloads.sql deleted file mode 100644 index 978bd16b7..000000000 --- a/crates/aether-data/adapters/mysql/migrations/20260827010000_purge_legacy_payment_callback_payloads.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Callback idempotency uses payload_hash and does not require the raw body. --- Older versions stored provider-controlled payloads that may contain payment --- capabilities or customer PII, so remove those legacy copies fail-closed. -UPDATE payment_callbacks -SET payload = NULL -WHERE payload IS NOT NULL; diff --git a/crates/aether-data/adapters/mysql/migrations/20260827020000_purge_video_task_prompts.sql b/crates/aether-data/adapters/mysql/migrations/20260827020000_purge_video_task_prompts.sql deleted file mode 100644 index 5a13072b8..000000000 --- a/crates/aether-data/adapters/mysql/migrations/20260827020000_purge_video_task_prompts.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Video prompts are user content and are not required for polling or billing. --- Remove historical copies now that new writes discard them before persistence. -UPDATE video_tasks -SET prompt = NULL -WHERE prompt IS NOT NULL; diff --git a/crates/aether-data/adapters/mysql/migrations/20260827030000_purge_identity_oauth_raw_userinfo.sql b/crates/aether-data/adapters/mysql/migrations/20260827030000_purge_identity_oauth_raw_userinfo.sql deleted file mode 100644 index abe3746a2..000000000 --- a/crates/aether-data/adapters/mysql/migrations/20260827030000_purge_identity_oauth_raw_userinfo.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Identity claims required by the application are stored in dedicated columns. --- Remove legacy provider-controlled userinfo JSON, which may contain unrelated --- PII or credentials and is not needed for authentication or account binding. -UPDATE user_oauth_links -SET extra_data = NULL -WHERE extra_data IS NOT NULL; diff --git a/crates/aether-data/adapters/mysql/migrations/20260827050000_anonymize_deleted_user_history.sql b/crates/aether-data/adapters/mysql/migrations/20260827050000_anonymize_deleted_user_history.sql index 568bf024a..f8a2b9cb2 100644 --- a/crates/aether-data/adapters/mysql/migrations/20260827050000_anonymize_deleted_user_history.sql +++ b/crates/aether-data/adapters/mysql/migrations/20260827050000_anonymize_deleted_user_history.sql @@ -88,290 +88,6 @@ PREPARE aether_drop_fact_user_fk_stmt FROM @aether_drop_fact_user_fk_sql; EXECUTE aether_drop_fact_user_fk_stmt; DEALLOCATE PREPARE aether_drop_fact_user_fk_stmt; -UPDATE request_candidates -SET username = NULL, api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = request_candidates.user_id -) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE request_candidates -SET api_key_name = NULL -WHERE api_key_name IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = request_candidates.api_key_id - ); - -UPDATE video_tasks -SET username = NULL, api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = video_tasks.user_id -) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE video_tasks -SET api_key_name = NULL -WHERE api_key_name IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = video_tasks.api_key_id - ); - -UPDATE `usage` -SET username = NULL, api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = `usage`.user_id -) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE `usage` -SET api_key_name = NULL -WHERE api_key_name IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = `usage`.api_key_id - ); - -UPDATE stats_user_daily -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_summary -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_summary.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_model -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_model.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_provider -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_provider.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_api_format -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_api_format.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_model_provider -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_model_provider.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_provider -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings_provider.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_model -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings_model.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_model_provider -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings_model_provider.user_id -) - AND username IS NOT NULL; - -UPDATE stats_daily_api_key -SET api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = stats_daily_api_key.api_key_id -) - AND api_key_name IS NOT NULL; - -UPDATE user_plan_entitlements AS entitlement -SET status = CASE WHEN status = 'active' THEN 'revoked' ELSE status END, - expires_at = LEAST(expires_at, UNIX_TIMESTAMP()), - updated_at = UNIX_TIMESTAMP() -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = entitlement.user_id -); - -UPDATE wallets AS wallet -SET status = 'disabled', updated_at = UNIX_TIMESTAMP() -WHERE (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id - )); - -UPDATE user_referrals AS referral -SET invite_code_snapshot = 'deleted-user', - source_json = NULL, - updated_at = UNIX_TIMESTAMP() -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = referral.inviter_user_id - ) - OR NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = referral.invitee_user_id - ); - -UPDATE referral_rewards AS reward -SET status = CASE - WHEN status IN ('pending', 'failed', 'applying') THEN 'voided' - ELSE status - END, - failure_reason = NULL, - admin_note = NULL, - updated_at = UNIX_TIMESTAMP() -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = reward.inviter_user_id - ) - OR NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = reward.invitee_user_id - ); - -UPDATE audit_logs AS history -SET description = 'deleted user event', - ip_address = NULL, - user_agent = NULL, - event_metadata = NULL, - error_message = NULL -WHERE (history.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.user_id - )) - OR (history.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = history.api_key_id - )); - -UPDATE wallet_transactions AS history -SET description = NULL -WHERE EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ( - (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) -); - -UPDATE wallet_transactions AS history -SET description = NULL -WHERE history.operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.operator_id - ); - -UPDATE payment_orders AS history -SET gateway_response = NULL -WHERE (history.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.user_id - )) - OR EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ( - (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) -); - -UPDATE payment_callbacks AS history -SET payload = NULL, - error_message = NULL -WHERE EXISTS ( - SELECT 1 - FROM payment_orders AS payment_order - LEFT JOIN wallets AS wallet ON wallet.id = payment_order.wallet_id - WHERE ( - payment_order.id = history.payment_order_id - OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no) - ) - AND ( - (payment_order.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = payment_order.user_id - )) - OR (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) -); - -UPDATE refund_requests AS history -SET reason = NULL, - payout_reference = NULL, - payout_proof = NULL, - failure_reason = NULL -WHERE (history.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.user_id - )) - OR EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ( - (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) - ) - OR (history.requested_by IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.requested_by - )) - OR (history.approved_by IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.approved_by - )) - OR (history.processed_by IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.processed_by - )); - -UPDATE referral_rewards AS reward -SET failure_reason = NULL, - admin_note = NULL, - updated_at = UNIX_TIMESTAMP() -WHERE reward.admin_operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = reward.admin_operator_id - ); - -UPDATE redeem_code_batches AS history -SET description = NULL -WHERE history.created_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.created_by - ); +-- Keep existing historical row values unchanged. Runtime deletion paths enforce +-- the current anonymization policy for newly deleted users. +SELECT 1; diff --git a/crates/aether-data/adapters/mysql/migrations/20260829000000_harden_deleted_user_history_anonymization.sql b/crates/aether-data/adapters/mysql/migrations/20260829000000_harden_deleted_user_history_anonymization.sql deleted file mode 100644 index 55467f7d1..000000000 --- a/crates/aether-data/adapters/mysql/migrations/20260829000000_harden_deleted_user_history_anonymization.sql +++ /dev/null @@ -1,451 +0,0 @@ --- Keep nullable/system-owned history rows intact while clearing values tied to --- deleted users or API keys. Raw payment callback payloads are always removed --- because they are not required for idempotency and may contain PII. -UPDATE request_candidates -SET username = NULL, api_key_name = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = request_candidates.user_id AND users.is_deleted = 0 - ) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE request_candidates -SET api_key_name = NULL -WHERE api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = request_candidates.api_key_id - ) - AND api_key_name IS NOT NULL; - -UPDATE video_tasks -SET username = NULL, api_key_name = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = video_tasks.user_id AND users.is_deleted = 0 - ) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE video_tasks -SET api_key_name = NULL -WHERE api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = video_tasks.api_key_id - ) - AND api_key_name IS NOT NULL; - -UPDATE `usage` -SET username = NULL, api_key_name = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = `usage`.user_id AND users.is_deleted = 0 - ) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE `usage` -SET api_key_name = NULL -WHERE api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = `usage`.api_key_id - ) - AND api_key_name IS NOT NULL; - -UPDATE stats_user_daily -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_summary -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_summary.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_model -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_model.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_provider -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_provider.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_api_format -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_api_format.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_model_provider -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_model_provider.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_cost_savings.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_provider -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_cost_savings_provider.user_id - AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_model -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_cost_savings_model.user_id - AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_model_provider -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM users - WHERE users.id = stats_user_daily_cost_savings_model_provider.user_id - AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_daily_api_key -SET api_key_name = NULL -WHERE api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = stats_daily_api_key.api_key_id - ) - AND api_key_name IS NOT NULL; - -UPDATE user_plan_entitlements AS entitlement -SET status = CASE WHEN status = 'active' THEN 'revoked' ELSE status END, - expires_at = LEAST(expires_at, UNIX_TIMESTAMP()), - updated_at = UNIX_TIMESTAMP() -WHERE NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = entitlement.user_id AND users.is_deleted = 0 -); - -UPDATE user_referrals AS referral -SET invite_code_snapshot = 'deleted-user', - source_json = NULL, - updated_at = UNIX_TIMESTAMP() -WHERE NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = referral.inviter_user_id AND users.is_deleted = 0 - ) - OR NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = referral.invitee_user_id AND users.is_deleted = 0 - ); - -UPDATE referral_rewards AS reward -SET status = CASE - WHEN status IN ('pending', 'failed', 'applying') THEN 'voided' - ELSE status - END, - failure_reason = NULL, - admin_note = NULL, - updated_at = UNIX_TIMESTAMP() -WHERE NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = reward.inviter_user_id AND users.is_deleted = 0 - ) - OR NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = reward.invitee_user_id AND users.is_deleted = 0 - ); - -UPDATE referral_rewards AS reward -SET failure_reason = NULL, - admin_note = NULL, - updated_at = UNIX_TIMESTAMP() -WHERE reward.admin_operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = reward.admin_operator_id AND users.is_deleted = 0 - ); - -UPDATE wallets AS wallet -SET status = 'disabled', updated_at = UNIX_TIMESTAMP() -WHERE (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = wallet.user_id AND users.is_deleted = 0 - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = wallet.api_key_id - )); - -UPDATE audit_logs AS history -SET description = 'deleted user event', - ip_address = NULL, - user_agent = NULL, - event_metadata = NULL, - error_message = NULL -WHERE (history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.user_id AND users.is_deleted = 0 - )) - OR (history.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = history.api_key_id - )); - -UPDATE wallet_transactions AS history -SET description = NULL -WHERE EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = wallet.user_id AND users.is_deleted = 0 - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id - AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = wallet.api_key_id - ))) -) - OR NOT EXISTS ( - SELECT 1 FROM wallets AS wallet WHERE wallet.id = history.wallet_id - ) - OR (history.operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.operator_id AND users.is_deleted = 0 - )); - -UPDATE payment_orders AS history -SET gateway_response = NULL -WHERE (history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.user_id AND users.is_deleted = 0 - )) - OR EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = wallet.user_id AND users.is_deleted = 0 - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id - AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = wallet.api_key_id - ))) -) - OR NOT EXISTS ( - SELECT 1 FROM wallets AS wallet WHERE wallet.id = history.wallet_id - ); - --- Raw provider payloads are never needed for idempotency and must be purged --- even when an old callback cannot be linked back to an order. -UPDATE payment_callbacks -SET payload = NULL -WHERE payload IS NOT NULL; - -UPDATE payment_callbacks AS history -SET error_message = NULL -WHERE history.error_message IS NOT NULL - AND ( - NOT EXISTS ( - SELECT 1 - FROM payment_orders AS payment_order - WHERE payment_order.id = history.payment_order_id - OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no) - ) - OR EXISTS ( - SELECT 1 - FROM payment_orders AS payment_order - LEFT JOIN wallets AS wallet ON wallet.id = payment_order.wallet_id - WHERE (payment_order.id = history.payment_order_id - OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no)) - AND ((payment_order.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = payment_order.user_id AND users.is_deleted = 0 - )) - OR wallet.id IS NULL - OR (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = wallet.user_id AND users.is_deleted = 0 - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id - AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = wallet.api_key_id - ))) - ) - ); - -UPDATE refund_requests AS history -SET reason = NULL, - payout_reference = NULL, - payout_proof = NULL, - failure_reason = NULL -WHERE (history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.user_id AND users.is_deleted = 0 - )) - OR EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = wallet.user_id AND users.is_deleted = 0 - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id - AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = wallet.api_key_id - ))) -) - OR NOT EXISTS ( - SELECT 1 FROM wallets AS wallet WHERE wallet.id = history.wallet_id - ) - OR (history.requested_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.requested_by AND users.is_deleted = 0 - )) - OR (history.approved_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.approved_by AND users.is_deleted = 0 - )) - OR (history.processed_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.processed_by AND users.is_deleted = 0 - )); - -UPDATE redeem_code_batches AS history -SET description = NULL -WHERE history.created_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.created_by AND users.is_deleted = 0 - ); - -UPDATE refund_requests -SET requested_by = NULL -WHERE requested_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = refund_requests.requested_by AND users.is_deleted = 0 - ); - -UPDATE refund_requests -SET approved_by = NULL -WHERE approved_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = refund_requests.approved_by AND users.is_deleted = 0 - ); - -UPDATE refund_requests -SET processed_by = NULL -WHERE processed_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = refund_requests.processed_by AND users.is_deleted = 0 - ); diff --git a/crates/aether-data/adapters/mysql/migrations/20260903010000_reset_legacy_oauth_email_verification.sql b/crates/aether-data/adapters/mysql/migrations/20260903010000_reset_legacy_oauth_email_verification.sql deleted file mode 100644 index 51dd0f076..000000000 --- a/crates/aether-data/adapters/mysql/migrations/20260903010000_reset_legacy_oauth_email_verification.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Older gateway releases marked every OAuth email as verified without --- retaining verification provenance. Reset those claims conservatively; a --- later trusted OAuth assertion for the same normalized email can upgrade it. -UPDATE users -SET email_verified = 0, - updated_at = UNIX_TIMESTAMP() -WHERE LOWER(TRIM(auth_source)) = 'oauth' - AND email_verified = 1; diff --git a/crates/aether-data/adapters/postgres/migrations/20260822000000_purge_request_candidate_sensitive_diagnostics.sql b/crates/aether-data/adapters/postgres/migrations/20260822000000_purge_request_candidate_sensitive_diagnostics.sql deleted file mode 100644 index a631c9686..000000000 --- a/crates/aether-data/adapters/postgres/migrations/20260822000000_purge_request_candidate_sensitive_diagnostics.sql +++ /dev/null @@ -1,16 +0,0 @@ -UPDATE request_candidates -SET - username = NULL, - api_key_name = NULL, - extra_data = NULL, - required_capabilities = NULL, - error_message = NULL, - error_type = NULL, - skip_reason = NULL -WHERE username IS NOT NULL - OR api_key_name IS NOT NULL - OR extra_data IS NOT NULL - OR required_capabilities IS NOT NULL - OR error_message IS NOT NULL - OR error_type IS NOT NULL - OR skip_reason IS NOT NULL; diff --git a/crates/aether-data/adapters/postgres/migrations/20260822010000_purge_usage_and_video_task_sensitive_diagnostics.sql b/crates/aether-data/adapters/postgres/migrations/20260822010000_purge_usage_and_video_task_sensitive_diagnostics.sql deleted file mode 100644 index 580669ba5..000000000 --- a/crates/aether-data/adapters/postgres/migrations/20260822010000_purge_usage_and_video_task_sensitive_diagnostics.sql +++ /dev/null @@ -1,98 +0,0 @@ --- Historical HTTP captures predate the deny-by-default persistence policy and --- may contain credentials or request content. Keep the usage and projected --- billing facts, but remove every legacy copy of the raw exchange. -UPDATE public.usage AS usage_rows -SET error_message = NULL, - error_category = CASE - WHEN usage_rows.error_category IS NULL - OR BTRIM(usage_rows.error_category) = '' THEN NULL - WHEN LOWER(BTRIM(usage_rows.error_category)) IN ( - 'auth', 'cancelled', 'client_error', 'http_error', - 'non_success_status', 'provider_error', 'rate_limit', 'redirect', - 'server_error', 'stream_missing_terminal_event', - 'stream_terminal_error', 'upstream_error' - ) THEN LOWER(BTRIM(usage_rows.error_category)) - ELSE 'other_error' - END, - request_headers = NULL, - request_body = NULL, - provider_request_headers = NULL, - provider_request_body = NULL, - response_headers = NULL, - response_body = NULL, - client_response_headers = NULL, - client_response_body = NULL, - request_body_compressed = NULL, - provider_request_body_compressed = NULL, - response_body_compressed = NULL, - client_response_body_compressed = NULL, - request_metadata = ( - SELECT JSONB_STRIP_NULLS(JSONB_BUILD_OBJECT( - 'plan_usage_reservation_token', reservation.reservation_token, - 'plan_usage_reservation_deferred', CASE - WHEN usage_rows.request_metadata::jsonb - -> 'plan_usage_reservation_deferred' = 'true'::jsonb - THEN TRUE - ELSE NULL - END - ))::json - FROM public.usage_cost_reservations AS reservation - WHERE reservation.state = 'reserved' - AND reservation.reservation_token ~ - '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' - AND reservation.request_id = usage_rows.request_id - AND reservation.subject_id = usage_rows.user_id - AND reservation.reservation_token = usage_rows.request_metadata - ->> 'plan_usage_reservation_token' - LIMIT 1 - ) -WHERE error_message IS NOT NULL - OR error_category IS NOT NULL - OR request_headers IS NOT NULL - OR request_body IS NOT NULL - OR provider_request_headers IS NOT NULL - OR provider_request_body IS NOT NULL - OR response_headers IS NOT NULL - OR response_body IS NOT NULL - OR client_response_headers IS NOT NULL - OR client_response_body IS NOT NULL - OR request_body_compressed IS NOT NULL - OR provider_request_body_compressed IS NOT NULL - OR response_body_compressed IS NOT NULL - OR client_response_body_compressed IS NOT NULL - OR request_metadata IS NOT NULL; - -DELETE FROM public.usage_http_audits; - -DELETE FROM public.usage_body_blobs; - --- All fields needed by billing reads are projected into typed columns on this --- table. The historical JSON documents include rule expressions, catalog --- snapshots, and arbitrary dimensions, so they are not retained. -UPDATE public.usage_settlement_snapshots -SET settlement_snapshot = NULL, - billing_dimensions = NULL -WHERE settlement_snapshot IS NOT NULL - OR billing_dimensions IS NOT NULL; - -UPDATE public.video_tasks -SET original_request_body = NULL, - converted_request_body = NULL, - progress_message = NULL, - error_message = NULL, - request_metadata = NULL, - video_url = NULL, - video_urls = NULL, - thumbnail_url = NULL, - stored_video_path = NULL, - webhook_url = NULL -WHERE original_request_body IS NOT NULL - OR converted_request_body IS NOT NULL - OR progress_message IS NOT NULL - OR error_message IS NOT NULL - OR request_metadata IS NOT NULL - OR video_url IS NOT NULL - OR video_urls IS NOT NULL - OR thumbnail_url IS NOT NULL - OR stored_video_path IS NOT NULL - OR webhook_url IS NOT NULL; diff --git a/crates/aether-data/adapters/postgres/migrations/20260822020000_purge_background_task_sensitive_diagnostics.sql b/crates/aether-data/adapters/postgres/migrations/20260822020000_purge_background_task_sensitive_diagnostics.sql deleted file mode 100644 index db9697057..000000000 --- a/crates/aether-data/adapters/postgres/migrations/20260822020000_purge_background_task_sensitive_diagnostics.sql +++ /dev/null @@ -1,43 +0,0 @@ -UPDATE background_task_runs -SET owner_instance = NULL, - created_by = CASE - WHEN LOWER(BTRIM(created_by)) IN ('admin', 'scheduler', 'system') - THEN LOWER(BTRIM(created_by)) - ELSE NULL - END, - progress_message = NULL, - payload_json = NULL, - result_json = NULL, - error_message = CASE - WHEN status = 'failed' THEN 'background_task_failed' - ELSE NULL - END -WHERE owner_instance IS NOT NULL - OR created_by IS NOT NULL - OR progress_message IS NOT NULL - OR payload_json IS NOT NULL - OR result_json IS NOT NULL - OR error_message IS NOT NULL; - -UPDATE background_task_events -SET event_type = CASE - WHEN event_type IN ( - 'cancel_requested', 'failed', 'queued', 'running', - 'skipped', 'succeeded', 'worker_boot' - ) THEN event_type - ELSE 'unclassified_event' - END, - message = CASE - WHEN event_type IN ( - 'cancel_requested', 'failed', 'queued', 'running', - 'skipped', 'succeeded', 'worker_boot' - ) THEN event_type - ELSE 'unclassified_event' - END, - payload_json = NULL -WHERE message <> event_type - OR payload_json IS NOT NULL - OR event_type NOT IN ( - 'cancel_requested', 'failed', 'queued', 'running', - 'skipped', 'succeeded', 'worker_boot' - ); diff --git a/crates/aether-data/adapters/postgres/migrations/20260827000000_purge_plaintext_stripe_client_secrets.sql b/crates/aether-data/adapters/postgres/migrations/20260827000000_purge_plaintext_stripe_client_secrets.sql deleted file mode 100644 index ef4868f1c..000000000 --- a/crates/aether-data/adapters/postgres/migrations/20260827000000_purge_plaintext_stripe_client_secrets.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Stripe PaymentIntent client secrets are payment capabilities. New writes --- store only encrypted values; legacy plaintext cannot be migrated safely --- without the application encryption key, so remove it fail-closed. -UPDATE payment_orders -SET status = CASE - WHEN LOWER(BTRIM(payment_method)) = 'stripe' AND status = 'pending' - THEN 'expired' - ELSE status - END, - gateway_response = gateway_response - 'client_secret' -WHERE gateway_response ? 'client_secret'; diff --git a/crates/aether-data/adapters/postgres/migrations/20260827010000_purge_legacy_payment_callback_payloads.sql b/crates/aether-data/adapters/postgres/migrations/20260827010000_purge_legacy_payment_callback_payloads.sql deleted file mode 100644 index 978bd16b7..000000000 --- a/crates/aether-data/adapters/postgres/migrations/20260827010000_purge_legacy_payment_callback_payloads.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Callback idempotency uses payload_hash and does not require the raw body. --- Older versions stored provider-controlled payloads that may contain payment --- capabilities or customer PII, so remove those legacy copies fail-closed. -UPDATE payment_callbacks -SET payload = NULL -WHERE payload IS NOT NULL; diff --git a/crates/aether-data/adapters/postgres/migrations/20260827020000_purge_video_task_prompts.sql b/crates/aether-data/adapters/postgres/migrations/20260827020000_purge_video_task_prompts.sql deleted file mode 100644 index b7f2f68aa..000000000 --- a/crates/aether-data/adapters/postgres/migrations/20260827020000_purge_video_task_prompts.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Video prompts are user content and are not required for polling or billing. --- Remove historical copies now that new writes discard them before persistence. -UPDATE public.video_tasks -SET prompt = NULL -WHERE prompt IS NOT NULL; diff --git a/crates/aether-data/adapters/postgres/migrations/20260827030000_purge_identity_oauth_raw_userinfo.sql b/crates/aether-data/adapters/postgres/migrations/20260827030000_purge_identity_oauth_raw_userinfo.sql deleted file mode 100644 index 944fac5db..000000000 --- a/crates/aether-data/adapters/postgres/migrations/20260827030000_purge_identity_oauth_raw_userinfo.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Identity claims required by the application are stored in dedicated columns. --- Remove legacy provider-controlled userinfo JSON, which may contain unrelated --- PII or credentials and is not needed for authentication or account binding. -UPDATE public.user_oauth_links -SET extra_data = NULL -WHERE extra_data IS NOT NULL; diff --git a/crates/aether-data/adapters/postgres/migrations/20260827050000_anonymize_deleted_user_history.sql b/crates/aether-data/adapters/postgres/migrations/20260827050000_anonymize_deleted_user_history.sql index 3b14d10f0..a62944394 100644 --- a/crates/aether-data/adapters/postgres/migrations/20260827050000_anonymize_deleted_user_history.sql +++ b/crates/aether-data/adapters/postgres/migrations/20260827050000_anonymize_deleted_user_history.sql @@ -73,290 +73,4 @@ ALTER TABLE public.usage ALTER TABLE public.stats_daily_api_key DROP CONSTRAINT IF EXISTS stats_daily_api_key_api_key_id_fkey; -UPDATE public.request_candidates AS history -SET username = NULL, api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE public.request_candidates AS history -SET api_key_name = NULL -WHERE api_key_name IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.api_keys WHERE api_keys.id = history.api_key_id - ); - -UPDATE public.video_tasks AS history -SET username = NULL, api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE public.video_tasks AS history -SET api_key_name = NULL -WHERE api_key_name IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.api_keys WHERE api_keys.id = history.api_key_id - ); - -UPDATE public.usage AS history -SET username = NULL, api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE public.usage AS history -SET api_key_name = NULL -WHERE api_key_name IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.api_keys WHERE api_keys.id = history.api_key_id - ); - -UPDATE public.stats_user_daily AS history -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND username IS NOT NULL; - -UPDATE public.stats_user_summary AS history -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND username IS NOT NULL; - -UPDATE public.stats_user_daily_model AS history -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND username IS NOT NULL; - -UPDATE public.stats_user_daily_provider AS history -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND username IS NOT NULL; - -UPDATE public.stats_user_daily_api_format AS history -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND username IS NOT NULL; - -UPDATE public.stats_user_daily_model_provider AS history -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND username IS NOT NULL; - -UPDATE public.stats_user_daily_cost_savings AS history -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND username IS NOT NULL; - -UPDATE public.stats_user_daily_cost_savings_provider AS history -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND username IS NOT NULL; - -UPDATE public.stats_user_daily_cost_savings_model AS history -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND username IS NOT NULL; - -UPDATE public.stats_user_daily_cost_savings_model_provider AS history -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id -) - AND username IS NOT NULL; - -UPDATE public.stats_daily_api_key AS history -SET api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM public.api_keys WHERE api_keys.id = history.api_key_id -) - AND api_key_name IS NOT NULL; - -UPDATE public.user_plan_entitlements AS entitlement -SET status = CASE WHEN status = 'active' THEN 'revoked' ELSE status END, - expires_at = LEAST(expires_at, NOW()), - updated_at = NOW() -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = entitlement.user_id -); - -UPDATE public.wallets AS wallet -SET status = 'disabled', updated_at = NOW() -WHERE (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.api_keys WHERE api_keys.id = wallet.api_key_id - )); - -UPDATE public.user_referrals AS referral -SET invite_code_snapshot = 'deleted-user', - source_json = NULL, - updated_at = NOW() -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = referral.inviter_user_id - ) - OR NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = referral.invitee_user_id - ); - -UPDATE public.referral_rewards AS reward -SET status = CASE - WHEN status IN ('pending', 'failed', 'applying') THEN 'voided' - ELSE status - END, - failure_reason = NULL, - admin_note = NULL, - updated_at = NOW() -WHERE NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = reward.inviter_user_id - ) - OR NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = reward.invitee_user_id - ); - -UPDATE public.audit_logs AS history -SET description = 'deleted user event', - ip_address = NULL, - user_agent = NULL, - event_metadata = NULL, - error_message = NULL -WHERE history.user_id IS NULL - OR NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id - ) - OR (history.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.api_keys WHERE api_keys.id = history.api_key_id - )); - -UPDATE public.wallet_transactions AS history -SET description = NULL -WHERE EXISTS ( - SELECT 1 - FROM public.wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ( - (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) -); - -UPDATE public.wallet_transactions AS history -SET description = NULL -WHERE history.operator_id IS NULL - OR NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.operator_id - ); - -UPDATE public.payment_orders AS history -SET gateway_response = NULL -WHERE history.user_id IS NULL - OR NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id - ) - OR EXISTS ( - SELECT 1 - FROM public.wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ( - (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) - ); - -UPDATE public.payment_callbacks AS history -SET payload = NULL, - error_message = NULL -WHERE EXISTS ( - SELECT 1 - FROM public.payment_orders AS payment_order - LEFT JOIN public.wallets AS wallet ON wallet.id = payment_order.wallet_id - WHERE ( - payment_order.id = history.payment_order_id - OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no) - ) - AND ( - payment_order.user_id IS NULL - OR NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = payment_order.user_id - ) - OR (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) -); - -UPDATE public.refund_requests AS history -SET reason = NULL, - payout_reference = NULL, - payout_proof = NULL, - failure_reason = NULL -WHERE history.user_id IS NULL - OR NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.user_id - ) - OR EXISTS ( - SELECT 1 - FROM public.wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ( - (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM public.api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) - ); - -UPDATE public.referral_rewards AS reward -SET failure_reason = NULL, - admin_note = NULL, - updated_at = NOW() -WHERE reward.admin_operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = reward.admin_operator_id - ); - -UPDATE public.redeem_code_batches AS history -SET description = NULL -WHERE history.created_by IS NULL - OR NOT EXISTS ( - SELECT 1 FROM public.users WHERE users.id = history.created_by - ); +-- Legacy rows are intentionally retained; current writes enforce the policy. diff --git a/crates/aether-data/adapters/postgres/migrations/20260829000000_harden_deleted_user_history_anonymization.sql b/crates/aether-data/adapters/postgres/migrations/20260829000000_harden_deleted_user_history_anonymization.sql deleted file mode 100644 index e804f5a7f..000000000 --- a/crates/aether-data/adapters/postgres/migrations/20260829000000_harden_deleted_user_history_anonymization.sql +++ /dev/null @@ -1,444 +0,0 @@ --- Preserve nullable/system-owned history rows while clearing data tied to --- deleted users or API keys. Raw payment callback payloads are always removed --- because they are not required for idempotency and may contain PII. -UPDATE public.request_candidates AS history -SET username = NULL, api_key_name = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND (history.username IS NOT NULL OR history.api_key_name IS NOT NULL); - -UPDATE public.request_candidates AS history -SET api_key_name = NULL -WHERE history.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM public.api_keys - JOIN public.users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted IS FALSE - WHERE api_keys.id = history.api_key_id - ) - AND history.api_key_name IS NOT NULL; - -UPDATE public.video_tasks AS history -SET username = NULL, api_key_name = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND (history.username IS NOT NULL OR history.api_key_name IS NOT NULL); - -UPDATE public.video_tasks AS history -SET api_key_name = NULL -WHERE history.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM public.api_keys - JOIN public.users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted IS FALSE - WHERE api_keys.id = history.api_key_id - ) - AND history.api_key_name IS NOT NULL; - -UPDATE public.usage AS history -SET username = NULL, api_key_name = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND (history.username IS NOT NULL OR history.api_key_name IS NOT NULL); - -UPDATE public.usage AS history -SET api_key_name = NULL -WHERE history.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM public.api_keys - JOIN public.users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted IS FALSE - WHERE api_keys.id = history.api_key_id - ) - AND history.api_key_name IS NOT NULL; - -UPDATE public.stats_user_daily AS history -SET username = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND history.username IS NOT NULL; - -UPDATE public.stats_user_summary AS history -SET username = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND history.username IS NOT NULL; - -UPDATE public.stats_user_daily_model AS history -SET username = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND history.username IS NOT NULL; - -UPDATE public.stats_user_daily_provider AS history -SET username = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND history.username IS NOT NULL; - -UPDATE public.stats_user_daily_api_format AS history -SET username = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND history.username IS NOT NULL; - -UPDATE public.stats_user_daily_model_provider AS history -SET username = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND history.username IS NOT NULL; - -UPDATE public.stats_user_daily_cost_savings AS history -SET username = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND history.username IS NOT NULL; - -UPDATE public.stats_user_daily_cost_savings_provider AS history -SET username = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND history.username IS NOT NULL; - -UPDATE public.stats_user_daily_cost_savings_model AS history -SET username = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND history.username IS NOT NULL; - -UPDATE public.stats_user_daily_cost_savings_model_provider AS history -SET username = NULL -WHERE history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - ) - AND history.username IS NOT NULL; - -UPDATE public.stats_daily_api_key AS history -SET api_key_name = NULL -WHERE history.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM public.api_keys - JOIN public.users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted IS FALSE - WHERE api_keys.id = history.api_key_id - ) - AND history.api_key_name IS NOT NULL; - -UPDATE public.user_plan_entitlements AS entitlement -SET status = CASE WHEN status = 'active' THEN 'revoked' ELSE status END, - expires_at = LEAST(expires_at, NOW()), - updated_at = NOW() -WHERE NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = entitlement.user_id AND users.is_deleted IS FALSE -); - -UPDATE public.user_referrals AS referral -SET invite_code_snapshot = 'deleted-user', - source_json = NULL, - updated_at = NOW() -WHERE NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = referral.inviter_user_id AND users.is_deleted IS FALSE - ) - OR NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = referral.invitee_user_id AND users.is_deleted IS FALSE - ); - -UPDATE public.referral_rewards AS reward -SET status = CASE - WHEN status IN ('pending', 'failed', 'applying') THEN 'voided' - ELSE status - END, - failure_reason = NULL, - admin_note = NULL, - updated_at = NOW() -WHERE NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = reward.inviter_user_id AND users.is_deleted IS FALSE - ) - OR NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = reward.invitee_user_id AND users.is_deleted IS FALSE - ); - -UPDATE public.referral_rewards AS reward -SET failure_reason = NULL, - admin_note = NULL, - updated_at = NOW() -WHERE reward.admin_operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = reward.admin_operator_id AND users.is_deleted IS FALSE - ); - -UPDATE public.wallets AS wallet -SET status = 'disabled', updated_at = NOW() -WHERE (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = wallet.user_id AND users.is_deleted IS FALSE - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM public.api_keys - JOIN public.users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted IS FALSE - WHERE api_keys.id = wallet.api_key_id - )); - -UPDATE public.audit_logs AS history -SET description = 'deleted user event', - ip_address = NULL, - user_agent = NULL, - event_metadata = NULL, - error_message = NULL -WHERE (history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - )) - OR (history.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM public.api_keys - JOIN public.users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted IS FALSE - WHERE api_keys.id = history.api_key_id - )); - -UPDATE public.wallet_transactions AS history -SET description = NULL -WHERE EXISTS ( - SELECT 1 - FROM public.wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = wallet.user_id AND users.is_deleted IS FALSE - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM public.api_keys - JOIN public.users AS api_key_owner - ON api_key_owner.id = api_keys.user_id - AND api_key_owner.is_deleted IS FALSE - WHERE api_keys.id = wallet.api_key_id - ))) -) - OR NOT EXISTS ( - SELECT 1 FROM public.wallets AS wallet WHERE wallet.id = history.wallet_id - ) - OR (history.operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.operator_id AND users.is_deleted IS FALSE - )); - -UPDATE public.payment_orders AS history -SET gateway_response = NULL -WHERE (history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - )) - OR EXISTS ( - SELECT 1 - FROM public.wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = wallet.user_id AND users.is_deleted IS FALSE - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM public.api_keys - JOIN public.users AS api_key_owner - ON api_key_owner.id = api_keys.user_id - AND api_key_owner.is_deleted IS FALSE - WHERE api_keys.id = wallet.api_key_id - ))) -) - OR NOT EXISTS ( - SELECT 1 FROM public.wallets AS wallet WHERE wallet.id = history.wallet_id - ); - --- Raw provider payloads are never needed for idempotency and must be purged --- even when an old callback cannot be linked back to an order. -UPDATE public.payment_callbacks -SET payload = NULL -WHERE payload IS NOT NULL; - -UPDATE public.payment_callbacks AS history -SET error_message = NULL -WHERE history.error_message IS NOT NULL - AND ( - NOT EXISTS ( - SELECT 1 - FROM public.payment_orders AS payment_order - WHERE payment_order.id = history.payment_order_id - OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no) - ) - OR EXISTS ( - SELECT 1 - FROM public.payment_orders AS payment_order - LEFT JOIN public.wallets AS wallet ON wallet.id = payment_order.wallet_id - WHERE (payment_order.id = history.payment_order_id - OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no)) - AND ((payment_order.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = payment_order.user_id AND users.is_deleted IS FALSE - )) - OR wallet.id IS NULL - OR (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = wallet.user_id AND users.is_deleted IS FALSE - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM public.api_keys - JOIN public.users AS api_key_owner - ON api_key_owner.id = api_keys.user_id - AND api_key_owner.is_deleted IS FALSE - WHERE api_keys.id = wallet.api_key_id - ))) - ) - ); - -UPDATE public.refund_requests AS history -SET reason = NULL, payout_reference = NULL, payout_proof = NULL, failure_reason = NULL -WHERE (history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.user_id AND users.is_deleted IS FALSE - )) - OR EXISTS ( - SELECT 1 - FROM public.wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = wallet.user_id AND users.is_deleted IS FALSE - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM public.api_keys - JOIN public.users AS api_key_owner - ON api_key_owner.id = api_keys.user_id - AND api_key_owner.is_deleted IS FALSE - WHERE api_keys.id = wallet.api_key_id - ))) -) - OR NOT EXISTS ( - SELECT 1 FROM public.wallets AS wallet WHERE wallet.id = history.wallet_id - ) - OR (history.requested_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.requested_by AND users.is_deleted IS FALSE - )) - OR (history.approved_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.approved_by AND users.is_deleted IS FALSE - )) - OR (history.processed_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.processed_by AND users.is_deleted IS FALSE - )); - -UPDATE public.refund_requests AS history -SET requested_by = NULL -WHERE history.requested_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.requested_by AND users.is_deleted IS FALSE - ); - -UPDATE public.refund_requests AS history -SET approved_by = NULL -WHERE history.approved_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.approved_by AND users.is_deleted IS FALSE - ); - -UPDATE public.refund_requests AS history -SET processed_by = NULL -WHERE history.processed_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.processed_by AND users.is_deleted IS FALSE - ); - -UPDATE public.redeem_code_batches AS history -SET description = NULL -WHERE history.created_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM public.users - WHERE users.id = history.created_by AND users.is_deleted IS FALSE - ); diff --git a/crates/aether-data/adapters/postgres/migrations/20260903010000_reset_legacy_oauth_email_verification.sql b/crates/aether-data/adapters/postgres/migrations/20260903010000_reset_legacy_oauth_email_verification.sql deleted file mode 100644 index 8ad0cd6f9..000000000 --- a/crates/aether-data/adapters/postgres/migrations/20260903010000_reset_legacy_oauth_email_verification.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Older gateway releases marked every OAuth email as verified, even when the --- provider did not make a trustworthy verification assertion. There is no --- persisted provenance that can distinguish those rows, so fail closed. A --- later OAuth login may re-verify the same normalized email from a trusted --- provider assertion. -UPDATE public.users -SET email_verified = FALSE, - updated_at = CURRENT_TIMESTAMP -WHERE LOWER(TRIM(auth_source::text)) = 'oauth' - AND email_verified = TRUE; diff --git a/crates/aether-data/adapters/sqlite/migrations/20260822000000_purge_request_candidate_sensitive_diagnostics.sql b/crates/aether-data/adapters/sqlite/migrations/20260822000000_purge_request_candidate_sensitive_diagnostics.sql deleted file mode 100644 index a631c9686..000000000 --- a/crates/aether-data/adapters/sqlite/migrations/20260822000000_purge_request_candidate_sensitive_diagnostics.sql +++ /dev/null @@ -1,16 +0,0 @@ -UPDATE request_candidates -SET - username = NULL, - api_key_name = NULL, - extra_data = NULL, - required_capabilities = NULL, - error_message = NULL, - error_type = NULL, - skip_reason = NULL -WHERE username IS NOT NULL - OR api_key_name IS NOT NULL - OR extra_data IS NOT NULL - OR required_capabilities IS NOT NULL - OR error_message IS NOT NULL - OR error_type IS NOT NULL - OR skip_reason IS NOT NULL; diff --git a/crates/aether-data/adapters/sqlite/migrations/20260822010000_purge_usage_and_video_task_sensitive_diagnostics.sql b/crates/aether-data/adapters/sqlite/migrations/20260822010000_purge_usage_and_video_task_sensitive_diagnostics.sql deleted file mode 100644 index ec170a4c7..000000000 --- a/crates/aether-data/adapters/sqlite/migrations/20260822010000_purge_usage_and_video_task_sensitive_diagnostics.sql +++ /dev/null @@ -1,121 +0,0 @@ --- Historical HTTP captures predate the deny-by-default persistence policy and --- may contain credentials or request content. Keep the usage and projected --- billing facts, but remove every legacy copy of the raw exchange. -UPDATE "usage" -SET error_message = NULL, - error_category = CASE - WHEN error_category IS NULL OR TRIM(error_category) = '' THEN NULL - WHEN LOWER(TRIM(error_category)) IN ( - 'auth', 'cancelled', 'client_error', 'http_error', - 'non_success_status', 'provider_error', 'rate_limit', 'redirect', - 'server_error', 'stream_missing_terminal_event', - 'stream_terminal_error', 'upstream_error' - ) THEN LOWER(TRIM(error_category)) - ELSE 'other_error' - END, - request_headers = NULL, - request_body = NULL, - provider_request_headers = NULL, - provider_request_body = NULL, - response_headers = NULL, - response_body = NULL, - client_response_headers = NULL, - client_response_body = NULL, - request_body_compressed = NULL, - provider_request_body_compressed = NULL, - response_body_compressed = NULL, - client_response_body_compressed = NULL, - request_metadata = ( - SELECT CASE - WHEN JSON_TYPE( - CASE - WHEN JSON_VALID("usage".request_metadata) - THEN "usage".request_metadata - ELSE '{}' - END, - '$.plan_usage_reservation_deferred' - ) = 'true' - THEN JSON_SET( - JSON_OBJECT( - 'plan_usage_reservation_token', reservation.reservation_token - ), - '$.plan_usage_reservation_deferred', - JSON('true') - ) - ELSE JSON_OBJECT( - 'plan_usage_reservation_token', reservation.reservation_token - ) - END - FROM usage_cost_reservations AS reservation - WHERE reservation.state = 'reserved' - AND LENGTH(reservation.reservation_token) = 36 - AND reservation.reservation_token = LOWER(reservation.reservation_token) - AND SUBSTR(reservation.reservation_token, 9, 1) = '-' - AND SUBSTR(reservation.reservation_token, 14, 1) = '-' - AND SUBSTR(reservation.reservation_token, 19, 1) = '-' - AND SUBSTR(reservation.reservation_token, 24, 1) = '-' - AND LENGTH(REPLACE(reservation.reservation_token, '-', '')) = 32 - AND REPLACE(reservation.reservation_token, '-', '') - NOT GLOB '*[^0-9a-f]*' - AND reservation.request_id = "usage".request_id - AND reservation.subject_id = "usage".user_id - AND reservation.reservation_token = JSON_EXTRACT( - CASE - WHEN JSON_VALID("usage".request_metadata) - THEN "usage".request_metadata - ELSE '{}' - END, - '$.plan_usage_reservation_token' - ) - LIMIT 1 - ) -WHERE error_message IS NOT NULL - OR error_category IS NOT NULL - OR request_headers IS NOT NULL - OR request_body IS NOT NULL - OR provider_request_headers IS NOT NULL - OR provider_request_body IS NOT NULL - OR response_headers IS NOT NULL - OR response_body IS NOT NULL - OR client_response_headers IS NOT NULL - OR client_response_body IS NOT NULL - OR request_body_compressed IS NOT NULL - OR provider_request_body_compressed IS NOT NULL - OR response_body_compressed IS NOT NULL - OR client_response_body_compressed IS NOT NULL - OR request_metadata IS NOT NULL; - -DELETE FROM usage_http_audits; - -DELETE FROM usage_body_blobs; - --- All fields needed by billing reads are projected into typed columns on this --- table. The historical JSON documents include rule expressions, catalog --- snapshots, and arbitrary dimensions, so they are not retained. -UPDATE usage_settlement_snapshots -SET settlement_snapshot = NULL, - billing_dimensions = NULL -WHERE settlement_snapshot IS NOT NULL - OR billing_dimensions IS NOT NULL; - -UPDATE video_tasks -SET original_request_body = NULL, - converted_request_body = NULL, - progress_message = NULL, - error_message = NULL, - request_metadata = NULL, - video_url = NULL, - video_urls = NULL, - thumbnail_url = NULL, - stored_video_path = NULL, - webhook_url = NULL -WHERE original_request_body IS NOT NULL - OR converted_request_body IS NOT NULL - OR progress_message IS NOT NULL - OR error_message IS NOT NULL - OR request_metadata IS NOT NULL - OR video_url IS NOT NULL - OR video_urls IS NOT NULL - OR thumbnail_url IS NOT NULL - OR stored_video_path IS NOT NULL - OR webhook_url IS NOT NULL; diff --git a/crates/aether-data/adapters/sqlite/migrations/20260822020000_purge_background_task_sensitive_diagnostics.sql b/crates/aether-data/adapters/sqlite/migrations/20260822020000_purge_background_task_sensitive_diagnostics.sql deleted file mode 100644 index d3738a5e7..000000000 --- a/crates/aether-data/adapters/sqlite/migrations/20260822020000_purge_background_task_sensitive_diagnostics.sql +++ /dev/null @@ -1,43 +0,0 @@ -UPDATE background_task_runs -SET owner_instance = NULL, - created_by = CASE - WHEN LOWER(TRIM(created_by)) IN ('admin', 'scheduler', 'system') - THEN LOWER(TRIM(created_by)) - ELSE NULL - END, - progress_message = NULL, - payload_json = NULL, - result_json = NULL, - error_message = CASE - WHEN status = 'failed' THEN 'background_task_failed' - ELSE NULL - END -WHERE owner_instance IS NOT NULL - OR created_by IS NOT NULL - OR progress_message IS NOT NULL - OR payload_json IS NOT NULL - OR result_json IS NOT NULL - OR error_message IS NOT NULL; - -UPDATE background_task_events -SET event_type = CASE - WHEN event_type IN ( - 'cancel_requested', 'failed', 'queued', 'running', - 'skipped', 'succeeded', 'worker_boot' - ) THEN event_type - ELSE 'unclassified_event' - END, - message = CASE - WHEN event_type IN ( - 'cancel_requested', 'failed', 'queued', 'running', - 'skipped', 'succeeded', 'worker_boot' - ) THEN event_type - ELSE 'unclassified_event' - END, - payload_json = NULL -WHERE message <> event_type - OR payload_json IS NOT NULL - OR event_type NOT IN ( - 'cancel_requested', 'failed', 'queued', 'running', - 'skipped', 'succeeded', 'worker_boot' - ); diff --git a/crates/aether-data/adapters/sqlite/migrations/20260827000000_purge_plaintext_stripe_client_secrets.sql b/crates/aether-data/adapters/sqlite/migrations/20260827000000_purge_plaintext_stripe_client_secrets.sql deleted file mode 100644 index 02ccb37c2..000000000 --- a/crates/aether-data/adapters/sqlite/migrations/20260827000000_purge_plaintext_stripe_client_secrets.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Stripe PaymentIntent client secrets are payment capabilities. New writes --- store only encrypted values; legacy plaintext cannot be migrated safely --- without the application encryption key, so remove it fail-closed. -UPDATE payment_orders -SET status = CASE - WHEN LOWER(TRIM(payment_method)) = 'stripe' AND status = 'pending' - THEN 'expired' - ELSE status - END, - gateway_response = JSON_REMOVE(gateway_response, '$.client_secret') -WHERE gateway_response IS NOT NULL - AND JSON_VALID(gateway_response) - AND JSON_TYPE(gateway_response, '$.client_secret') IS NOT NULL; diff --git a/crates/aether-data/adapters/sqlite/migrations/20260827010000_purge_legacy_payment_callback_payloads.sql b/crates/aether-data/adapters/sqlite/migrations/20260827010000_purge_legacy_payment_callback_payloads.sql deleted file mode 100644 index 978bd16b7..000000000 --- a/crates/aether-data/adapters/sqlite/migrations/20260827010000_purge_legacy_payment_callback_payloads.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Callback idempotency uses payload_hash and does not require the raw body. --- Older versions stored provider-controlled payloads that may contain payment --- capabilities or customer PII, so remove those legacy copies fail-closed. -UPDATE payment_callbacks -SET payload = NULL -WHERE payload IS NOT NULL; diff --git a/crates/aether-data/adapters/sqlite/migrations/20260827020000_purge_video_task_prompts.sql b/crates/aether-data/adapters/sqlite/migrations/20260827020000_purge_video_task_prompts.sql deleted file mode 100644 index 5a13072b8..000000000 --- a/crates/aether-data/adapters/sqlite/migrations/20260827020000_purge_video_task_prompts.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Video prompts are user content and are not required for polling or billing. --- Remove historical copies now that new writes discard them before persistence. -UPDATE video_tasks -SET prompt = NULL -WHERE prompt IS NOT NULL; diff --git a/crates/aether-data/adapters/sqlite/migrations/20260827030000_purge_identity_oauth_raw_userinfo.sql b/crates/aether-data/adapters/sqlite/migrations/20260827030000_purge_identity_oauth_raw_userinfo.sql deleted file mode 100644 index abe3746a2..000000000 --- a/crates/aether-data/adapters/sqlite/migrations/20260827030000_purge_identity_oauth_raw_userinfo.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Identity claims required by the application are stored in dedicated columns. --- Remove legacy provider-controlled userinfo JSON, which may contain unrelated --- PII or credentials and is not needed for authentication or account binding. -UPDATE user_oauth_links -SET extra_data = NULL -WHERE extra_data IS NOT NULL; diff --git a/crates/aether-data/adapters/sqlite/migrations/20260827050000_anonymize_deleted_user_history.sql b/crates/aether-data/adapters/sqlite/migrations/20260827050000_anonymize_deleted_user_history.sql index ace3e78a7..05d81afcc 100644 --- a/crates/aether-data/adapters/sqlite/migrations/20260827050000_anonymize_deleted_user_history.sql +++ b/crates/aether-data/adapters/sqlite/migrations/20260827050000_anonymize_deleted_user_history.sql @@ -1,292 +1,5 @@ -UPDATE request_candidates -SET username = NULL, api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = request_candidates.user_id -) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE request_candidates -SET api_key_name = NULL -WHERE api_key_name IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = request_candidates.api_key_id - ); - -UPDATE video_tasks -SET username = NULL, api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = video_tasks.user_id -) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE video_tasks -SET api_key_name = NULL -WHERE api_key_name IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = video_tasks.api_key_id - ); - -UPDATE usage -SET username = NULL, api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = usage.user_id -) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE usage -SET api_key_name = NULL -WHERE api_key_name IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = usage.api_key_id - ); - -UPDATE stats_user_daily -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_summary -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_summary.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_model -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_model.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_provider -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_provider.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_api_format -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_api_format.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_model_provider -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_model_provider.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_provider -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings_provider.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_model -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings_model.user_id -) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_model_provider -SET username = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings_model_provider.user_id -) - AND username IS NOT NULL; - -UPDATE stats_daily_api_key -SET api_key_name = NULL -WHERE NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = stats_daily_api_key.api_key_id -) - AND api_key_name IS NOT NULL; - -UPDATE user_plan_entitlements AS entitlement -SET status = CASE WHEN status = 'active' THEN 'revoked' ELSE status END, - expires_at = MIN(expires_at, CAST(strftime('%s', 'now') AS INTEGER)), - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = entitlement.user_id -); - -UPDATE wallets AS wallet -SET status = 'disabled', - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id - )); - -UPDATE user_referrals AS referral -SET invite_code_snapshot = 'deleted-user', - source_json = NULL, - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = referral.inviter_user_id - ) - OR NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = referral.invitee_user_id - ); - -UPDATE referral_rewards AS reward -SET status = CASE - WHEN status IN ('pending', 'failed', 'applying') THEN 'voided' - ELSE status - END, - failure_reason = NULL, - admin_note = NULL, - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = reward.inviter_user_id - ) - OR NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = reward.invitee_user_id - ); - -UPDATE audit_logs AS history -SET description = 'deleted user event', - ip_address = NULL, - user_agent = NULL, - event_metadata = NULL, - error_message = NULL -WHERE (history.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.user_id - )) - OR (history.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = history.api_key_id - )); - -UPDATE wallet_transactions AS history -SET description = NULL -WHERE EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ( - (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) -); - -UPDATE wallet_transactions AS history -SET description = NULL -WHERE history.operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.operator_id - ); - -UPDATE payment_orders AS history -SET gateway_response = NULL -WHERE (history.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.user_id - )) - OR EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ( - (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) -); - -UPDATE payment_callbacks AS history -SET payload = NULL, - error_message = NULL -WHERE EXISTS ( - SELECT 1 - FROM payment_orders AS payment_order - LEFT JOIN wallets AS wallet ON wallet.id = payment_order.wallet_id - WHERE ( - payment_order.id = history.payment_order_id - OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no) - ) - AND ( - (payment_order.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = payment_order.user_id - )) - OR (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) -); - -UPDATE refund_requests AS history -SET reason = NULL, - payout_reference = NULL, - payout_proof = NULL, - failure_reason = NULL -WHERE (history.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.user_id - )) - OR EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ( - (wallet.user_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = wallet.user_id - )) - OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id - )) - ) - ) - OR (history.requested_by IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.requested_by - )) - OR (history.approved_by IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.approved_by - )) - OR (history.processed_by IS NOT NULL AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.processed_by - )); - -UPDATE referral_rewards AS reward -SET failure_reason = NULL, - admin_note = NULL, - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE reward.admin_operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = reward.admin_operator_id - ); - -UPDATE redeem_code_batches AS history -SET description = NULL -WHERE history.created_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users WHERE users.id = history.created_by - ); - +-- Retain legacy row contents. This migration only removes user foreign keys +-- required by the current account-deletion flow. ALTER TABLE user_plan_entitlements RENAME TO _aether_user_plan_entitlements_with_user_fk; DROP INDEX IF EXISTS idx_user_plan_entitlements_user_active; DROP INDEX IF EXISTS idx_user_plan_entitlements_order; diff --git a/crates/aether-data/adapters/sqlite/migrations/20260829000000_harden_deleted_user_history_anonymization.sql b/crates/aether-data/adapters/sqlite/migrations/20260829000000_harden_deleted_user_history_anonymization.sql deleted file mode 100644 index 9840421a9..000000000 --- a/crates/aether-data/adapters/sqlite/migrations/20260829000000_harden_deleted_user_history_anonymization.sql +++ /dev/null @@ -1,449 +0,0 @@ --- Keep nullable/system-owned history rows intact while clearing values tied to --- deleted users or API keys. Raw payment callback payloads are always removed --- because they are not required for idempotency and may contain PII. -UPDATE request_candidates -SET username = NULL, api_key_name = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = request_candidates.user_id AND users.is_deleted = 0 - ) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE request_candidates -SET api_key_name = NULL -WHERE api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = request_candidates.api_key_id - ) - AND api_key_name IS NOT NULL; - -UPDATE video_tasks -SET username = NULL, api_key_name = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = video_tasks.user_id AND users.is_deleted = 0 - ) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE video_tasks -SET api_key_name = NULL -WHERE api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = video_tasks.api_key_id - ) - AND api_key_name IS NOT NULL; - -UPDATE `usage` -SET username = NULL, api_key_name = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = `usage`.user_id AND users.is_deleted = 0 - ) - AND (username IS NOT NULL OR api_key_name IS NOT NULL); - -UPDATE `usage` -SET api_key_name = NULL -WHERE api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = `usage`.api_key_id - ) - AND api_key_name IS NOT NULL; - -UPDATE stats_user_daily -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_summary -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_summary.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_model -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_model.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_provider -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_provider.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_api_format -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_api_format.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_model_provider -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_model_provider.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_cost_savings.user_id AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_provider -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_cost_savings_provider.user_id - AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_model -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = stats_user_daily_cost_savings_model.user_id - AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_user_daily_cost_savings_model_provider -SET username = NULL -WHERE user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM users - WHERE users.id = stats_user_daily_cost_savings_model_provider.user_id - AND users.is_deleted = 0 - ) - AND username IS NOT NULL; - -UPDATE stats_daily_api_key -SET api_key_name = NULL -WHERE api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = stats_daily_api_key.api_key_id - ) - AND api_key_name IS NOT NULL; - -UPDATE user_plan_entitlements AS entitlement -SET status = CASE WHEN status = 'active' THEN 'revoked' ELSE status END, - expires_at = MIN(expires_at, CAST(strftime('%s', 'now') AS INTEGER)), - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = entitlement.user_id AND users.is_deleted = 0 -); - -UPDATE user_referrals AS referral -SET invite_code_snapshot = 'deleted-user', - source_json = NULL, - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = referral.inviter_user_id AND users.is_deleted = 0 - ) - OR NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = referral.invitee_user_id AND users.is_deleted = 0 - ); - -UPDATE referral_rewards AS reward -SET status = CASE - WHEN status IN ('pending', 'failed', 'applying') THEN 'voided' - ELSE status - END, - failure_reason = NULL, - admin_note = NULL, - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = reward.inviter_user_id AND users.is_deleted = 0 - ) - OR NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = reward.invitee_user_id AND users.is_deleted = 0 - ); - -UPDATE referral_rewards AS reward -SET failure_reason = NULL, - admin_note = NULL, - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE reward.admin_operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = reward.admin_operator_id AND users.is_deleted = 0 - ); - -UPDATE wallets AS wallet -SET status = 'disabled', - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = wallet.user_id AND users.is_deleted = 0 - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = wallet.api_key_id - )); - -UPDATE audit_logs AS history -SET description = 'deleted user event', - ip_address = NULL, - user_agent = NULL, - event_metadata = NULL, - error_message = NULL -WHERE (history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.user_id AND users.is_deleted = 0 - )) - OR (history.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = history.api_key_id - )); - -UPDATE wallet_transactions AS history -SET description = NULL -WHERE EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = wallet.user_id AND users.is_deleted = 0 - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = wallet.api_key_id - ))) -) - OR NOT EXISTS ( - SELECT 1 FROM wallets AS wallet WHERE wallet.id = history.wallet_id - ) - OR (history.operator_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.operator_id AND users.is_deleted = 0 - )); - -UPDATE payment_orders AS history -SET gateway_response = NULL -WHERE (history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.user_id AND users.is_deleted = 0 - )) - OR EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = wallet.user_id AND users.is_deleted = 0 - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = wallet.api_key_id - ))) -) - OR NOT EXISTS ( - SELECT 1 FROM wallets AS wallet WHERE wallet.id = history.wallet_id - ); - --- Raw provider payloads are never needed for idempotency and must be purged --- even when an old callback cannot be linked back to an order. -UPDATE payment_callbacks -SET payload = NULL -WHERE payload IS NOT NULL; - -UPDATE payment_callbacks AS history -SET error_message = NULL -WHERE history.error_message IS NOT NULL - AND ( - NOT EXISTS ( - SELECT 1 - FROM payment_orders AS payment_order - WHERE payment_order.id = history.payment_order_id - OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no) - ) - OR EXISTS ( - SELECT 1 - FROM payment_orders AS payment_order - LEFT JOIN wallets AS wallet ON wallet.id = payment_order.wallet_id - WHERE (payment_order.id = history.payment_order_id - OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no)) - AND ((payment_order.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = payment_order.user_id AND users.is_deleted = 0 - )) - OR wallet.id IS NULL - OR (wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = wallet.user_id AND users.is_deleted = 0 - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id - AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = wallet.api_key_id - ))) - ) - ); - -UPDATE refund_requests AS history -SET reason = NULL, - payout_reference = NULL, - payout_proof = NULL, - failure_reason = NULL -WHERE (history.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.user_id AND users.is_deleted = 0 - )) - OR EXISTS ( - SELECT 1 - FROM wallets AS wallet - WHERE wallet.id = history.wallet_id - AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL) - OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL) - OR (wallet.user_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = wallet.user_id AND users.is_deleted = 0 - )) - OR (wallet.api_key_id IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM api_keys - JOIN users AS api_key_owner - ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0 - WHERE api_keys.id = wallet.api_key_id - ))) -) - OR NOT EXISTS ( - SELECT 1 FROM wallets AS wallet WHERE wallet.id = history.wallet_id - ) - OR (history.requested_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.requested_by AND users.is_deleted = 0 - )) - OR (history.approved_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.approved_by AND users.is_deleted = 0 - )) - OR (history.processed_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.processed_by AND users.is_deleted = 0 - )); - -UPDATE redeem_code_batches AS history -SET description = NULL -WHERE history.created_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = history.created_by AND users.is_deleted = 0 - ); - -UPDATE refund_requests -SET requested_by = NULL -WHERE requested_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = refund_requests.requested_by AND users.is_deleted = 0 - ); - -UPDATE refund_requests -SET approved_by = NULL -WHERE approved_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = refund_requests.approved_by AND users.is_deleted = 0 - ); - -UPDATE refund_requests -SET processed_by = NULL -WHERE processed_by IS NOT NULL - AND NOT EXISTS ( - SELECT 1 FROM users - WHERE users.id = refund_requests.processed_by AND users.is_deleted = 0 - ); diff --git a/crates/aether-data/adapters/sqlite/migrations/20260903010000_reset_legacy_oauth_email_verification.sql b/crates/aether-data/adapters/sqlite/migrations/20260903010000_reset_legacy_oauth_email_verification.sql deleted file mode 100644 index 831e8256d..000000000 --- a/crates/aether-data/adapters/sqlite/migrations/20260903010000_reset_legacy_oauth_email_verification.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Older gateway releases marked every OAuth email as verified without --- retaining verification provenance. Reset those claims conservatively; a --- later trusted OAuth assertion for the same normalized email can upgrade it. -UPDATE users -SET email_verified = 0, - updated_at = CAST(strftime('%s', 'now') AS INTEGER) -WHERE LOWER(TRIM(auth_source)) = 'oauth' - AND email_verified = 1; diff --git a/crates/aether-data/runtime/src/lifecycle/bootstrap/postgres.rs b/crates/aether-data/runtime/src/lifecycle/bootstrap/postgres.rs index 1e486b223..ea405239a 100644 --- a/crates/aether-data/runtime/src/lifecycle/bootstrap/postgres.rs +++ b/crates/aether-data/runtime/src/lifecycle/bootstrap/postgres.rs @@ -7,9 +7,9 @@ use tracing::info; // Generated by build.rs from schema/bootstrap/postgres. pub(crate) static EMPTY_DATABASE_SNAPSHOT_SQL: &str = include_str!(concat!(env!("OUT_DIR"), "/empty_database_snapshot.sql")); -// Keep data migrations after the privacy/security frontier executable on a -// fresh database. The bootstrap SQL is schema-only; stamping later data -// migrations would skip required cleanup/anonymization work. +// Keep post-snapshot migrations executable on a fresh database so required +// schema changes still run. Compatibility migrations after this frontier are +// explicit no-ops and must not rewrite legacy rows. pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260821130000; const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#" diff --git a/crates/aether-data/runtime/src/lifecycle/migrate/tests.rs b/crates/aether-data/runtime/src/lifecycle/migrate/tests.rs index 68f380f5d..19413908a 100644 --- a/crates/aether-data/runtime/src/lifecycle/migrate/tests.rs +++ b/crates/aether-data/runtime/src/lifecycle/migrate/tests.rs @@ -7,7 +7,7 @@ use std::time::{Duration, Instant}; use sqlx::{ migrate::{AppliedMigration, Migrate}, - query, query_scalar, Connection, PgConnection, PgPool, Row, SqlitePool, + query, query_scalar, Connection, PgConnection, PgPool, SqlitePool, }; use aether_data_contracts::repository::{ @@ -1031,59 +1031,40 @@ fn worker_boot_cleanup_migration_is_enabled_for_every_driver() { } } -#[test] -fn request_candidate_sensitive_diagnostic_purge_is_enabled_for_every_driver() { - const VERSION: i64 = 20260822000000; +const UNPUBLISHED_LEGACY_DATA_REWRITE_MIGRATION_VERSIONS: &[i64] = &[ + 20260822000000, + 20260822010000, + 20260822020000, + 20260827000000, + 20260827010000, + 20260827020000, + 20260827030000, + 20260829000000, + 20260903010000, +]; +#[test] +fn unpublished_legacy_data_rewrite_migrations_are_absent_for_every_driver() { for (driver, migrator) in [ ("postgres", &POSTGRES_MIGRATOR), ("mysql", &super::mysql::MIGRATOR), ("sqlite", &super::sqlite::MIGRATOR), ] { - let migration = migrator - .iter() - .find(|migration| migration.version == VERSION) - .unwrap_or_else(|| { - panic!("{driver} request candidate diagnostic purge should be embedded") - }); - let sql = migration.sql.as_ref(); - - for required in [ - "UPDATE request_candidates", - "username = NULL", - "api_key_name = NULL", - "extra_data = NULL", - "required_capabilities = NULL", - "error_message = NULL", - "error_type = NULL", - "skip_reason = NULL", - ] { + for version in UNPUBLISHED_LEGACY_DATA_REWRITE_MIGRATION_VERSIONS { assert!( - sql.contains(required), - "{driver} request candidate diagnostic purge is missing {required}" + migrator + .iter() + .all(|migration| migration.version != *version), + "{driver} must not embed unpublished legacy rewrite migration {version}" ); } } } #[test] -fn deleted_user_history_anonymization_is_enabled_for_every_driver() { +fn deleted_user_history_schema_decoupling_does_not_rewrite_history() { const VERSION: i64 = 20260827050000; - const HISTORY_TABLES: &[&str] = &[ - "request_candidates", - "video_tasks", - "usage", - "stats_user_daily", - "stats_user_summary", - "stats_user_daily_model", - "stats_user_daily_provider", - "stats_user_daily_api_format", - "stats_user_daily_model_provider", - "stats_user_daily_cost_savings", - "stats_user_daily_cost_savings_provider", - "stats_user_daily_cost_savings_model", - "stats_user_daily_cost_savings_model_provider", - ]; + for (driver, migrator) in [ ("postgres", &POSTGRES_MIGRATOR), ("mysql", &super::mysql::MIGRATOR), @@ -1092,61 +1073,21 @@ fn deleted_user_history_anonymization_is_enabled_for_every_driver() { let migration = migrator .iter() .find(|migration| migration.version == VERSION) - .unwrap_or_else(|| panic!("{driver} user-history anonymization should be embedded")); - let sql = migration.sql.as_ref(); - - for table in HISTORY_TABLES { + .unwrap_or_else(|| panic!("{driver} user-history schema migration should be embedded")); + let sql = migration + .sql + .lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with("--")) + .collect::>() + .join("\n") + .to_ascii_uppercase(); + for history_rewrite in ["UPDATE ", "DELETE ", "TRUNCATE ", "REPLACE ", "MERGE "] { assert!( - sql.contains(&format!("UPDATE {table}")) - || sql.contains(&format!("UPDATE `{table}`")) - || sql.contains(&format!("UPDATE public.{table}")), - "{driver} user-history anonymization is missing {table}" - ); - } - assert_eq!( - sql.matches("SET username = NULL").count(), - HISTORY_TABLES.len(), - "{driver} must erase every username snapshot" - ); - assert!( - sql.matches("api_key_name = NULL").count() >= 4, - "{driver} must erase API key names from request, video, usage, and API key aggregates" - ); - assert!( - sql.matches("WHERE NOT EXISTS").count() >= HISTORY_TABLES.len() + 1, - "{driver} migration must leave existing users untouched" - ); - assert!( - sql.contains("UPDATE stats_daily_api_key") - || sql.contains("UPDATE public.stats_daily_api_key"), - "{driver} migration must anonymize orphaned API key aggregate names" - ); - for fact_table in [ - "user_plan_entitlements", - "wallets", - "user_referrals", - "referral_rewards", - ] { - assert!( - sql.contains(&format!("UPDATE {fact_table}")) - || sql.contains(&format!("UPDATE public.{fact_table}")), - "{driver} deleted-user fact migration is missing {fact_table}" - ); - } - for required in [ - "THEN 'revoked'", - "status = 'disabled'", - "THEN 'voided'", - "source_json = NULL", - "failure_reason = NULL", - "admin_note = NULL", - "payment_callbacks", - "payload = NULL", - "error_message = NULL", - ] { - assert!( - sql.contains(required), - "{driver} deleted-user fact migration is missing {required}" + !sql + .split(';') + .any(|statement| statement.trim_start().starts_with(history_rewrite)), + "{driver} user-history schema migration rewrites legacy rows with {history_rewrite}" ); } } @@ -1154,7 +1095,7 @@ fn deleted_user_history_anonymization_is_enabled_for_every_driver() { let postgres_migration = POSTGRES_MIGRATOR .iter() .find(|migration| migration.version == VERSION) - .expect("postgres user-history anonymization should be embedded"); + .expect("postgres user-history schema migration should be embedded"); for constraint in [ "request_candidates_user_id_fkey", "video_tasks_user_id_fkey", @@ -1195,320 +1136,41 @@ fn deleted_user_history_anonymization_is_enabled_for_every_driver() { "postgres migration must decouple {constraint}" ); } -} -#[test] -fn deleted_user_history_anonymization_remediation_is_enabled_for_every_driver() { - const VERSION: i64 = 20260829000000; - const HISTORY_TABLES: &[&str] = &[ - "request_candidates", - "video_tasks", - "usage", - "stats_user_daily", - "stats_user_summary", - "stats_user_daily_model", - "stats_user_daily_provider", - "stats_user_daily_api_format", - "stats_user_daily_model_provider", - "stats_user_daily_cost_savings", - "stats_user_daily_cost_savings_provider", - "stats_user_daily_cost_savings_model", - "stats_user_daily_cost_savings_model_provider", - ]; - const OWNER_HISTORY_TABLES: &[&str] = &[ - "wallets", - "audit_logs", - "wallet_transactions", - "payment_orders", - "payment_callbacks", - "refund_requests", - "redeem_code_batches", - ]; - - for (driver, migrator) in [ - ("postgres", &POSTGRES_MIGRATOR), - ("mysql", &super::mysql::MIGRATOR), - ("sqlite", &super::sqlite::MIGRATOR), - ] { - let migration = migrator - .iter() - .find(|migration| migration.version == VERSION) - .unwrap_or_else(|| panic!("{driver} history remediation should be embedded")); - let sql = migration.sql.as_ref(); - for table in HISTORY_TABLES { - assert!( - sql.contains(&format!("UPDATE {table}")) - || sql.contains(&format!("UPDATE `{table}`")) - || sql.contains(&format!("UPDATE public.{table}")), - "{driver} remediation is missing {table}" - ); - } - for table in OWNER_HISTORY_TABLES { - assert!( - sql.contains(&format!("UPDATE {table}")) - || sql.contains(&format!("UPDATE `{table}`")) - || sql.contains(&format!("UPDATE public.{table}")), - "{driver} remediation is missing owner-linked history table {table}" - ); - } - for table in ["request_candidates", "video_tasks", "usage"] { - let has_api_key_projection = sql.split(';').any(|statement| { - let statement = statement.replace('`', ""); - (statement.contains(&format!("UPDATE {table}")) - || statement.contains(&format!("UPDATE public.{table}"))) - && statement.contains("SET api_key_name = NULL") - && statement.contains("api_key_id IS NOT NULL") - }); - assert!( - has_api_key_projection, - "{driver} remediation must clear orphaned api_key_name values in {table}" - ); - } - for field in ["requested_by", "approved_by", "processed_by"] { - assert!( - sql.contains(&format!("SET {field} = NULL")) - && sql.contains(&format!("{field} IS NOT NULL")), - "{driver} remediation must clear only orphaned {field} references" - ); - } - let normalized_sql = sql.replace("public.", "").replace('`', ""); - assert!( - normalized_sql.split(';').any(|statement| { - statement.contains("UPDATE payment_callbacks") - && statement.contains("SET payload = NULL") - && statement.contains("WHERE payload IS NOT NULL") - }), - "{driver} remediation must purge every persisted callback payload" - ); - assert!( - normalized_sql.contains("NOT EXISTS (\n SELECT 1 FROM wallets") - || normalized_sql.contains("NOT EXISTS (\n SELECT 1 FROM wallets"), - "{driver} remediation must clear sensitive fields for missing wallets" - ); - } -} - -#[tokio::test] -async fn sqlite_history_remediation_fails_closed_for_orphaned_financial_records() { - const VERSION: i64 = 20260829000000; - - let pool = SqlitePool::connect("sqlite::memory:") - .await - .expect("sqlite pool should connect"); - let mut connection = pool.acquire().await.expect("sqlite connection should open"); - connection - .ensure_migrations_table() - .await - .expect("migration table should be created"); - for migration in super::sqlite::MIGRATOR - .iter() - .filter(|migration| migration.version < VERSION) - { - connection - .apply(migration) - .await - .expect("pre-remediation migration should apply"); - } - drop(connection); - - query( - r#" -INSERT INTO users (id, username, email, auth_source, created_at, updated_at) -VALUES ('remediation-live-user', 'remediation-live', 'remediation-live@example.com', 'local', 1, 1); - -INSERT INTO wallets ( - id, user_id, balance, gift_balance, limit_mode, currency, status, - total_recharged, total_consumed, total_refunded, total_adjusted, - created_at, updated_at -) VALUES ( - 'remediation-live-wallet', 'remediation-live-user', 0, 0, 'finite', 'USD', 'active', - 0, 0, 0, 0, 1, 1 -); - -INSERT INTO payment_orders ( - id, order_no, wallet_id, user_id, amount_usd, refunded_amount_usd, - refundable_amount_usd, payment_method, gateway_response, status, created_at -) VALUES - ('remediation-orphan-order', 'remediation-orphan-order-no', 'missing-wallet', NULL, - 10, 0, 0, 'epay', '{"secret":"orphan"}', 'pending', 1), - ('remediation-live-order', 'remediation-live-order-no', 'remediation-live-wallet', - 'remediation-live-user', 10, 0, 0, 'epay', '{"secret":"live"}', 'pending', 1); - -INSERT INTO payment_callbacks ( - id, payment_order_id, payment_method, callback_key, order_no, - payload_hash, signature_valid, status, payload, error_message, created_at -) VALUES - ('remediation-unmatched-callback', NULL, 'epay', 'remediation-unmatched-key', - 'remediation-unmatched-order-no', 'hash-unmatched', 0, 'failed', - 'SECRET-UNMATCHED', 'private unmatched error', 1), - ('remediation-orphan-callback', 'remediation-orphan-order', 'epay', - 'remediation-orphan-key', 'remediation-orphan-order-no', 'hash-orphan', 0, - 'failed', 'SECRET-ORPHAN', 'private orphan error', 1), - ('remediation-live-callback', 'remediation-live-order', 'epay', - 'remediation-live-key', 'remediation-live-order-no', 'hash-live', 0, - 'failed', 'SECRET-LIVE', 'retain diagnostic', 1); - -INSERT INTO wallet_transactions ( - id, wallet_id, category, reason_code, amount, balance_before, balance_after, - recharge_balance_before, recharge_balance_after, gift_balance_before, - gift_balance_after, description, created_at -) VALUES ( - 'remediation-orphan-wallet-tx', 'missing-wallet', 'adjust', 'manual', 1, - 0, 1, 0, 1, 0, 0, 'private orphan transaction note', 1 -); - -INSERT INTO refund_requests ( - id, refund_no, wallet_id, user_id, source_type, refund_mode, amount_usd, - status, reason, payout_reference, payout_proof, failure_reason, - created_at, updated_at -) VALUES ( - 'remediation-orphan-refund', 'remediation-orphan-refund-no', 'missing-wallet', - NULL, 'payment_order', 'original', 1, 'pending_approval', 'private refund reason', - 'private payout reference', 'private payout proof', 'private failure', 1, 1 -); -"#, - ) - .execute(&pool) - .await - .expect("orphan financial fixtures should insert"); - - let migration = super::sqlite::MIGRATOR + let mysql_migration = super::mysql::MIGRATOR .iter() .find(|migration| migration.version == VERSION) - .expect("history remediation migration should be embedded"); - sqlx::raw_sql(migration.sql.as_ref()) - .execute(&pool) - .await - .expect("history remediation should apply"); - - let payload_count: i64 = - query_scalar("SELECT COUNT(*) FROM payment_callbacks WHERE payload IS NOT NULL") - .fetch_one(&pool) - .await - .expect("callback payload count should query"); - assert_eq!( - payload_count, 0, - "raw callback payloads must be purged globally" - ); - - let orphan_error_count: i64 = query_scalar( - "SELECT COUNT(*) FROM payment_callbacks WHERE id IN ('remediation-unmatched-callback', 'remediation-orphan-callback') AND error_message IS NOT NULL", - ) - .fetch_one(&pool) - .await - .expect("orphan callback errors should query"); - assert_eq!( - orphan_error_count, 0, - "orphan callback diagnostics must be purged" - ); - - let live_error: Option = query_scalar( - "SELECT error_message FROM payment_callbacks WHERE id = 'remediation-live-callback'", - ) - .fetch_one(&pool) - .await - .expect("live callback error should query"); - assert_eq!(live_error.as_deref(), Some("retain diagnostic")); - - let orphan_gateway_response: Option = query_scalar( - "SELECT gateway_response FROM payment_orders WHERE id = 'remediation-orphan-order'", - ) - .fetch_one(&pool) - .await - .expect("orphan gateway response should query"); - assert_eq!(orphan_gateway_response, None); - - let orphan_description: Option = query_scalar( - "SELECT description FROM wallet_transactions WHERE id = 'remediation-orphan-wallet-tx'", - ) - .fetch_one(&pool) - .await - .expect("orphan transaction description should query"); - assert_eq!(orphan_description, None); - - let orphan_refund = query( - "SELECT reason, payout_reference, payout_proof, failure_reason FROM refund_requests WHERE id = 'remediation-orphan-refund'", - ) - .fetch_one(&pool) - .await - .expect("orphan refund should query"); - for column in [ - "reason", - "payout_reference", - "payout_proof", - "failure_reason", + .expect("mysql user-history schema migration should be embedded"); + for constraint in [ + "user_plan_entitlements_user_id_fkey", + "entitlement_usage_ledgers_user_id_fkey", + "user_referrals_inviter_user_id_fkey", + "user_referrals_invitee_user_id_fkey", + "referral_rewards_inviter_user_id_fkey", + "referral_rewards_invitee_user_id_fkey", ] { - assert_eq!( - orphan_refund - .try_get::, _>(column) - .expect("orphan refund column should decode"), - None, - "orphan refund {column} must be anonymized" + assert!( + mysql_migration.sql.contains(constraint), + "mysql migration must decouple {constraint}" ); } -} -#[test] -fn background_task_sensitive_diagnostic_purge_is_enabled_for_every_driver() { - const VERSION: i64 = 20260822020000; - - for (driver, migrator) in [ - ("postgres", &POSTGRES_MIGRATOR), - ("mysql", &super::mysql::MIGRATOR), - ("sqlite", &super::sqlite::MIGRATOR), + let sqlite_migration = super::sqlite::MIGRATOR + .iter() + .find(|migration| migration.version == VERSION) + .expect("sqlite user-history schema migration should be embedded"); + for table in [ + "user_plan_entitlements", + "entitlement_usage_ledgers", + "user_referrals", + "referral_rewards", ] { - let migration = migrator - .iter() - .find(|migration| migration.version == VERSION) - .unwrap_or_else(|| { - panic!("{driver} background task diagnostic purge should be embedded") - }); - let sql = migration.sql.as_ref(); - - for required in [ - "owner_instance = NULL", - "created_by = CASE", - "progress_message = NULL", - "payload_json = NULL", - "result_json = NULL", - "error_message = CASE", - "payload_json = NULL", - "unclassified_event", - ] { - assert!( - sql.contains(required), - "{driver} background task diagnostic purge is missing {required}" - ); - } - } -} - -#[test] -fn identity_oauth_raw_userinfo_purge_is_enabled_for_every_driver() { - const VERSION: i64 = 20260827030000; - - for (driver, migrator) in [ - ("postgres", &POSTGRES_MIGRATOR), - ("mysql", &super::mysql::MIGRATOR), - ("sqlite", &super::sqlite::MIGRATOR), - ] { - let migration = migrator - .iter() - .find(|migration| migration.version == VERSION) - .unwrap_or_else(|| panic!("{driver} identity OAuth userinfo purge should be embedded")); - let sql = migration.sql.as_ref(); - - for required in [ - "UPDATE", - "user_oauth_links", - "SET extra_data = NULL", - "WHERE extra_data IS NOT NULL", - ] { - assert!( - sql.contains(required), - "{driver} identity OAuth userinfo purge is missing {required}" - ); - } + assert!( + sqlite_migration + .sql + .contains(&format!("ALTER TABLE {table} RENAME TO")), + "sqlite migration must rebuild {table} without the legacy user foreign key" + ); } } @@ -1562,22 +1224,13 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() { 20260821000000, 20260821120000, 20260821130000, - 20260822000000, - 20260822010000, - 20260822020000, - 20260827000000, - 20260827010000, - 20260827020000, - 20260827030000, 20260827040000, 20260827050000, - 20260829000000, 20260831000000, 20260831010000, 20260831020000, 20260831030000, 20260903000000, - 20260903010000, ] ); assert_eq!( @@ -1618,73 +1271,17 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() { 20260821000000, 20260821120000, 20260821130000, - 20260822000000, - 20260822010000, - 20260822020000, - 20260827000000, - 20260827010000, - 20260827020000, - 20260827030000, 20260827040000, 20260827050000, - 20260829000000, 20260831000000, 20260831010000, 20260831020000, 20260831030000, 20260903000000, - 20260903010000, ] ); } -#[tokio::test] -async fn sqlite_legacy_oauth_email_verification_migration_fails_closed() { - const VERSION: i64 = 20260903010000; - - let pool = SqlitePool::connect("sqlite::memory:") - .await - .expect("sqlite pool should connect"); - super::run_sqlite_migrations(&pool) - .await - .expect("sqlite migrations should run"); - query( - r#" -INSERT INTO users ( - id, email, username, auth_source, email_verified, created_at, updated_at -) VALUES - ('legacy-oauth', 'oauth@example.com', 'legacy-oauth', 'oauth', 1, 1, 1), - ('local-user', 'local@example.com', 'local-user', 'local', 1, 1, 1), - ('ldap-user', 'ldap@example.com', 'ldap-user', 'ldap', 1, 1, 1) -"#, - ) - .execute(&pool) - .await - .expect("email verification fixtures should insert"); - - let migration = super::sqlite::MIGRATOR - .iter() - .find(|migration| migration.version == VERSION) - .expect("legacy OAuth verification migration should be embedded"); - sqlx::raw_sql(migration.sql.as_ref()) - .execute(&pool) - .await - .expect("legacy OAuth verification migration should apply"); - - for (user_id, expected) in [ - ("legacy-oauth", 0_i64), - ("local-user", 1_i64), - ("ldap-user", 1_i64), - ] { - let verified: i64 = query_scalar("SELECT email_verified FROM users WHERE id = ?") - .bind(user_id) - .fetch_one(&pool) - .await - .expect("user verification state should load"); - assert_eq!(verified, expected, "unexpected state for {user_id}"); - } -} - #[tokio::test] async fn sqlite_gateway_order_uniqueness_migration_rejects_historical_duplicates() { const VERSION: i64 = 20260821120000; @@ -2938,22 +2535,13 @@ fn pending_migrations_from_applied_skips_versions_already_applied() { 20260821000000, 20260821120000, 20260821130000, - 20260822000000, - 20260822010000, - 20260822020000, - 20260827000000, - 20260827010000, - 20260827020000, - 20260827030000, 20260827040000, 20260827050000, - 20260829000000, 20260831000000, 20260831010000, 20260831030000, 20260901000000, 20260903000000, - 20260903010000, ] ); }