feat(security): harden gateway boundaries and usage policies

Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change.

Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
elky
2026-09-04 03:45:52 +08:00
parent ddcbeb3ae9
commit 579f2c7cc1
1019 changed files with 190437 additions and 26080 deletions
@@ -578,7 +578,7 @@ CREATE TABLE IF NOT EXISTS proxy_nodes (
is_manual TINYINT(1) NOT NULL DEFAULT 0,
proxy_url VARCHAR(500),
proxy_username VARCHAR(255),
proxy_password VARCHAR(500),
proxy_password TEXT,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
remote_config TEXT,
@@ -0,0 +1,35 @@
CREATE TABLE IF NOT EXISTS usage_cost_reservations (
`request_id` VARCHAR(128) NOT NULL,
`subject_id` VARCHAR(128) NOT NULL,
`reservation_token` VARCHAR(128) NOT NULL,
`admitted_at` BIGINT NOT NULL,
`reserved_cost_units` BIGINT NOT NULL,
`actual_cost_units` BIGINT,
`state` VARCHAR(20) NOT NULL,
`reservation_expires_at` BIGINT NOT NULL,
`retain_until` BIGINT NOT NULL,
`finalized_at` BIGINT,
`created_at` BIGINT NOT NULL,
`updated_at` BIGINT NOT NULL,
PRIMARY KEY (`reservation_token`),
CONSTRAINT usage_cost_reservations_state_check
CHECK (`state` IN ('reserved', 'finalized', 'released')),
CONSTRAINT usage_cost_reservations_reserved_cost_units_check
CHECK (`reserved_cost_units` >= 0),
CONSTRAINT usage_cost_reservations_actual_cost_units_check
CHECK (`actual_cost_units` IS NULL OR `actual_cost_units` >= 0),
CONSTRAINT usage_cost_reservations_expiry_check
CHECK (`reservation_expires_at` > `admitted_at`),
CONSTRAINT usage_cost_reservations_retention_check
CHECK (`retain_until` >= `reservation_expires_at`),
CONSTRAINT usage_cost_reservations_lifecycle_check CHECK (
(`state` = 'reserved' AND `actual_cost_units` IS NULL AND `finalized_at` IS NULL)
OR (`state` = 'finalized' AND `actual_cost_units` IS NOT NULL AND `finalized_at` IS NOT NULL)
OR (`state` = 'released' AND `actual_cost_units` IS NOT NULL
AND `actual_cost_units` = 0 AND `finalized_at` IS NOT NULL)
),
KEY usage_cost_reservations_request_id_idx (`request_id`),
KEY usage_cost_reservations_subject_admitted_at_idx (`subject_id`, `admitted_at`),
KEY usage_cost_reservations_reservation_expires_at_idx (`reservation_expires_at`),
KEY usage_cost_reservations_retain_until_token_idx (`retain_until`, `reservation_token`)
);
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS usage_request_admissions (
`request_id` VARCHAR(128) NOT NULL,
`subject_id` VARCHAR(128) NOT NULL,
`event_token` VARCHAR(128) NOT NULL,
`admitted_at` BIGINT NOT NULL,
`retain_until` BIGINT NOT NULL,
`state` VARCHAR(20) NOT NULL,
`released_at` BIGINT,
`created_at` BIGINT NOT NULL,
PRIMARY KEY (`event_token`),
CONSTRAINT usage_request_admissions_retention_check
CHECK (`retain_until` > `admitted_at`),
CONSTRAINT usage_request_admissions_state_check
CHECK (`state` IN ('active', 'released')),
CONSTRAINT usage_request_admissions_lifecycle_check CHECK (
(`state` = 'active' AND `released_at` IS NULL)
OR (`state` = 'released' AND `released_at` IS NOT NULL
AND `released_at` >= `admitted_at`)
),
KEY usage_request_admissions_subject_admitted_at_idx (`subject_id`, `admitted_at`),
KEY usage_request_admissions_retain_until_token_idx (`retain_until`, `event_token`)
);
@@ -0,0 +1,12 @@
-- Preserve the already-published cost-ledger migration checksum. This
-- follow-up also upgrades databases which ran the feature branch before user
-- ownership was enforced. Keep one ALTER TABLE per migration because MySQL
-- DDL implicitly commits.
DELETE reservation
FROM usage_cost_reservations AS reservation
LEFT JOIN users AS app_user ON app_user.id = reservation.subject_id
WHERE app_user.id IS NULL;
ALTER TABLE usage_cost_reservations
ADD CONSTRAINT usage_cost_reservations_subject_id_fkey
FOREIGN KEY (`subject_id`) REFERENCES users (`id`) ON DELETE CASCADE;
@@ -0,0 +1,10 @@
-- Split from the cost-ledger foreign key so a MySQL implicit DDL commit cannot
-- leave two table changes behind one dirty migration record.
DELETE admission
FROM usage_request_admissions AS admission
LEFT JOIN users AS app_user ON app_user.id = admission.subject_id
WHERE app_user.id IS NULL;
ALTER TABLE usage_request_admissions
ADD CONSTRAINT usage_request_admissions_subject_id_fkey
FOREIGN KEY (`subject_id`) REFERENCES users (`id`) ON DELETE CASCADE;
@@ -0,0 +1,55 @@
-- A gateway transaction identifier may repeat across payment methods, but
-- must never identify two orders in the same normalized method. MySQL commits
-- persistent DDL implicitly, so reject historical conflicts before any
-- persistent UPDATE or ALTER TABLE. CREATE/DROP TEMPORARY TABLE do not cause an
-- implicit commit, and the leading DROP also makes a same-session retry safe.
-- Diagnose with:
-- SELECT LOWER(TRIM(payment_method)),
-- CONVERT(gateway_order_id USING utf8mb4) COLLATE utf8mb4_0900_bin,
-- COUNT(*)
-- FROM payment_orders
-- WHERE gateway_order_id IS NOT NULL
-- GROUP BY LOWER(TRIM(payment_method)),
-- CONVERT(gateway_order_id USING utf8mb4) COLLATE utf8mb4_0900_bin
-- HAVING COUNT(*) > 1;
DROP TEMPORARY TABLE IF EXISTS aether_payment_gateway_order_uniqueness_preflight;
CREATE TEMPORARY TABLE aether_payment_gateway_order_uniqueness_preflight (
conflict_marker TINYINT NOT NULL PRIMARY KEY
);
INSERT INTO aether_payment_gateway_order_uniqueness_preflight (conflict_marker)
VALUES (1);
-- Inserting the same marker fails on the first conflicting group. The grouping
-- mirrors the values and collations used by the normalization and final index:
-- payment methods use their existing column collation after LOWER/TRIM, while
-- opaque gateway identifiers use MySQL 8's case-sensitive binary collation.
INSERT INTO aether_payment_gateway_order_uniqueness_preflight (conflict_marker)
SELECT 1
FROM payment_orders
WHERE gateway_order_id IS NOT NULL
GROUP BY
LOWER(TRIM(payment_method)),
CONVERT(gateway_order_id USING utf8mb4) COLLATE utf8mb4_0900_bin
HAVING COUNT(*) > 1
LIMIT 1;
DROP TEMPORARY TABLE aether_payment_gateway_order_uniqueness_preflight;
UPDATE payment_orders
SET payment_method = LOWER(TRIM(payment_method))
WHERE BINARY payment_method <> BINARY LOWER(TRIM(payment_method));
UPDATE payment_callbacks
SET payment_method = LOWER(TRIM(payment_method))
WHERE BINARY payment_method <> BINARY LOWER(TRIM(payment_method));
-- Gateway identifiers are opaque and case-sensitive. Changing the column
-- collation and adding the unique index in one ALTER avoids a persistent
-- intermediate schema if either operation fails.
ALTER TABLE payment_orders
MODIFY COLUMN gateway_order_id VARCHAR(128)
CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin NULL,
ADD UNIQUE INDEX uq_payment_orders_payment_method_gateway_order_id
(payment_method, gateway_order_id);
@@ -0,0 +1,5 @@
ALTER TABLE users
ADD COLUMN security_version BIGINT NOT NULL DEFAULT 0;
ALTER TABLE user_sessions
ADD COLUMN security_version BIGINT NOT NULL DEFAULT 0;
@@ -0,0 +1,16 @@
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;
@@ -0,0 +1,124 @@
-- 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;
@@ -0,0 +1,43 @@
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'
);
@@ -0,0 +1,13 @@
-- 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;
@@ -0,0 +1,6 @@
-- 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;
@@ -0,0 +1,5 @@
-- 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;
@@ -0,0 +1,6 @@
-- 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;
@@ -0,0 +1,3 @@
-- Purpose-bound Fernet envelopes can exceed the former 500-character plaintext limit.
ALTER TABLE proxy_nodes
MODIFY COLUMN proxy_password TEXT NULL;
@@ -0,0 +1,377 @@
SET @aether_drop_fact_user_fk_sql := IF(
EXISTS (
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'user_plan_entitlements'
AND CONSTRAINT_NAME = 'user_plan_entitlements_user_id_fkey'
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
),
'ALTER TABLE user_plan_entitlements DROP FOREIGN KEY user_plan_entitlements_user_id_fkey',
'DO 0'
);
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;
SET @aether_drop_fact_user_fk_sql := IF(
EXISTS (
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'entitlement_usage_ledgers'
AND CONSTRAINT_NAME = 'entitlement_usage_ledgers_user_id_fkey'
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
),
'ALTER TABLE entitlement_usage_ledgers DROP FOREIGN KEY entitlement_usage_ledgers_user_id_fkey',
'DO 0'
);
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;
SET @aether_drop_fact_user_fk_sql := IF(
EXISTS (
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'user_referrals'
AND CONSTRAINT_NAME = 'user_referrals_inviter_user_id_fkey'
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
),
'ALTER TABLE user_referrals DROP FOREIGN KEY user_referrals_inviter_user_id_fkey',
'DO 0'
);
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;
SET @aether_drop_fact_user_fk_sql := IF(
EXISTS (
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'user_referrals'
AND CONSTRAINT_NAME = 'user_referrals_invitee_user_id_fkey'
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
),
'ALTER TABLE user_referrals DROP FOREIGN KEY user_referrals_invitee_user_id_fkey',
'DO 0'
);
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;
SET @aether_drop_fact_user_fk_sql := IF(
EXISTS (
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'referral_rewards'
AND CONSTRAINT_NAME = 'referral_rewards_inviter_user_id_fkey'
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
),
'ALTER TABLE referral_rewards DROP FOREIGN KEY referral_rewards_inviter_user_id_fkey',
'DO 0'
);
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;
SET @aether_drop_fact_user_fk_sql := IF(
EXISTS (
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'referral_rewards'
AND CONSTRAINT_NAME = 'referral_rewards_invitee_user_id_fkey'
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
),
'ALTER TABLE referral_rewards DROP FOREIGN KEY referral_rewards_invitee_user_id_fkey',
'DO 0'
);
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
);
@@ -0,0 +1,451 @@
-- 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
);
@@ -0,0 +1,13 @@
-- LDAP configuration is a database-wide singleton. Preserve the row selected by the legacy
-- reader (the smallest id), remove historical duplicates, and let the database arbitrate
-- concurrent first creation.
DELETE FROM ldap_configs
WHERE id <> (
SELECT keep_id
FROM (SELECT MIN(id) AS keep_id FROM ldap_configs) AS ldap_singleton_keeper
);
ALTER TABLE ldap_configs
ADD COLUMN singleton_key INT NOT NULL DEFAULT 1,
ADD CONSTRAINT ldap_configs_singleton_key_check CHECK (singleton_key = 1),
ADD UNIQUE KEY ldap_configs_singleton_key_key (singleton_key);
@@ -0,0 +1,9 @@
ALTER TABLE proxy_nodes
ADD COLUMN tunnel_generation VARCHAR(64) NULL AFTER id;
UPDATE proxy_nodes
SET tunnel_generation = UUID()
WHERE tunnel_generation IS NULL OR TRIM(tunnel_generation) = '';
ALTER TABLE proxy_nodes
MODIFY COLUMN tunnel_generation VARCHAR(64) NOT NULL;
@@ -0,0 +1,10 @@
-- A proxy endpoint has one stable node identity across manual and tunnel
-- registrations. MySQL 8 performs ALTER TABLE atomically; if historical
-- duplicates exist this migration fails without choosing or deleting a row.
-- Diagnose with:
-- SELECT ip, port, COUNT(*)
-- FROM proxy_nodes
-- GROUP BY ip, port
-- HAVING COUNT(*) > 1;
ALTER TABLE proxy_nodes
ADD UNIQUE INDEX uq_proxy_node_ip_port (ip, port);
@@ -0,0 +1,2 @@
ALTER TABLE usage_counter_deltas
ADD COLUMN target_tunnel_generation VARCHAR(64) NULL;
@@ -0,0 +1,8 @@
-- 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;