diff --git a/.downstream/upstream-base b/.downstream/upstream-base index fd8518c9d5ea..ec13377f3e51 100644 --- a/.downstream/upstream-base +++ b/.downstream/upstream-base @@ -1 +1 @@ -v0.2.13 +v0.2.14 diff --git a/.downstream/upstream-review-v0.2.14.md b/.downstream/upstream-review-v0.2.14.md new file mode 100644 index 000000000000..c27fc9825b88 --- /dev/null +++ b/.downstream/upstream-review-v0.2.14.md @@ -0,0 +1,24 @@ +# Official v0.2.14 integration + +- Official tag: `v0.2.14`, commit `0363b8cdba8cec3e2ba4b2dbd49c4481143fa55d`. +- Downstream starting commit: `7132011d7a4275e7d6407c92db4ec205a1c14ad1`. +- The already integrated v0.2.13 ancestry is recorded by an ours merge of + `b8dece9000c68815a5b867ca5a1e6f236e173905`; this marker changes no files. +- The three conflicts are the frontend package, lockfile, and audit exceptions. + Vue uses `^3.5.43`; upstream DOMPurify/source-map-js constraints and downstream + constraints are retained. XLSX remains the patched SheetJS CDN 0.20.3 artifact, + with no renewed XLSX exceptions. The final lockfile installs frozen. +- Security fixes and remote Codex API-key model discovery follow the official tag. + Existing administrator credentials are skipped, and existing payment reconcile + paths remain. File catalogs, the 1 MiB fallback, auth/WS and source-bound capacity + contracts remain. Runtime configuration and UI styling are unchanged. +- VERSION metadata is synchronized to 0.2.14; the upstream release tag itself + retains the older fallback value. No unreleased upstream feature is imported. +- Historical SQL, migration 274 and Ent schemas are unchanged. No new database + migration is introduced. Retired acceptance/plugin/resource/rollback paths + remain retired. The current interfaces are in [DOWNSTREAM](../DOWNSTREAM.md). + +Validation is the affected setup/EasyPay/return URL cases and mounted Codex +configuration scenarios, plus the normal required PR checks. Source/package +checks do not prove deployment or live model/payment effects. Production status +remains owned by the operator workspace's production pointer. diff --git a/.downstream/upstream-risk.json b/.downstream/upstream-risk.json index 894b4b7763f8..bdf36c2123d7 100644 --- a/.downstream/upstream-risk.json +++ b/.downstream/upstream-risk.json @@ -1,68 +1,33 @@ { - "critical_file_count": 31, + "critical_file_count": 10, "critical_files": [ - "backend/ent/migrate/schema.go", - "backend/ent/mutation.go", - "backend/ent/paymentorder.go", - "backend/ent/paymentorder/paymentorder.go", - "backend/ent/paymentorder/where.go", - "backend/ent/paymentorder_create.go", - "backend/ent/paymentorder_update.go", - "backend/ent/runtime/runtime.go", - "backend/ent/schema/payment_order.go", - "backend/ent/schema/user_platform_quota.go", - "backend/internal/repository/api_key_repo.go", - "backend/internal/repository/api_key_repo_sort_test.go", - "backend/internal/repository/email_cache.go", - "backend/internal/repository/email_cache_atomic_test.go", - "backend/internal/repository/http_upstream.go", - "backend/internal/repository/http_upstream_test.go", - "backend/internal/repository/usage_billing_repo.go", - "backend/internal/repository/usage_billing_repo_integration_test.go", - "backend/internal/repository/usage_billing_repo_unit_test.go", - "backend/internal/securityaudit/prompt_snapshot.go", - "backend/internal/securityaudit/prompt_snapshot_test.go", - "backend/internal/service/billing_service.go", - "backend/internal/service/billing_service_test.go", - "backend/internal/service/gateway_systemone.go", - "backend/internal/service/gateway_systemone_test.go", - "backend/internal/service/openai_gateway_grok.go", - "backend/migrations/241_add_payment_order_bonus_amount.sql", - "backend/migrations/241_add_typesafe_platform.sql", - "backend/migrations/typesafe_platform_migration_test.go", + "deploy/.env.example", + "deploy/APPLE_CONTAINER.md", + "deploy/README.md", + "deploy/docker-compose.dev.yml", + "deploy/docker-compose.local.yml", + "deploy/docker-compose.standalone.yml", + "deploy/docker-compose.yml", + "deploy/docker-deploy.sh", "frontend/package.json", "frontend/pnpm-lock.yaml" ], - "downstream_commit": "291d25e7f8a8c82d45072a1eee742c21ba3b8d21", - "downstream_file_count": 1420, + "downstream_commit": "7132011d7a4275e7d6407c92db4ec205a1c14ad1", + "downstream_file_count": 1232, "downstream_files": [ ".dockerignore", ".downstream/README.md", - ".downstream/cindy-catalog-refresh-20260905.json", - ".downstream/cindy-catalog-refresh-20260911.json", - ".downstream/coverage-v0.2.7.json", - ".downstream/coverage-v0.2.7.md", ".downstream/gpt6-models-and-prompts.md", - ".downstream/implementation-v0.2.7.md", ".downstream/native-domains.md", - ".downstream/refresh-coverage.mjs", - ".downstream/refresh-coverage.test.mjs", - ".downstream/upstream-0.2.0-overlap-review.md", ".downstream/upstream-base", - ".downstream/upstream-review-v0.2.1.md", - ".downstream/upstream-review-v0.2.10.md", - ".downstream/upstream-review-v0.2.11.md", - ".downstream/upstream-review-v0.2.5.md", - ".downstream/upstream-review-v0.2.6.md", - ".downstream/upstream-review-v0.2.8.md", - ".downstream/upstream-review-v0.2.9.md", + ".downstream/upstream-review-v0.2.13.md", ".downstream/upstream-risk.json", ".gitattributes", + ".github/audit-exceptions.yml", ".github/scripts/release_preflight.py", ".github/scripts/test_agent_protocol.sh", ".github/scripts/test_model_context_capacity.sh", - ".github/scripts/test_native_domains.sh", - ".github/scripts/test_reasoning_replay.sh", + ".github/scripts/test_reasoning_recovery.sh", ".github/scripts/test_release_preflight.py", ".github/scripts/test_upstream_automation_contract.py", ".github/scripts/test_upstream_risk.py", @@ -71,7 +36,6 @@ ".github/workflows/backend-ci.yml", ".github/workflows/downstream-release.yml", ".github/workflows/downstream-verify.yml", - ".github/workflows/legacy-ghcr-cleanup.yml", ".github/workflows/production-deploy.yml", ".github/workflows/release.yml", ".github/workflows/security-scan.yml", @@ -81,26 +45,15 @@ ".github/workflows/upstream-risk-gate.yml", ".github/workflows/upstream-sync.yml", ".gitignore", - ".superpowers/sdd/cindy-group-account-ops-v180/progress.md", - ".superpowers/sdd/cindy-group-account-ops-v180/task-3-report.md", - ".superpowers/sdd/cindy-group-account-ops-v180/task-4b-report.md", - ".superpowers/sdd/cindy-group-account-ops-v180/task-5-report.md", - ".superpowers/sdd/cindy-group-account-ops-v180/task-6-report.md", "DOWNSTREAM.md", "Dockerfile", "Dockerfile.goreleaser", "Makefile", - "README.md", - "README_CN.md", - "README_JA.md", "THIRD_PARTY_NOTICES.md", "backend/.golangci.yml", "backend/cmd/cleanup-ingress-reject-logs/main.go", - "backend/cmd/profit-preview/main.go", - "backend/cmd/profit-preview/main_test.go", "backend/cmd/server/VERSION", "backend/cmd/server/main.go", - "backend/cmd/server/main_test.go", "backend/cmd/server/wire.go", "backend/cmd/server/wire_gen.go", "backend/cmd/server/wire_gen_test.go", @@ -131,11 +84,6 @@ "backend/ent/accounttagbinding_delete.go", "backend/ent/accounttagbinding_query.go", "backend/ent/accounttagbinding_update.go", - "backend/ent/apikey.go", - "backend/ent/apikey/apikey.go", - "backend/ent/apikey/where.go", - "backend/ent/apikey_create.go", - "backend/ent/apikey_update.go", "backend/ent/client.go", "backend/ent/ent.go", "backend/ent/hook/hook.go", @@ -148,21 +96,11 @@ "backend/ent/schema/account_folder.go", "backend/ent/schema/account_tag.go", "backend/ent/schema/account_tag_binding.go", - "backend/ent/schema/api_key.go", "backend/ent/tx.go", "backend/go.mod", "backend/go.sum", - "backend/internal/accounttools/policy/import.go", - "backend/internal/accounttools/policy/import_test.go", - "backend/internal/accounttools/policy/module.go", - "backend/internal/accounttools/policy/module_test.go", - "backend/internal/codexruntime/profile/identity.go", - "backend/internal/codexruntime/profile/identity_test.go", - "backend/internal/codexruntime/recovery/policy.go", - "backend/internal/codexruntime/recovery/policy_test.go", "backend/internal/config/config.go", "backend/internal/config/config_test.go", - "backend/internal/config/image_studio_feature_flags.go", "backend/internal/handler/admin/account_api_key_visibility.go", "backend/internal/handler/admin/account_api_key_visibility_test.go", "backend/internal/handler/admin/account_bulk_job_scope_test.go", @@ -186,27 +124,23 @@ "backend/internal/handler/admin/account_handler_long_context_billing_test.go", "backend/internal/handler/admin/account_handler_mixed_channel_test.go", "backend/internal/handler/admin/account_handler_model_context_test.go", - "backend/internal/handler/admin/account_handler_traffic_telemetry_test.go", "backend/internal/handler/admin/account_job_execution.go", "backend/internal/handler/admin/account_job_execution_test.go", "backend/internal/handler/admin/account_job_handler.go", "backend/internal/handler/admin/account_job_submit_test.go", - "backend/internal/handler/admin/account_reasoning_policy.go", - "backend/internal/handler/admin/account_reasoning_policy_test.go", "backend/internal/handler/admin/account_taxonomy.go", "backend/internal/handler/admin/account_taxonomy_handler_test.go", "backend/internal/handler/admin/account_test_plan.go", "backend/internal/handler/admin/account_test_plan_test.go", "backend/internal/handler/admin/account_today_stats_cache.go", - "backend/internal/handler/admin/admin_basic_handlers_test.go", + "backend/internal/handler/admin/account_upstream_billing_rates.go", + "backend/internal/handler/admin/account_upstream_billing_rates_test.go", "backend/internal/handler/admin/batch_update_credentials_test.go", "backend/internal/handler/admin/codex_error_diagnostics.go", - "backend/internal/handler/admin/group_handler.go", "backend/internal/handler/admin/group_handler_simple_mode_test.go", - "backend/internal/handler/admin/native_policy_fixture_test.go", - "backend/internal/handler/admin/openai_oauth_handler.go", "backend/internal/handler/admin/payment_handler_test.go", - "backend/internal/handler/admin/plugin_handler.go", + "backend/internal/handler/admin/reasoning_recovery_handler.go", + "backend/internal/handler/admin/reasoning_recovery_handler_test.go", "backend/internal/handler/admin/scheduled_test_handler.go", "backend/internal/handler/admin/setting_handler.go", "backend/internal/handler/admin/setting_handler_audit.go", @@ -218,16 +152,11 @@ "backend/internal/handler/admin/snapshot_cache_test.go", "backend/internal/handler/admin/system_prompt_handler.go", "backend/internal/handler/admin/user_platform_quota_admin_test.go", - "backend/internal/handler/continuation_repair_live_support_test.go", - "backend/internal/handler/continuation_repair_live_test.go", - "backend/internal/handler/dto/account_view_facts_test.go", "backend/internal/handler/dto/credentials_redact.go", "backend/internal/handler/dto/credentials_redact_test.go", - "backend/internal/handler/dto/group_mapper_profit_visibility_test.go", "backend/internal/handler/dto/mappers.go", "backend/internal/handler/dto/settings.go", "backend/internal/handler/dto/types.go", - "backend/internal/handler/failover_loop_profit_veto_test.go", "backend/internal/handler/failover_loop_test.go", "backend/internal/handler/gateway_handler.go", "backend/internal/handler/gateway_handler_chat_completions.go", @@ -236,10 +165,6 @@ "backend/internal/handler/gateway_models_test.go", "backend/internal/handler/grok_media.go", "backend/internal/handler/handler.go", - "backend/internal/handler/image_studio_executor.go", - "backend/internal/handler/image_studio_executor_test.go", - "backend/internal/handler/image_studio_job_handler.go", - "backend/internal/handler/image_studio_job_handler_test.go", "backend/internal/handler/no_account_error.go", "backend/internal/handler/no_account_error_test.go", "backend/internal/handler/openai_alpha_search.go", @@ -249,6 +174,7 @@ "backend/internal/handler/openai_chat_completions.go", "backend/internal/handler/openai_codex_models_handler.go", "backend/internal/handler/openai_codex_models_handler_test.go", + "backend/internal/handler/openai_continuation_account_mismatch_test.go", "backend/internal/handler/openai_count_tokens_model_not_supported_test.go", "backend/internal/handler/openai_embeddings.go", "backend/internal/handler/openai_failover_retry.go", @@ -269,8 +195,6 @@ "backend/internal/handler/openai_official_http_failover.go", "backend/internal/handler/openai_official_http_failover_integration_test.go", "backend/internal/handler/openai_official_http_retry_test.go", - "backend/internal/handler/openai_profit_slot_recheck_test.go", - "backend/internal/handler/openai_profit_veto_budget_test.go", "backend/internal/handler/openai_reasoning_recovery_terminal_handler_test.go", "backend/internal/handler/openai_refusal_recovery_handler_test.go", "backend/internal/handler/openai_replayable_endpoints_failover_test.go", @@ -282,22 +206,11 @@ "backend/internal/handler/ops_error_logger.go", "backend/internal/handler/ops_error_logger_continuation_test.go", "backend/internal/handler/ops_error_logger_test.go", - "backend/internal/handler/plugin_contract_fixture_test.go", "backend/internal/handler/request_body_limit.go", "backend/internal/handler/request_body_limit_test.go", "backend/internal/handler/setting_handler.go", "backend/internal/handler/stream_error_event.go", "backend/internal/handler/wire.go", - "backend/internal/nativeapi/account_import.go", - "backend/internal/nativeapi/account_tools.go", - "backend/internal/nativeapi/api.go", - "backend/internal/nativeapi/catalog.go", - "backend/internal/nativeapi/codex_identity.go", - "backend/internal/nativeapi/codex_recovery.go", - "backend/internal/nativeapi/codex_runtime.go", - "backend/internal/nativeapi/decision.go", - "backend/internal/nativeapi/image.go", - "backend/internal/nativeapi/observability.go", "backend/internal/payment/provider/alipay_test.go", "backend/internal/payment/provider/easypay_refund_test.go", "backend/internal/payment/provider/wxpay_test.go", @@ -348,11 +261,10 @@ "backend/internal/pkg/openai_compat/upstream_capability.go", "backend/internal/pkg/openai_compat/upstream_capability_test.go", "backend/internal/pkg/proxyurl/parse_test.go", - "backend/internal/pkg/proxyutil/dialer.go", "backend/internal/pkg/tlsfingerprint/dialer.go", "backend/internal/pkg/tlsfingerprint/dialer_deadline_test.go", "backend/internal/pkg/tlsfingerprint/dialer_test.go", - "backend/internal/proxytransport/transport.go", + "backend/internal/pkg/typesafe/client.go", "backend/internal/repository/account_extra_revision.go", "backend/internal/repository/account_extra_revision_integration_test.go", "backend/internal/repository/account_job_connection_integration_test.go", @@ -361,6 +273,7 @@ "backend/internal/repository/account_quota_estimate.go", "backend/internal/repository/account_quota_estimate_integration_test.go", "backend/internal/repository/account_quota_snapshot_order_integration_test.go", + "backend/internal/repository/account_reasoning_options_purge_migration_integration_test.go", "backend/internal/repository/account_repo.go", "backend/internal/repository/account_repo_codex_fingerprint_seed_test.go", "backend/internal/repository/account_repo_integration_test.go", @@ -373,10 +286,9 @@ "backend/internal/repository/account_repo_temp_unsched_test.go", "backend/internal/repository/account_repo_upstream_billing_probe_update_test.go", "backend/internal/repository/account_taxonomy_integration_test.go", - "backend/internal/repository/account_traffic_observe_cache.go", + "backend/internal/repository/account_traffic_telemetry_switch_purge_migration_integration_test.go", "backend/internal/repository/affiliate_repo.go", "backend/internal/repository/api_key_repo.go", - "backend/internal/repository/api_key_repo_integration_test.go", "backend/internal/repository/audit_log_repo.go", "backend/internal/repository/batch_image_repo_integration_test.go", "backend/internal/repository/channel_monitor_repo.go", @@ -392,14 +304,13 @@ "backend/internal/repository/channel_repo_test.go", "backend/internal/repository/codex_fingerprint_seed_migration_integration_test.go", "backend/internal/repository/codex_runtime_plugin_purge_migration_integration_test.go", - "backend/internal/repository/concurrency_cache_live_test.go", "backend/internal/repository/dashboard_aggregation_repo.go", "backend/internal/repository/db_pool.go", - "backend/internal/repository/db_pool_test.go", "backend/internal/repository/decompress_response_test.go", "backend/internal/repository/ent.go", "backend/internal/repository/ent_init_retry_test.go", "backend/internal/repository/error_translate.go", + "backend/internal/repository/first_party_plugin_purge_migration_integration_test.go", "backend/internal/repository/gateway_cache.go", "backend/internal/repository/gateway_cache_live_test.go", "backend/internal/repository/gateway_cache_reasoning_state.go", @@ -409,19 +320,13 @@ "backend/internal/repository/group_usage_rollup_trigger_integration_test.go", "backend/internal/repository/http_upstream.go", "backend/internal/repository/http_upstream_compression_test.go", - "backend/internal/repository/image_studio_job_repo.go", - "backend/internal/repository/image_studio_job_repo_test.go", + "backend/internal/repository/image_studio_drop_migration_integration_test.go", "backend/internal/repository/integration_harness_test.go", "backend/internal/repository/migrations_historical_preflight.go", "backend/internal/repository/migrations_historical_preflight_test.go", "backend/internal/repository/migrations_runner.go", - "backend/internal/repository/native_feature_bootstrap.go", - "backend/internal/repository/native_feature_bootstrap_integration_test.go", - "backend/internal/repository/native_plugin_boundary.go", - "backend/internal/repository/native_plugin_boundary_test.go", - "backend/internal/repository/native_policy_fixture_test.go", + "backend/internal/repository/openai_apikey_compat_settings_purge_migration_integration_test.go", "backend/internal/repository/openai_oauth_service.go", - "backend/internal/repository/ops_error_where_test.go", "backend/internal/repository/ops_repo.go", "backend/internal/repository/ops_write_pressure_integration_test.go", "backend/internal/repository/passkey_repo.go", @@ -434,13 +339,13 @@ "backend/internal/repository/quota_activity.go", "backend/internal/repository/quota_activity_test.go", "backend/internal/repository/scheduler_cache.go", - "backend/internal/repository/scheduler_cache_reasoning_policy_test.go", "backend/internal/repository/scheduler_cache_unit_test.go", + "backend/internal/repository/system_prompt_binding_purge_migration_integration_test.go", "backend/internal/repository/system_prompt_bindings.go", + "backend/internal/repository/system_prompt_remote_bootstrap_migration_integration_test.go", "backend/internal/repository/temp_unsched_cache.go", "backend/internal/repository/temp_unsched_cache_health_test.go", - "backend/internal/repository/usage_billing_owner_test.go", - "backend/internal/repository/usage_billing_repo.go", + "backend/internal/repository/usage_billing_dedup_account_id_drop_migration_integration_test.go", "backend/internal/repository/usage_log_repo_stats.go", "backend/internal/repository/user_group_rate_repo.go", "backend/internal/repository/user_platform_quota_repo.go", @@ -468,24 +373,22 @@ "backend/internal/server/routes/gateway_model_allowlist_test.go", "backend/internal/server/routes/gateway_models_pinned_test.go", "backend/internal/server/routes/gateway_test.go", + "backend/internal/server/routes/reasoning_recovery_routes_test.go", "backend/internal/server/routes/system_prompt_routes_test.go", - "backend/internal/server/routes/user.go", "backend/internal/service/account.go", "backend/internal/service/account_available_models_test.go", "backend/internal/service/account_base_url.go", "backend/internal/service/account_base_url_input_test.go", "backend/internal/service/account_business_errors.go", "backend/internal/service/account_business_errors_test.go", - "backend/internal/service/account_edit_codec.go", - "backend/internal/service/account_import_policy.go", - "backend/internal/service/account_import_policy_test.go", "backend/internal/service/account_job.go", "backend/internal/service/account_job_test.go", "backend/internal/service/account_quota_window_state.go", "backend/internal/service/account_quota_window_state_test.go", - "backend/internal/service/account_reasoning_policy_test.go", "backend/internal/service/account_scheduling_threshold_eval.go", "backend/internal/service/account_scheduling_threshold_eval_test.go", + "backend/internal/service/account_search_predicate.go", + "backend/internal/service/account_search_predicate_test.go", "backend/internal/service/account_service.go", "backend/internal/service/account_test_model.go", "backend/internal/service/account_test_models_test.go", @@ -502,30 +405,23 @@ "backend/internal/service/account_test_service_openai_image_test.go", "backend/internal/service/account_test_service_openai_test.go", "backend/internal/service/account_test_service_opencode_go.go", + "backend/internal/service/account_test_service_typesafe.go", + "backend/internal/service/account_test_service_typesafe_test.go", "backend/internal/service/account_test_stream.go", "backend/internal/service/account_test_stream_contract_test.go", - "backend/internal/service/account_tools.go", - "backend/internal/service/account_tools_module.go", - "backend/internal/service/account_traffic_observe.go", "backend/internal/service/account_usage_service.go", "backend/internal/service/account_usage_service_test.go", "backend/internal/service/account_usage_session_window_test.go", - "backend/internal/service/account_view_facts.go", "backend/internal/service/admin_account.go", "backend/internal/service/admin_account_model_context_test.go", "backend/internal/service/admin_account_taxonomy.go", "backend/internal/service/admin_account_taxonomy_test.go", - "backend/internal/service/admin_group_duplicate.go", "backend/internal/service/admin_group_platform_cache_test.go", - "backend/internal/service/admin_observability.go", - "backend/internal/service/admin_observability_test.go", "backend/internal/service/admin_service.go", - "backend/internal/service/admin_service_apikey_test.go", "backend/internal/service/admin_service_bulk_update_test.go", "backend/internal/service/admin_service_composite_group_test.go", "backend/internal/service/admin_service_group_test.go", "backend/internal/service/admin_service_spark_shadow_test.go", - "backend/internal/service/admin_user.go", "backend/internal/service/agent_cc_stream_contract_test.go", "backend/internal/service/agent_raw_stream_contract_test.go", "backend/internal/service/agent_request_capabilities_test.go", @@ -536,11 +432,8 @@ "backend/internal/service/antigravity_gateway_retry.go", "backend/internal/service/antigravity_gateway_service.go", "backend/internal/service/antigravity_gateway_upstream.go", - "backend/internal/service/api_key.go", "backend/internal/service/api_key_auth_cache_impl.go", - "backend/internal/service/api_key_auth_cache_profit_test.go", "backend/internal/service/api_key_auth_cache_version_test.go", - "backend/internal/service/api_key_service.go", "backend/internal/service/audit_log_service.go", "backend/internal/service/audit_log_test.go", "backend/internal/service/auth_service_platform_quota_test.go", @@ -549,6 +442,7 @@ "backend/internal/service/billing_context_schedule.go", "backend/internal/service/billing_service.go", "backend/internal/service/billing_token_cost_request.go", + "backend/internal/service/channel.go", "backend/internal/service/claude_reset_redeem.go", "backend/internal/service/claude_reset_redeem_test.go", "backend/internal/service/cn_provider_balance_check_service.go", @@ -558,6 +452,8 @@ "backend/internal/service/codex_outbound_contract.go", "backend/internal/service/composite_platform_test.go", "backend/internal/service/content_moderation_runtime_cache_test.go", + "backend/internal/service/conversation_standard_pricing.go", + "backend/internal/service/conversation_standard_pricing_test.go", "backend/internal/service/deepseek_pricing.go", "backend/internal/service/deepseek_pricing_test.go", "backend/internal/service/domain_constants.go", @@ -568,16 +464,15 @@ "backend/internal/service/gateway_forward_as_chat_completions.go", "backend/internal/service/gateway_forward_as_responses.go", "backend/internal/service/gateway_hotpath_optimization_test.go", - "backend/internal/service/gateway_image_reasoning_pricing_test.go", "backend/internal/service/gateway_model_availability.go", "backend/internal/service/gateway_model_availability_test.go", "backend/internal/service/gateway_multiplatform_test.go", - "backend/internal/service/gateway_profit_control.go", - "backend/internal/service/gateway_profit_control_v2_test.go", "backend/internal/service/gateway_scheduling.go", "backend/internal/service/gateway_service.go", "backend/internal/service/gateway_service_bedrock_beta_test.go", "backend/internal/service/gateway_service_subscription_billing_test.go", + "backend/internal/service/gateway_systemone.go", + "backend/internal/service/gateway_systemone_test.go", "backend/internal/service/gateway_usage_billing.go", "backend/internal/service/gemini_chat_completions_compat_service.go", "backend/internal/service/gemini_messages_compat_service.go", @@ -588,23 +483,11 @@ "backend/internal/service/grok_free_quota_gate_test.go", "backend/internal/service/grok_quota_service_test.go", "backend/internal/service/grok_upstream_errors_test.go", - "backend/internal/service/group.go", "backend/internal/service/group_model_allowlist.go", "backend/internal/service/group_model_allowlist_test.go", "backend/internal/service/group_model_capacity.go", "backend/internal/service/group_model_capacity_test.go", "backend/internal/service/group_peak_rate_test.go", - "backend/internal/service/group_profit_platform_test.go", - "backend/internal/service/image_studio_artifact.go", - "backend/internal/service/image_studio_artifact_test.go", - "backend/internal/service/image_studio_job.go", - "backend/internal/service/image_studio_job_service.go", - "backend/internal/service/image_studio_job_service_test.go", - "backend/internal/service/image_studio_job_test.go", - "backend/internal/service/image_studio_runtime.go", - "backend/internal/service/image_tools.go", - "backend/internal/service/image_tools_policy.go", - "backend/internal/service/image_tools_policy_test.go", "backend/internal/service/kimi_anthropic_usage_test.go", "backend/internal/service/model_context_capacity.go", "backend/internal/service/model_context_capacity_catalog_test.go", @@ -618,11 +501,6 @@ "backend/internal/service/model_context_official_catalog_test.go", "backend/internal/service/model_pricing_resolver.go", "backend/internal/service/model_rate_limit.go", - "backend/internal/service/native_feature_bootstrap.go", - "backend/internal/service/native_feature_bootstrap_test.go", - "backend/internal/service/native_policy_errors.go", - "backend/internal/service/native_policy_fixtures.go", - "backend/internal/service/native_setting_load_test.go", "backend/internal/service/oauth_service.go", "backend/internal/service/oauth_service_test.go", "backend/internal/service/openai_account_model_transient.go", @@ -633,22 +511,19 @@ "backend/internal/service/openai_account_scheduler_test.go", "backend/internal/service/openai_agent_identity_compat_test.go", "backend/internal/service/openai_alpha_search.go", - "backend/internal/service/openai_alpha_search_billing_test.go", "backend/internal/service/openai_alpha_search_test.go", "backend/internal/service/openai_apikey_compat.go", "backend/internal/service/openai_apikey_compat_test.go", + "backend/internal/service/openai_apikey_protocol_modes.go", + "backend/internal/service/openai_apikey_protocol_modes_test.go", "backend/internal/service/openai_astra_pro_mode_test.go", "backend/internal/service/openai_astra_pro_retry_test.go", "backend/internal/service/openai_budget_exceeded.go", "backend/internal/service/openai_bulk_account_settings.go", - "backend/internal/service/openai_chat_reasoning_replay.go", - "backend/internal/service/openai_chat_reasoning_replay_test.go", "backend/internal/service/openai_codex_account_identity.go", "backend/internal/service/openai_codex_account_identity_test.go", "backend/internal/service/openai_codex_client_identity.go", "backend/internal/service/openai_codex_client_identity_test.go", - "backend/internal/service/openai_codex_context_contract.go", - "backend/internal/service/openai_codex_context_contract_test.go", "backend/internal/service/openai_codex_context_window_test.go", "backend/internal/service/openai_codex_fingerprint.go", "backend/internal/service/openai_codex_fingerprint_test.go", @@ -693,7 +568,6 @@ "backend/internal/service/openai_gateway_chat_completions_raw_retry_metadata_test.go", "backend/internal/service/openai_gateway_chat_completions_raw_test.go", "backend/internal/service/openai_gateway_chat_completions_test.go", - "backend/internal/service/openai_gateway_cn_fixes_test.go", "backend/internal/service/openai_gateway_count_tokens.go", "backend/internal/service/openai_gateway_deepseek_chat_reasoning_test.go", "backend/internal/service/openai_gateway_forward.go", @@ -722,7 +596,6 @@ "backend/internal/service/openai_gateway_responses_chat_fallback_test.go", "backend/internal/service/openai_gateway_responses_client_tools_test.go", "backend/internal/service/openai_gateway_scheduling.go", - "backend/internal/service/openai_gateway_search_surcharge_test.go", "backend/internal/service/openai_gateway_service.go", "backend/internal/service/openai_gateway_service_codex_cli_only_test.go", "backend/internal/service/openai_gateway_service_codex_snapshot_test.go", @@ -769,11 +642,6 @@ "backend/internal/service/openai_previous_response_id.go", "backend/internal/service/openai_previous_response_id_test.go", "backend/internal/service/openai_privacy_service.go", - "backend/internal/service/openai_profit_control.go", - "backend/internal/service/openai_profit_control_legacy_diagnostics_test.go", - "backend/internal/service/openai_profit_control_paths_test.go", - "backend/internal/service/openai_profit_control_pricing_test.go", - "backend/internal/service/openai_profit_control_test.go", "backend/internal/service/openai_quota_auto_reset.go", "backend/internal/service/openai_quota_service.go", "backend/internal/service/openai_raw_stream_truncation.go", @@ -782,6 +650,9 @@ "backend/internal/service/openai_reasoning_effort_policy.go", "backend/internal/service/openai_reasoning_recovery.go", "backend/internal/service/openai_reasoning_recovery_integration_test.go", + "backend/internal/service/openai_reasoning_recovery_item_id_test.go", + "backend/internal/service/openai_reasoning_recovery_policy.go", + "backend/internal/service/openai_reasoning_recovery_policy_test.go", "backend/internal/service/openai_reasoning_recovery_terminal_test.go", "backend/internal/service/openai_reasoning_recovery_test.go", "backend/internal/service/openai_reasoning_state_cache.go", @@ -816,8 +687,6 @@ "backend/internal/service/openai_sticky_compat.go", "backend/internal/service/openai_sticky_compat_test.go", "backend/internal/service/openai_subscription_test.go", - "backend/internal/service/openai_tool_continuation.go", - "backend/internal/service/openai_tool_continuation_test.go", "backend/internal/service/openai_upstream_client_error_test.go", "backend/internal/service/openai_upstream_transport_error.go", "backend/internal/service/openai_upstream_transport_error_handle_test.go", @@ -856,7 +725,6 @@ "backend/internal/service/ops_metrics_collector.go", "backend/internal/service/ops_metrics_collector_projection_test.go", "backend/internal/service/ops_metrics_collector_test.go", - "backend/internal/service/ops_models.go", "backend/internal/service/ops_queue_sanitize_test.go", "backend/internal/service/ops_service.go", "backend/internal/service/ops_service_user_error_test.go", @@ -864,15 +732,8 @@ "backend/internal/service/ops_upstream_context.go", "backend/internal/service/ops_user_error_test.go", "backend/internal/service/payment_order_result_test.go", - "backend/internal/service/plugin_codex_recovery.go", - "backend/internal/service/plugin_codex_recovery_test.go", - "backend/internal/service/policy_context.go", - "backend/internal/service/policy_context_test.go", "backend/internal/service/pricing_named_family_test.go", "backend/internal/service/pricing_service.go", - "backend/internal/service/profit_control_dormant_regression_test.go", - "backend/internal/service/profit_preview.go", - "backend/internal/service/profit_preview_test.go", "backend/internal/service/quota_activity.go", "backend/internal/service/quota_activity_test.go", "backend/internal/service/quota_estimate.go", @@ -885,34 +746,28 @@ "backend/internal/service/ratelimit_service_openai_test.go", "backend/internal/service/ratelimit_session_window_test.go", "backend/internal/service/reasoning_effort_billing_test.go", - "backend/internal/service/reasoning_fidelity_control_test.go", - "backend/internal/service/reasoning_fidelity_live_test.go", - "backend/internal/service/reasoning_fidelity_parse_test.go", - "backend/internal/service/reasoning_fidelity_support_test.go", - "backend/internal/service/reasoning_replay_diagnostic_control_test.go", - "backend/internal/service/reasoning_replay_diagnostic_live_test.go", - "backend/internal/service/reasoning_replay_diagnostic_parse_test.go", - "backend/internal/service/reasoning_replay_diagnostic_support_test.go", + "backend/internal/service/reasoning_recovery_setting.go", + "backend/internal/service/reasoning_recovery_setting_test.go", "backend/internal/service/responses_chat_protocol_callers_test.go", - "backend/internal/service/scheduler_snapshot_bulk_event_test.go", "backend/internal/service/scheduler_snapshot_service.go", "backend/internal/service/setting_admin_observability.go", "backend/internal/service/setting_codex_runtime.go", "backend/internal/service/setting_gateway_runtime.go", "backend/internal/service/setting_gateway_runtime_refusal_test.go", - "backend/internal/service/setting_image_tools.go", "backend/internal/service/setting_json.go", "backend/internal/service/setting_parse.go", "backend/internal/service/setting_public.go", "backend/internal/service/setting_service.go", + "backend/internal/service/setting_switch_load_test.go", "backend/internal/service/setting_update.go", "backend/internal/service/settings_view.go", "backend/internal/service/system_prompt.go", "backend/internal/service/system_prompt_inject.go", "backend/internal/service/system_prompt_openai.go", + "backend/internal/service/system_prompt_test.go", "backend/internal/service/temp_unsched.go", "backend/internal/service/testdata/account_available_models_contract.json", - "backend/internal/service/testdata/plugin_contribution_buckets.json", + "backend/internal/service/testdata/cindy_model_metadata_v4.json", "backend/internal/service/timing_wheel_service_test.go", "backend/internal/service/token_refresh_service.go", "backend/internal/service/token_refresh_service_candidates_test.go", @@ -924,12 +779,12 @@ "backend/internal/service/upstream_model_metadata_projection_test.go", "backend/internal/service/upstream_models.go", "backend/internal/service/upstream_models_capacity_sync_test.go", + "backend/internal/service/upstream_models_cindy.go", + "backend/internal/service/upstream_models_cindy_test.go", "backend/internal/service/upstream_models_test.go", - "backend/internal/service/usage_billing.go", "backend/internal/service/usage_commit_observer_test.go", "backend/internal/service/wire.go", "backend/internal/service/wire_test.go", - "backend/internal/testextensions/fixtures.go", "backend/internal/testutil/stubs.go", "backend/internal/web/agent_route_ownership_test.go", "backend/internal/web/embed_on.go", @@ -969,6 +824,7 @@ "backend/migrations/238_opencode_go_platform.sql", "backend/migrations/239_codexrip_release_acceptance_api_key_leases.sql", "backend/migrations/240_admin_account_capabilities.sql", + "backend/migrations/241_add_typesafe_platform.sql", "backend/migrations/241_minimax_cindy_platform_quota_compat.sql", "backend/migrations/242_retire_public_model_manager.sql", "backend/migrations/243_codex_fingerprint_device_default.sql", @@ -992,35 +848,24 @@ "backend/migrations/262_account_job_error_text.sql", "backend/migrations/263_purge_retired_codex_ticket_data.sql", "backend/migrations/264_purge_codex_runtime_plugin_data.sql", - "backend/migrations/account_credential_identity_migration_test.go", + "backend/migrations/265_user_platform_quota_typesafe_union.sql", + "backend/migrations/266_purge_retired_openai_apikey_compat_settings.sql", + "backend/migrations/267_purge_account_reasoning_options.sql", + "backend/migrations/268_purge_system_prompt_bindings_without_insertion_point.sql", + "backend/migrations/269_strip_removed_remote_rules_bootstrap_from_system_prompts.sql", + "backend/migrations/270_drop_image_studio.sql", + "backend/migrations/271_drop_usage_billing_dedup_account_id.sql", + "backend/migrations/272_purge_first_party_plugins.sql", + "backend/migrations/273_purge_account_traffic_telemetry_switch.sql", + "backend/migrations/274_purge_release_acceptance_api_keys.sql", "backend/migrations/account_jobs_slim_migration_test.go", - "backend/migrations/cindy_account_stats_reset_migration_test.go", - "backend/migrations/cindy_alpha_search_bridge_migration_integration_test.go", - "backend/migrations/cindy_alpha_search_bridge_migration_test.go", - "backend/migrations/cindy_balance_confirmation_reset_migration_test.go", - "backend/migrations/cindy_balance_marker_reset_migration_integration_test.go", - "backend/migrations/cindy_balance_marker_reset_migration_test.go", - "backend/migrations/cindy_balance_marker_restore_migration_test.go", - "backend/migrations/cindy_balance_probe_jobs_migration_test.go", - "backend/migrations/cindy_channel_binding_migration_integration_test.go", - "backend/migrations/cindy_group_identity_auth_cache_invalidation_migration_test.go", - "backend/migrations/cindy_health_state_migration_test.go", - "backend/migrations/cindy_openai_defaults_migration_integration_test.go", - "backend/migrations/cindy_openai_defaults_migration_test.go", - "backend/migrations/cindy_platform_v1_migration_integration_test.go", - "backend/migrations/cindy_platform_v1_migration_test.go", - "backend/migrations/cindy_prompt_cache_hash_migration_integration_test.go", - "backend/migrations/cindy_prompt_cache_hash_migration_test.go", "backend/migrations/historical_cindy_null_total_migration_integration_test.go", - "backend/migrations/image_studio_jobs_migration_test.go", "backend/migrations/minimax_platform_migration_test.go", "backend/migrations/opencode_go_platform_migration_test.go", "backend/migrations/platform_quota_forward_union_migration_integration_test.go", - "backend/migrations/release_acceptance_api_key_lease_migration_test.go", + "backend/migrations/release_acceptance_retirement_migration_integration_test.go", "backend/migrations/remote_skill_sources_migration_integration_test.go", - "backend/migrations/user_platform_quota_cindy_migration_test.go", - "backend/pkg/pluginapi/v1/manifest.schema.json", - "backend/pkg/pluginapi/v1/runtime.go", + "backend/migrations/typesafe_platform_migration_test.go", "backend/resources/model-pricing/model_prices_and_context_window.json", "deploy/.env.example", "deploy/Dockerfile", @@ -1031,11 +876,7 @@ "deploy/docker-compose.standalone.yml", "deploy/docker-compose.yml", "deploy/docker-entrypoint.sh", - "deploy/plugin-bundle/README.md", - "deploy/tests/docker-compose-gateway-env-test.sh", - "deploy/tests/docker-runtime-resources-test.sh", "deploy/tests/production-deploy-workflow-test.sh", - "docs/channel-monitor-v2-safe-defaults.md", "frontend/index.html", "frontend/package.json", "frontend/pnpm-lock.yaml", @@ -1048,19 +889,14 @@ "frontend/src/api/__tests__/client.spec.ts", "frontend/src/api/__tests__/codex.spec.ts", "frontend/src/api/__tests__/downstreamStatus.spec.ts", - "frontend/src/api/__tests__/pluginRawConfig.spec.ts", "frontend/src/api/__tests__/settings.authSourceDefaults.spec.ts", "frontend/src/api/accountOperationIdempotency.ts", "frontend/src/api/admin/__tests__/accountJobs.spec.ts", "frontend/src/api/admin/accountJobs.ts", - "frontend/src/api/admin/accountViewClient.ts", "frontend/src/api/admin/accounts.ts", - "frontend/src/api/admin/cnProviders.ts", "frontend/src/api/admin/codexRuntime.ts", - "frontend/src/api/admin/grok.ts", "frontend/src/api/admin/index.ts", - "frontend/src/api/admin/plugins.ts", - "frontend/src/api/admin/scheduledTests.ts", + "frontend/src/api/admin/reasoningRecovery.ts", "frontend/src/api/admin/settings.ts", "frontend/src/api/admin/system.ts", "frontend/src/api/admin/systemPrompts.ts", @@ -1068,9 +904,6 @@ "frontend/src/api/client.ts", "frontend/src/api/codex.ts", "frontend/src/api/downstreamStatus.ts", - "frontend/src/assets/fonts/geist/Geist-Variable.latin.woff2", - "frontend/src/assets/fonts/geist/GeistMono-Variable.latin.woff2", - "frontend/src/assets/fonts/geist/OFL.txt", "frontend/src/components/account/AccountCapacityCell.vue", "frontend/src/components/account/AccountGroupsCell.vue", "frontend/src/components/account/AccountIdentityBadges.vue", @@ -1080,22 +913,17 @@ "frontend/src/components/account/AccountTestModal.vue", "frontend/src/components/account/AccountUsageCell.vue", "frontend/src/components/account/BulkEditAccountModal.vue", - "frontend/src/components/account/CNProviderBalanceCell.vue", - "frontend/src/components/account/CNProviderQuotaCell.vue", "frontend/src/components/account/ClaudeResetCreditsCell.vue", "frontend/src/components/account/CreateAccountModal.vue", "frontend/src/components/account/EditAccountModal.vue", - "frontend/src/components/account/GrokQuotaProbeCell.vue", "frontend/src/components/account/HeaderOverrideEditor.vue", "frontend/src/components/account/ModelContextCapacityField.vue", "frontend/src/components/account/ModelWhitelistSelector.vue", "frontend/src/components/account/OAuthAuthorizationFlow.vue", "frontend/src/components/account/OllamaCloudUsageCell.vue", "frontend/src/components/account/OpenAIQuotaResetCell.vue", - "frontend/src/components/account/OpenAIReasoningPolicyFields.vue", "frontend/src/components/account/OpenCodeGoProtocolRulesEditor.vue", "frontend/src/components/account/QuotaNotifyToggle.vue", - "frontend/src/components/account/TempUnschedStatusModal.vue", "frontend/src/components/account/UpstreamModelPickerDialog.vue", "frontend/src/components/account/UsageProgressBar.vue", "frontend/src/components/account/__tests__/AccountStatusIndicator.spec.ts", @@ -1110,15 +938,11 @@ "frontend/src/components/account/__tests__/ModelContextCapacityField.spec.ts", "frontend/src/components/account/__tests__/ModelWhitelistSelector.spec.ts", "frontend/src/components/account/__tests__/OpenAIQuotaResetCell.spark_shadow.spec.ts", - "frontend/src/components/account/__tests__/OpenAIReasoningPolicyFields.spec.ts", - "frontend/src/components/account/__tests__/TempUnschedStatusModal.spec.ts", "frontend/src/components/account/__tests__/UsageProgressBar.spec.ts", "frontend/src/components/account/__tests__/credentialsBuilder.spec.ts", "frontend/src/components/account/credentialsBuilder.ts", - "frontend/src/components/admin/ImageToolsSettingsPanel.vue", "frontend/src/components/admin/ObservabilitySettingsPanel.vue", "frontend/src/components/admin/OfficialModelCatalogPanel.vue", - "frontend/src/components/admin/RetiredPluginsPanel.vue", "frontend/src/components/admin/account-jobs/AccountOperationConfirmDialog.vue", "frontend/src/components/admin/account-jobs/AccountOperationDialog.vue", "frontend/src/components/admin/account-jobs/AccountOperationProgress.vue", @@ -1137,7 +961,6 @@ "frontend/src/components/admin/account/AccountLastUpstreamError.vue", "frontend/src/components/admin/account/AccountStatsModal.vue", "frontend/src/components/admin/account/AccountSystemPromptBinding.vue", - "frontend/src/components/admin/account/AccountTableFilters.vue", "frontend/src/components/admin/account/AccountTaxonomyEditor.vue", "frontend/src/components/admin/account/AccountTaxonomyManager.vue", "frontend/src/components/admin/account/AccountTestModal.vue", @@ -1149,24 +972,28 @@ "frontend/src/components/admin/account/ImportDataModal.vue", "frontend/src/components/admin/account/ReAuthAccountModal.vue", "frontend/src/components/admin/account/ScheduledTestsPanel.vue", + "frontend/src/components/admin/account/__tests__/AccountActionMenu.spark_shadow.spec.ts", "frontend/src/components/admin/account/__tests__/AccountBulkActionsBar.spec.ts", "frontend/src/components/admin/account/__tests__/AccountBulkTaxonomyModal.spec.ts", "frontend/src/components/admin/account/__tests__/AccountConsoleFilters.spec.ts", "frontend/src/components/admin/account/__tests__/AccountDetailsDrawer.spec.ts", "frontend/src/components/admin/account/__tests__/AccountFolderBar.spec.ts", "frontend/src/components/admin/account/__tests__/AccountLastUpstreamError.spec.ts", + "frontend/src/components/admin/account/__tests__/AccountStatsModal.spec.ts", "frontend/src/components/admin/account/__tests__/AccountTaxonomyManager.spec.ts", "frontend/src/components/admin/account/__tests__/AccountTestModal.displayContract.spec.ts", - "frontend/src/components/admin/account/__tests__/AccountTestModal.policyPlan.spec.ts", "frontend/src/components/admin/account/__tests__/AccountTestModal.qualityCheck.spec.ts", "frontend/src/components/admin/account/__tests__/AccountTestModal.spec.ts", + "frontend/src/components/admin/account/__tests__/AccountTestModal.testPlan.spec.ts", "frontend/src/components/admin/account/__tests__/AccountUsageViews.spec.ts", "frontend/src/components/admin/account/__tests__/ImportDataModal.spec.ts", + "frontend/src/components/admin/account/__tests__/ReAuthAccountModal.spec.ts", + "frontend/src/components/admin/account/__tests__/ScheduledTestsPanel.spec.ts", "frontend/src/components/admin/announcements/AnnouncementReadStatusDialog.vue", "frontend/src/components/admin/announcements/AnnouncementTargetingEditor.vue", "frontend/src/components/admin/codex/CodexContinuationDiagnostics.vue", "frontend/src/components/admin/codex/CodexRuntimeSettings.vue", - "frontend/src/components/admin/codex/__tests__/CodexRuntime.native.spec.ts", + "frontend/src/components/admin/codex/__tests__/CodexRuntime.spec.ts", "frontend/src/components/admin/group/CodexManifestAccountsField.vue", "frontend/src/components/admin/group/GroupRPMOverridesModal.vue", "frontend/src/components/admin/group/GroupRateMultipliersModal.vue", @@ -1177,6 +1004,7 @@ "frontend/src/components/admin/monitor/MonitorTemplateManagerDialog.vue", "frontend/src/components/admin/payment/DailyRevenueChart.vue", "frontend/src/components/admin/payment/OrderStatsCards.vue", + "frontend/src/components/admin/settings/RechargeBonusTierEditor.vue", "frontend/src/components/admin/usage/UsageFilters.vue", "frontend/src/components/admin/usage/UsageStatsCards.vue", "frontend/src/components/admin/usage/UsageTable.vue", @@ -1221,7 +1049,6 @@ "frontend/src/components/common/__tests__/BaseDialog.spec.ts", "frontend/src/components/common/__tests__/DataTable.spec.ts", "frontend/src/components/common/__tests__/PlatformTypeBadge.openaiPlans.spec.ts", - "frontend/src/components/common/__tests__/Select.disabled.spec.ts", "frontend/src/components/common/__tests__/VersionBadge.downstream.spec.ts", "frontend/src/components/common/columnStack.ts", "frontend/src/components/common/dialogStack.ts", @@ -1236,6 +1063,7 @@ "frontend/src/components/layout/AuthLayout.vue", "frontend/src/components/layout/AuthPixelGrid.vue", "frontend/src/components/layout/TablePageLayout.vue", + "frontend/src/components/layout/__tests__/AppLayout.accountTaskDrawer.spec.ts", "frontend/src/components/layout/__tests__/AppSidebar.simple-mode.spec.ts", "frontend/src/components/layout/__tests__/AppSidebar.spec.ts", "frontend/src/components/modelPlaza/PlazaFilterBar.vue", @@ -1243,7 +1071,6 @@ "frontend/src/components/payment/StripePaymentInline.vue", "frontend/src/components/payment/SubscriptionPlanCard.vue", "frontend/src/components/payment/__tests__/StripePaymentInline.contrast.spec.ts", - "frontend/src/components/payment/__tests__/SubscriptionPlanCard.spec.ts", "frontend/src/components/user/MonitorDetailDialog.vue", "frontend/src/components/user/UserAttributesConfigModal.vue", "frontend/src/components/user/UserErrorRequestsTable.vue", @@ -1251,6 +1078,7 @@ "frontend/src/components/user/dashboard/UserDashboardCharts.vue", "frontend/src/components/user/dashboard/UserDashboardQuickActions.vue", "frontend/src/components/user/dashboard/UserDashboardStats.vue", + "frontend/src/components/user/dashboard/__tests__/UserDashboardStats.spec.ts", "frontend/src/components/user/monitor/MonitorCard.vue", "frontend/src/components/user/monitor/MonitorCardGrid.vue", "frontend/src/components/user/monitor/MonitorHero.vue", @@ -1260,64 +1088,40 @@ "frontend/src/composables/__tests__/useModelContextCapacities.spec.ts", "frontend/src/composables/__tests__/useModelWhitelist.spec.ts", "frontend/src/composables/useAccountTestPrompt.ts", - "frontend/src/composables/useAccountViewContext.ts", "frontend/src/composables/useModelContextCapacities.ts", "frontend/src/composables/useModelWhitelist.ts", "frontend/src/composables/usePresentationColors.ts", "frontend/src/constants/__tests__/platforms.spec.ts", "frontend/src/constants/platforms.ts", - "frontend/src/features/account-jobs/__tests__/integrationSurface.spec.ts", "frontend/src/features/channel-monitor-v2/FilterMultiSelect.vue", "frontend/src/features/channel-monitor-v2/MonitorTrendChart.vue", "frontend/src/features/channel-monitor-v2/RelayPulseMatrix.vue", - "frontend/src/features/image-studio/ImageStudioPage.vue", - "frontend/src/features/image-studio/__tests__/ImageStudioPage.spec.ts", - "frontend/src/features/image-studio/__tests__/api.spec.ts", - "frontend/src/features/image-studio/api.ts", - "frontend/src/features/image-studio/history.legacy.spec.ts", - "frontend/src/features/image-studio/history.spec.ts", - "frontend/src/features/image-studio/history.ts", - "frontend/src/features/image-studio/locales/en.ts", - "frontend/src/features/image-studio/locales/zh.ts", "frontend/src/features/prompt-audit/PromptAuditView.vue", - "frontend/src/features/prompt-audit/__tests__/PromptAuditView.spec.ts", - "frontend/src/features/prompt-audit/__tests__/components.spec.ts", - "frontend/src/features/prompt-audit/components/EndpointPool.vue", "frontend/src/features/prompt-audit/components/EventWorkspace.vue", - "frontend/src/features/prompt-audit/viewModel.ts", "frontend/src/i18n/__tests__/accountConsoleLocales.spec.ts", "frontend/src/i18n/locales/en/admin/accountTasks.ts", "frontend/src/i18n/locales/en/admin/accounts.ts", "frontend/src/i18n/locales/en/admin/index.ts", - "frontend/src/i18n/locales/en/admin/overview.ts", - "frontend/src/i18n/locales/en/admin/plugins.ts", + "frontend/src/i18n/locales/en/admin/reasoningRecovery.ts", "frontend/src/i18n/locales/en/admin/resources.ts", "frontend/src/i18n/locales/en/admin/settings.ts", "frontend/src/i18n/locales/en/admin/systemPrompts.ts", "frontend/src/i18n/locales/en/common.ts", "frontend/src/i18n/locales/en/dashboard.ts", - "frontend/src/i18n/locales/en/imageStudio.ts", - "frontend/src/i18n/locales/en/index.ts", "frontend/src/i18n/locales/en/misc.ts", "frontend/src/i18n/locales/zh/admin/accountTasks.ts", "frontend/src/i18n/locales/zh/admin/accounts.ts", "frontend/src/i18n/locales/zh/admin/index.ts", - "frontend/src/i18n/locales/zh/admin/overview.ts", - "frontend/src/i18n/locales/zh/admin/plugins.ts", + "frontend/src/i18n/locales/zh/admin/reasoningRecovery.ts", "frontend/src/i18n/locales/zh/admin/resources.ts", "frontend/src/i18n/locales/zh/admin/settings.ts", "frontend/src/i18n/locales/zh/admin/systemPrompts.ts", "frontend/src/i18n/locales/zh/common.ts", "frontend/src/i18n/locales/zh/dashboard.ts", - "frontend/src/i18n/locales/zh/imageStudio.ts", - "frontend/src/i18n/locales/zh/index.ts", "frontend/src/i18n/locales/zh/misc.ts", "frontend/src/main.ts", - "frontend/src/router/__tests__/feature-access.spec.ts", "frontend/src/router/index.ts", - "frontend/src/router/meta.d.ts", "frontend/src/stores/__tests__/accountJobs.spec.ts", - "frontend/src/stores/__tests__/app.spec.ts", "frontend/src/stores/accountJobs.ts", "frontend/src/stores/app.ts", "frontend/src/stores/index.ts", @@ -1336,42 +1140,33 @@ "frontend/src/styles/console/tables.css", "frontend/src/styles/console/tokens.css", "frontend/src/styles/theme.css", - "frontend/src/types/accountEdit.ts", - "frontend/src/types/imageStudio.ts", "frontend/src/types/index.ts", - "frontend/src/utils/__tests__/accountEditCodec.spec.ts", + "frontend/src/utils/__tests__/accountKeySearch.spec.ts", "frontend/src/utils/__tests__/accountQualityCheck.spec.ts", - "frontend/src/utils/__tests__/imageStudioLocalResources.spec.ts", "frontend/src/utils/__tests__/modelContextCapacity.spec.ts", - "frontend/src/utils/__tests__/openaiReasoningPolicy.spec.ts", + "frontend/src/utils/__tests__/openaiProtocolModeEdits.spec.ts", "frontend/src/utils/__tests__/softWrap.spec.ts", - "frontend/src/utils/accountEditCodec.ts", "frontend/src/utils/accountImportParser.ts", "frontend/src/utils/accountImportWorker.ts", + "frontend/src/utils/accountKeySearch.ts", "frontend/src/utils/accountPresentation.ts", "frontend/src/utils/accountQualityCheck.ts", - "frontend/src/utils/accountQuotaWindows.ts", "frontend/src/utils/accountTestModels.ts", - "frontend/src/utils/browserActions.ts", - "frontend/src/utils/browserPreferences.ts", "frontend/src/utils/ccswitchImport.ts", "frontend/src/utils/codexCatalogConfig.ts", - "frontend/src/utils/featureFlags.ts", "frontend/src/utils/flatTheme.ts", - "frontend/src/utils/imageStudioLocalResources.ts", "frontend/src/utils/keyGroupProviders.ts", "frontend/src/utils/modelContextCapacity.ts", - "frontend/src/utils/nativeResource.ts", - "frontend/src/utils/openaiReasoningPolicy.ts", + "frontend/src/utils/openaiProtocolModeEdits.ts", "frontend/src/utils/planType.ts", "frontend/src/utils/platformColors.ts", "frontend/src/utils/softWrap.ts", "frontend/src/utils/systemPromptBinding.ts", + "frontend/src/utils/upstreamModelSync.ts", "frontend/src/views/HomeView.vue", "frontend/src/views/KeyUsageView.vue", "frontend/src/views/ModelPlazaView.vue", "frontend/src/views/NotFoundView.vue", - "frontend/src/views/admin/AccountTasksView.vue", "frontend/src/views/admin/AccountsView.vue", "frontend/src/views/admin/AnnouncementsView.vue", "frontend/src/views/admin/AuditLogView.vue", @@ -1384,6 +1179,7 @@ "frontend/src/views/admin/PluginsView.vue", "frontend/src/views/admin/PromoCodesView.vue", "frontend/src/views/admin/ProxiesView.vue", + "frontend/src/views/admin/ReasoningRecoveryView.vue", "frontend/src/views/admin/RedeemView.vue", "frontend/src/views/admin/RiskControlView.vue", "frontend/src/views/admin/SettingsView.vue", @@ -1391,7 +1187,6 @@ "frontend/src/views/admin/SystemPromptsView.vue", "frontend/src/views/admin/UsageView.vue", "frontend/src/views/admin/UsersView.vue", - "frontend/src/views/admin/__tests__/AccountTasksView.spec.ts", "frontend/src/views/admin/__tests__/AccountsView.bulkEdit.spec.ts", "frontend/src/views/admin/__tests__/AccountsView.console.spec.ts", "frontend/src/views/admin/__tests__/AccountsView.lite.spec.ts", @@ -1403,16 +1198,14 @@ "frontend/src/views/admin/__tests__/AccountsView.usageWindowsHint.spec.ts", "frontend/src/views/admin/__tests__/GroupsView.codexManifest.spec.ts", "frontend/src/views/admin/__tests__/GroupsView.duplicate.spec.ts", - "frontend/src/views/admin/__tests__/PluginsView.spec.ts", "frontend/src/views/admin/__tests__/ProxiesView.copyFormats.spec.ts", + "frontend/src/views/admin/__tests__/ReasoningRecoveryView.spec.ts", "frontend/src/views/admin/__tests__/SettingsView.spec.ts", "frontend/src/views/admin/__tests__/channelPlatformOptions.spec.ts", "frontend/src/views/admin/__tests__/groupModelAllowlist.spec.ts", - "frontend/src/views/admin/__tests__/groupsProfitControl.spec.ts", "frontend/src/views/admin/affiliates/AdminAffiliateRecordsTable.vue", "frontend/src/views/admin/affiliates/AffiliateOfflineWithdrawDialog.vue", "frontend/src/views/admin/groupModelAllowlist.ts", - "frontend/src/views/admin/groupsProfitControl.ts", "frontend/src/views/admin/ops/OpsDashboard.vue", "frontend/src/views/admin/ops/components/OpsAlertEventsCard.vue", "frontend/src/views/admin/ops/components/OpsAlertRulesCard.vue", @@ -1442,7 +1235,6 @@ "frontend/src/views/user/ChannelStatusV2View.vue", "frontend/src/views/user/CustomPageView.vue", "frontend/src/views/user/DashboardView.vue", - "frontend/src/views/user/ImageStudioView.vue", "frontend/src/views/user/KeysView.vue", "frontend/src/views/user/PaymentView.vue", "frontend/src/views/user/RedeemView.vue", @@ -1453,259 +1245,59 @@ "frontend/src/views/user/__tests__/StripePaymentView.spec.ts", "frontend/src/workers/accountImport.worker.ts", "frontend/tailwind.config.js", - "frontend/tsconfig.json", "frontend/vite.config.ts", - "frontend/vitest.config.ts" + "tools/check_pnpm_audit_exceptions.py" ], "initial_conflict_files": [ - "backend/internal/handler/gateway_handler.go", - "backend/internal/server/api_contract_test.go", - "backend/internal/service/account_service.go", - "backend/internal/service/account_test_service.go", - "backend/internal/service/admin_account.go", - "backend/internal/service/billing_service.go", - "frontend/src/api/admin/settings.ts", - "frontend/src/api/admin/users.ts", - "frontend/src/components/account/EditAccountModal.vue", - "frontend/src/components/admin/user/__tests__/UserPlatformQuotaModal.spec.ts", - "frontend/src/components/user/UserPlatformQuotaCell.vue", - "frontend/src/components/user/dashboard/UserDashboardStats.vue", - "frontend/src/utils/platformColors.ts", - "frontend/src/views/admin/AccountsView.vue", - "frontend/src/views/admin/SettingsView.vue", - "frontend/src/views/admin/__tests__/SettingsView.spec.ts" + ".github/audit-exceptions.yml", + "frontend/package.json", + "frontend/pnpm-lock.yaml" ], "merge_conflicts": [], - "overlap_file_count": 65, + "overlap_file_count": 11, "overlap_files": [ - "README.md", - "README_CN.md", + ".github/audit-exceptions.yml", "backend/cmd/server/VERSION", - "backend/ent/migrate/schema.go", - "backend/ent/mutation.go", - "backend/ent/runtime/runtime.go", - "backend/internal/handler/admin/account_handler.go", - "backend/internal/handler/admin/account_handler_available_models_test.go", - "backend/internal/handler/admin/group_handler.go", - "backend/internal/handler/admin/setting_handler.go", - "backend/internal/handler/admin/setting_handler_update.go", - "backend/internal/handler/dto/settings.go", - "backend/internal/handler/gateway_handler.go", - "backend/internal/handler/gateway_handler_chat_completions.go", - "backend/internal/handler/gateway_handler_responses.go", - "backend/internal/handler/gateway_models_test.go", - "backend/internal/repository/api_key_repo.go", - "backend/internal/repository/http_upstream.go", - "backend/internal/repository/usage_billing_repo.go", - "backend/internal/server/api_contract_test.go", - "backend/internal/server/router.go", - "backend/internal/server/routes/gateway.go", - "backend/internal/server/routes/gateway_model_allowlist_test.go", - "backend/internal/service/account.go", - "backend/internal/service/account_service.go", - "backend/internal/service/account_test_service.go", - "backend/internal/service/admin_account.go", - "backend/internal/service/antigravity_gateway_gemini.go", - "backend/internal/service/billing_service.go", - "backend/internal/service/composite_platform_test.go", - "backend/internal/service/domain_constants.go", - "backend/internal/service/openai_gateway_grok.go", - "backend/internal/service/scheduler_snapshot_service.go", + "deploy/.env.example", + "deploy/README.md", + "deploy/docker-compose.dev.yml", + "deploy/docker-compose.local.yml", + "deploy/docker-compose.standalone.yml", + "deploy/docker-compose.yml", "frontend/package.json", "frontend/pnpm-lock.yaml", - "frontend/src/api/admin/settings.ts", - "frontend/src/api/admin/users.ts", - "frontend/src/components/account/CreateAccountModal.vue", - "frontend/src/components/account/EditAccountModal.vue", - "frontend/src/components/admin/user/__tests__/UserPlatformQuotaModal.spec.ts", - "frontend/src/components/keys/UseKeyModal.vue", - "frontend/src/components/user/UserPlatformQuotaCell.vue", - "frontend/src/components/user/dashboard/UserDashboardStats.vue", - "frontend/src/composables/useModelWhitelist.ts", - "frontend/src/constants/__tests__/platforms.spec.ts", - "frontend/src/constants/platforms.ts", - "frontend/src/i18n/locales/en/admin/accounts.ts", - "frontend/src/i18n/locales/en/admin/overview.ts", - "frontend/src/i18n/locales/en/admin/settings.ts", - "frontend/src/i18n/locales/en/dashboard.ts", - "frontend/src/i18n/locales/en/misc.ts", - "frontend/src/i18n/locales/zh/admin/accounts.ts", - "frontend/src/i18n/locales/zh/admin/overview.ts", - "frontend/src/i18n/locales/zh/admin/settings.ts", - "frontend/src/i18n/locales/zh/dashboard.ts", - "frontend/src/i18n/locales/zh/misc.ts", - "frontend/src/types/index.ts", - "frontend/src/utils/keyGroupProviders.ts", - "frontend/src/utils/platformColors.ts", - "frontend/src/views/admin/AccountsView.vue", - "frontend/src/views/admin/ChannelsView.vue", - "frontend/src/views/admin/SettingsView.vue", - "frontend/src/views/admin/__tests__/SettingsView.spec.ts", - "frontend/src/views/user/KeysView.vue", - "frontend/src/views/user/PaymentView.vue" + "frontend/src/components/keys/UseKeyModal.vue" ], "risk_class": "review_required", "schema": 1, - "upstream_base": "v0.2.11", - "upstream_file_count": 157, + "upstream_base": "v0.2.13", + "upstream_file_count": 25, "upstream_files": [ - ".github/SECURITY.md", + ".github/audit-exceptions.yml", "README.md", "README_CN.md", + "README_JA.md", "backend/cmd/server/VERSION", - "backend/ent/migrate/schema.go", - "backend/ent/mutation.go", - "backend/ent/paymentorder.go", - "backend/ent/paymentorder/paymentorder.go", - "backend/ent/paymentorder/where.go", - "backend/ent/paymentorder_create.go", - "backend/ent/paymentorder_update.go", - "backend/ent/runtime/runtime.go", - "backend/ent/schema/payment_order.go", - "backend/ent/schema/user_platform_quota.go", - "backend/internal/domain/constants.go", - "backend/internal/handler/admin/account_handler.go", - "backend/internal/handler/admin/account_handler_available_models_test.go", - "backend/internal/handler/admin/channel_handler.go", - "backend/internal/handler/admin/channel_handler_test.go", - "backend/internal/handler/admin/group_handler.go", - "backend/internal/handler/admin/group_handler_platform_test.go", - "backend/internal/handler/admin/payment_handler.go", - "backend/internal/handler/admin/setting_handler.go", - "backend/internal/handler/admin/setting_handler_recharge_bonus.go", - "backend/internal/handler/admin/setting_handler_update.go", - "backend/internal/handler/auth_oauth_pending_flow_test.go", - "backend/internal/handler/dto/recharge_bonus_tiers.go", - "backend/internal/handler/dto/settings.go", - "backend/internal/handler/endpoint.go", - "backend/internal/handler/endpoint_test.go", - "backend/internal/handler/gateway_handler.go", - "backend/internal/handler/gateway_handler_chat_completions.go", - "backend/internal/handler/gateway_handler_responses.go", - "backend/internal/handler/gateway_models_retrieve_test.go", - "backend/internal/handler/gateway_models_test.go", - "backend/internal/handler/gateway_systemone.go", - "backend/internal/handler/gateway_systemone_test.go", - "backend/internal/handler/payment_handler.go", - "backend/internal/handler/user_handler_test.go", - "backend/internal/model/error_passthrough_rule.go", - "backend/internal/model/error_passthrough_rule_test.go", - "backend/internal/pkg/typesafe/client.go", - "backend/internal/pkg/typesafe/systemone.go", - "backend/internal/pkg/typesafe/systemone_test.go", - "backend/internal/pkg/xai/billing.go", - "backend/internal/pkg/xai/billing_test.go", - "backend/internal/pkg/xai/cli_identity.go", - "backend/internal/pkg/xai/cli_identity_test.go", - "backend/internal/repository/api_key_repo.go", - "backend/internal/repository/api_key_repo_sort_test.go", - "backend/internal/repository/email_cache.go", - "backend/internal/repository/email_cache_atomic_test.go", - "backend/internal/repository/http_upstream.go", - "backend/internal/repository/http_upstream_test.go", - "backend/internal/repository/usage_billing_repo.go", - "backend/internal/repository/usage_billing_repo_integration_test.go", - "backend/internal/repository/usage_billing_repo_unit_test.go", - "backend/internal/securityaudit/prompt_snapshot.go", - "backend/internal/securityaudit/prompt_snapshot_test.go", - "backend/internal/server/api_contract_test.go", - "backend/internal/server/router.go", - "backend/internal/server/routes/gateway.go", - "backend/internal/server/routes/gateway_model_allowlist_test.go", - "backend/internal/server/routes/payment.go", - "backend/internal/server/routes/payment_public_rate_limit_test.go", - "backend/internal/server/routes/prompt_audit_route_coverage_test.go", - "backend/internal/service/account.go", - "backend/internal/service/account_service.go", - "backend/internal/service/account_test_service.go", - "backend/internal/service/account_test_service_typesafe.go", - "backend/internal/service/account_test_service_typesafe_test.go", - "backend/internal/service/admin_account.go", - "backend/internal/service/admin_group.go", - "backend/internal/service/antigravity_gateway_gemini.go", - "backend/internal/service/antigravity_upstream_error_sanitize.go", - "backend/internal/service/antigravity_upstream_error_sanitize_test.go", - "backend/internal/service/auth_service_email_bind_test.go", - "backend/internal/service/auth_service_register_test.go", - "backend/internal/service/billing_service.go", - "backend/internal/service/billing_service_test.go", - "backend/internal/service/channel_service.go", - "backend/internal/service/channel_service_test.go", - "backend/internal/service/composite_platform.go", - "backend/internal/service/composite_platform_test.go", - "backend/internal/service/content_moderation.go", - "backend/internal/service/content_moderation_input.go", - "backend/internal/service/content_moderation_systemone_test.go", - "backend/internal/service/domain_constants.go", - "backend/internal/service/email_service.go", - "backend/internal/service/email_service_reset_token_test.go", - "backend/internal/service/gateway_systemone.go", - "backend/internal/service/gateway_systemone_test.go", - "backend/internal/service/grok_upstream_headers.go", - "backend/internal/service/grok_upstream_headers_test.go", - "backend/internal/service/openai_gateway_grok.go", - "backend/internal/service/payment_config_service.go", - "backend/internal/service/payment_fulfillment.go", - "backend/internal/service/payment_order.go", - "backend/internal/service/payment_order_provider_snapshot_test.go", - "backend/internal/service/payment_recharge_bonus.go", - "backend/internal/service/payment_recharge_bonus_test.go", - "backend/internal/service/payment_service.go", - "backend/internal/service/scheduler_snapshot_service.go", - "backend/internal/service/upstream_billing_probe.go", - "backend/internal/service/upstream_billing_probe_multiplatform_test.go", - "backend/internal/service/user_service.go", - "backend/migrations/241_add_payment_order_bonus_amount.sql", - "backend/migrations/241_add_typesafe_platform.sql", - "backend/migrations/typesafe_platform_migration_test.go", + "backend/internal/integration/e2e_user_flow_test.go", + "backend/internal/payment/provider/easypay.go", + "backend/internal/payment/provider/easypay_notify_security_test.go", + "backend/internal/service/payment_resume_service.go", + "backend/internal/service/payment_resume_service_test.go", + "backend/internal/setup/cli.go", + "backend/internal/setup/handler.go", + "backend/internal/setup/setup.go", + "backend/internal/setup/setup_test.go", + "deploy/.env.example", + "deploy/APPLE_CONTAINER.md", + "deploy/README.md", + "deploy/docker-compose.dev.yml", + "deploy/docker-compose.local.yml", + "deploy/docker-compose.standalone.yml", + "deploy/docker-compose.yml", + "deploy/docker-deploy.sh", "frontend/package.json", "frontend/pnpm-lock.yaml", - "frontend/src/api/admin/settings.ts", - "frontend/src/api/admin/users.ts", - "frontend/src/components/account/AccountPriorityCell.vue", - "frontend/src/components/account/CreateAccountModal.vue", - "frontend/src/components/account/EditAccountModal.vue", - "frontend/src/components/account/__tests__/AccountPriorityCell.spec.ts", - "frontend/src/components/admin/payment/AdminOrderDetail.vue", - "frontend/src/components/admin/payment/AdminOrderTable.vue", - "frontend/src/components/admin/settings/RechargeBonusTierEditor.vue", - "frontend/src/components/admin/user/__tests__/UserPlatformQuotaModal.spec.ts", - "frontend/src/components/keys/UseKeyModal.vue", - "frontend/src/components/payment/AmountInput.vue", - "frontend/src/components/payment/OrderTable.vue", - "frontend/src/components/payment/PaymentStatusPanel.vue", - "frontend/src/components/payment/__tests__/AmountInput.spec.ts", - "frontend/src/components/user/UserPlatformQuotaCell.vue", - "frontend/src/components/user/__tests__/UserPlatformQuotaCell.spec.ts", - "frontend/src/components/user/dashboard/UserDashboardStats.vue", - "frontend/src/components/user/dashboard/__tests__/UserDashboardStats.spec.ts", - "frontend/src/composables/useModelWhitelist.ts", - "frontend/src/constants/__tests__/platforms.spec.ts", - "frontend/src/constants/platforms.ts", - "frontend/src/i18n/locales/en/admin/accounts.ts", - "frontend/src/i18n/locales/en/admin/overview.ts", - "frontend/src/i18n/locales/en/admin/settings.ts", - "frontend/src/i18n/locales/en/dashboard.ts", - "frontend/src/i18n/locales/en/misc.ts", - "frontend/src/i18n/locales/zh/admin/accounts.ts", - "frontend/src/i18n/locales/zh/admin/overview.ts", - "frontend/src/i18n/locales/zh/admin/settings.ts", - "frontend/src/i18n/locales/zh/dashboard.ts", - "frontend/src/i18n/locales/zh/misc.ts", - "frontend/src/types/index.ts", - "frontend/src/types/payment.ts", - "frontend/src/utils/__tests__/rechargeBonus.spec.ts", - "frontend/src/utils/keyGroupProviders.ts", - "frontend/src/utils/platformColors.ts", - "frontend/src/utils/rechargeBonus.ts", - "frontend/src/views/admin/AccountsView.vue", - "frontend/src/views/admin/ChannelsView.vue", - "frontend/src/views/admin/SettingsView.vue", - "frontend/src/views/admin/__tests__/SettingsView.spec.ts", - "frontend/src/views/user/KeysView.vue", - "frontend/src/views/user/PaymentResultView.vue", - "frontend/src/views/user/PaymentView.vue", - "frontend/src/views/user/__tests__/KeysView.spec.ts" + "frontend/src/components/keys/UseKeyModal.vue" ], - "upstream_tag": "v0.2.13" + "upstream_tag": "v0.2.14" } diff --git a/DOWNSTREAM.md b/DOWNSTREAM.md index 6087dec374fc..44bab9bf1dd5 100644 --- a/DOWNSTREAM.md +++ b/DOWNSTREAM.md @@ -5,7 +5,7 @@ This fork maintains the `codexrip` patch set over official Sub2API releases. ## Source baseline - Integrated official baseline: the release recorded in `.downstream/upstream-base`. -- Retained compatibility decisions: [upstream review](.downstream/upstream-review-v0.2.13.md). +- Retained compatibility decisions: [upstream review](.downstream/upstream-review-v0.2.14.md). - The operator workspace `docs/sub2api.md` owns the production version and image digest. A source merge or Release does not establish deployment completion. ## Settings and account operations @@ -145,13 +145,9 @@ Scheduled discovery may identify newer official releases. It cannot deploy production. Each integration records its selected official commit, conflicts and retained downstream contracts before a new immutable release is published. -`.downstream/upstream-risk.json` is the recorded integration snapshot, not a live -worktree conflict report. Its current classifier inputs are upstream `v0.2.11` -through `v0.2.13` and downstream commit -`291d25e7f8a8c82d45072a1eee742c21ba3b8d21`. `initial_conflict_files` supplements -the classifier output with paths reviewed during the merge, including files -resolved automatically; it is historical review evidence rather than a list of -remaining unmerged paths. The final `merge_conflicts: []` records the resolved -merge. The risk gate recomputes from the tags and downstream commit in that -snapshot, rather than the current PR head, so later-deleted paths remain in its -classification; `review_required` still requires the `upstream-reviewed` label. +`.downstream/upstream-risk.json` records the v0.2.13-to-v0.2.14 integration against +downstream commit `7132011d7a4275e7d6407c92db4ec205a1c14ad1`. The risk gate +recomputes those exact inputs. `initial_conflict_files` lists the three dependency +conflicts resolved during this merge; `merge_conflicts: []` records that none +remain. A `review_required` candidate needs a real completed review and the +`upstream-reviewed` label before promotion. diff --git a/README.md b/README.md index 8ec0b7f59c50..1e8ea3f0beed 100644 --- a/README.md +++ b/README.md @@ -354,8 +354,10 @@ JWT_SECRET=your_jwt_secret_here TOTP_ENCRYPTION_KEY=your_totp_key_here # Optional: Admin account -ADMIN_EMAIL=admin@example.com -ADMIN_PASSWORD=your_admin_password +# Leave empty to auto-generate a random email (login username) and password, shown in logs on first startup. +# Avoid guessable values such as admin@example.com: they are brute-force targets. +ADMIN_EMAIL= +ADMIN_PASSWORD= # Optional: Custom port SERVER_PORT=8080 @@ -404,9 +406,9 @@ docker compose -f docker-compose.local.yml logs -f sub2api Open `http://YOUR_SERVER_IP:8080` in your browser. -If admin password was auto-generated, find it in logs: +If the admin email (login username) or password was auto-generated, find them in logs: ```bash -docker compose -f docker-compose.local.yml logs sub2api | grep "admin password" +docker compose -f docker-compose.local.yml logs sub2api | grep "Generated admin" ``` #### Upgrade diff --git a/README_CN.md b/README_CN.md index 70eef1990ddc..57d92ca92a2f 100644 --- a/README_CN.md +++ b/README_CN.md @@ -380,8 +380,10 @@ JWT_SECRET=your_jwt_secret_here TOTP_ENCRYPTION_KEY=your_totp_key_here # 可选:管理员账号 -ADMIN_EMAIL=admin@example.com -ADMIN_PASSWORD=your_admin_password +# 留空则首次启动时自动生成随机邮箱(登录用户名)和密码,并输出到日志。 +# 请勿使用 admin@example.com 这类可被猜中的值,它们是暴力破解的目标。 +ADMIN_EMAIL= +ADMIN_PASSWORD= # 可选:自定义端口 SERVER_PORT=8080 @@ -442,9 +444,9 @@ docker compose -f docker-compose.local.yml logs -f sub2api 在浏览器中打开 `http://你的服务器IP:8080` -如果管理员密码是自动生成的,在日志中查找: +如果管理员邮箱(登录用户名)或密码是自动生成的,在日志中查找: ```bash -docker compose -f docker-compose.local.yml logs sub2api | grep "admin password" +docker compose -f docker-compose.local.yml logs sub2api | grep "Generated admin" ``` #### 升级 diff --git a/README_JA.md b/README_JA.md index ca94ba55d39b..24ddd4dd6309 100644 --- a/README_JA.md +++ b/README_JA.md @@ -355,8 +355,10 @@ JWT_SECRET=your_jwt_secret_here TOTP_ENCRYPTION_KEY=your_totp_key_here # オプション: 管理者アカウント -ADMIN_EMAIL=admin@example.com -ADMIN_PASSWORD=your_admin_password +# 空欄の場合、初回起動時にランダムなメールアドレス(ログインユーザー名)とパスワードが自動生成され、ログに出力されます。 +# admin@example.com のような推測されやすい値は総当たり攻撃の標的になるため避けてください。 +ADMIN_EMAIL= +ADMIN_PASSWORD= # オプション: カスタムポート SERVER_PORT=8080 @@ -405,9 +407,9 @@ docker compose -f docker-compose.local.yml logs -f sub2api ブラウザで `http://YOUR_SERVER_IP:8080` を開いてください。 -管理者パスワードが自動生成された場合は、ログで確認できます: +管理者メールアドレス(ログインユーザー名)またはパスワードが自動生成された場合は、ログで確認できます: ```bash -docker compose -f docker-compose.local.yml logs sub2api | grep "admin password" +docker compose -f docker-compose.local.yml logs sub2api | grep "Generated admin" ``` #### アップグレード diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index 27792457cb50..769ed6ae7908 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.2.13 +0.2.14 diff --git a/backend/internal/integration/e2e_user_flow_test.go b/backend/internal/integration/e2e_user_flow_test.go index 5489d0a3262a..9a28c6623089 100644 --- a/backend/internal/integration/e2e_user_flow_test.go +++ b/backend/internal/integration/e2e_user_flow_test.go @@ -264,10 +264,11 @@ func loginTestUser(t *testing.T) string { t.Helper() // 先尝试用管理员账户登录 - adminEmail := getEnv("ADMIN_EMAIL", "admin@sub2api.local") + // 管理员邮箱不再有固定默认值(未设置时安装阶段随机生成),需同时提供邮箱与密码。 + adminEmail := getEnv("ADMIN_EMAIL", "") adminPassword := getEnv("ADMIN_PASSWORD", "") - if adminPassword == "" { + if adminEmail == "" || adminPassword == "" { // 尝试用测试用户 adminEmail = testUserEmail adminPassword = testUserPassword diff --git a/backend/internal/payment/provider/easypay.go b/backend/internal/payment/provider/easypay.go index 53e565132798..bbb007bb3ef8 100644 --- a/backend/internal/payment/provider/easypay.go +++ b/backend/internal/payment/provider/easypay.go @@ -376,6 +376,9 @@ func (e *EasyPay) VerifyNotification(_ context.Context, rawBody string, _ map[st // url.ParseQuery already decodes values — no additional decode needed. params := make(map[string]string) for k := range values { + if !easyPayNotifyAllowedParams[k] { + return nil, fmt.Errorf("unexpected notify param: %s", k) + } params[k] = values.Get(k) } sign := params["sign"] @@ -601,3 +604,26 @@ func easyPaySign(params map[string]string, pkey string) string { func easyPayVerifySign(params map[string]string, pkey string, sign string) bool { return hmac.Equal([]byte(easyPaySign(params, pkey)), []byte(sign)) } + +// easyPayNotifyAllowedParams is the exact parameter set a genuine EasyPay +// (彩虹易支付-compatible) async notification carries. Order-creation-only +// fields (notify_url, return_url, cid, device, clientip, ...) must never +// appear in a callback: because the sign base string concatenates values +// unescaped, a signed order URL whose return_url embeds e.g. +// "trade_status=TRADE_SUCCESS" could otherwise be replayed as a forged +// payment-success notification (issue #7881). Rejecting unknown keys closes +// the whole smuggling class; genuinely paid orders rejected by an exotic +// upstream variant are still recovered by the upstream QueryOrder reconcile +// path. +var easyPayNotifyAllowedParams = map[string]bool{ + "pid": true, + "trade_no": true, + "out_trade_no": true, + "type": true, + "name": true, + "money": true, + "trade_status": true, + "param": true, + "sign": true, + "sign_type": true, +} diff --git a/backend/internal/payment/provider/easypay_notify_security_test.go b/backend/internal/payment/provider/easypay_notify_security_test.go new file mode 100644 index 000000000000..66a373e71b17 --- /dev/null +++ b/backend/internal/payment/provider/easypay_notify_security_test.go @@ -0,0 +1,172 @@ +package provider + +// Regression tests for the EasyPay forged-callback vulnerability +// (Wei-Shaw/sub2api issue #7875). +// +// Attack recap: the order-creation signature (visible to the payer in the +// submit.php popup URL) signs return_url among other fields, and the sign +// base string concatenates values unescaped. A client-supplied return_url +// ending in "&trade_status=TRADE_SUCCESS" therefore produced a signature +// that was byte-identical to a payment-success notification's signature +// once the callback decoded return_url partially and promoted the smuggled +// pair to a top-level parameter. Merchant-key rotation could not help +// because the attacker never needed the key. +// +// Fixes covered here: +// 1. service.CanonicalizeReturnURL drops client-supplied query parameters +// (see payment_resume_service_test.go). +// 2. VerifyNotification rejects any parameter outside the genuine async +// notify set, so smuggled order-creation fields (return_url et al.) +// fail closed even if a signed value smuggles a fake pair. + +import ( + "context" + "net/url" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/payment" +) + +// easyPayPoCProvider returns a provider with a fixed test credential set. +func easyPayPoCProvider() *EasyPay { + return &EasyPay{config: map[string]string{ + "pid": "1000", + "pkey": "MERCHANT_SECRET_KEY", + "apiBase": "https://pay.example.com", + "notifyUrl": "https://site.example.com/api/v1/payment/webhook/easypay", + "returnUrl": "https://site.example.com/payment/result", + }} +} + +// TestEasyPayNotifyRejectsForgedSignReuseCallback is the exact PoC payload +// that was accepted before the fix: the order's own submit.php signature +// replayed with trade_status smuggled out of the return_url value. +func TestEasyPayNotifyRejectsForgedSignReuseCallback(t *testing.T) { + t.Parallel() + e := easyPayPoCProvider() + + // Order creation (popup mode): return_url carries the smuggled pair as + // its last sorted inner query key, exactly as buildPaymentReturnURL + // would have produced before CanonicalizeReturnURL stripped user query. + returnURL := "https://site.example.com/payment/result?order_id=99&out_trade_no=ORDER123&status=success&trade_status=TRADE_SUCCESS" + createParams := map[string]string{ + "pid": "1000", + "type": "alipay", + "out_trade_no": "ORDER123", + "notify_url": e.config["notifyUrl"], + "return_url": returnURL, + "name": "balance recharge", + "money": "650.00", + } + sign := easyPaySign(createParams, e.config["pkey"]) + + // Forged callback: return_url encoded only up to status=success, then a + // raw &trade_status=TRADE_SUCCESS promotes it to a top-level parameter. + prefix := "https://site.example.com/payment/result?order_id=99&out_trade_no=ORDER123&status=success" + cb := url.Values{} + cb.Set("pid", "1000") + cb.Set("type", "alipay") + cb.Set("out_trade_no", "ORDER123") + cb.Set("notify_url", e.config["notifyUrl"]) + cb.Set("name", "balance recharge") + cb.Set("money", "650.00") + cb.Set("return_url", prefix) + rawCallback := cb.Encode() + "&trade_status=TRADE_SUCCESS" + "&sign=" + sign + "&sign_type=MD5" + + if _, err := e.VerifyNotification(context.Background(), rawCallback, nil); err == nil { + t.Fatal("forged sign-reuse callback must be rejected") + } +} + +// TestEasyPayNotifyRejectsOrderURLReplay covers the milder variant where the +// attacker replays the complete signed pay URL unchanged: return_url itself +// is not a legitimate notify parameter and must also be rejected. +func TestEasyPayNotifyRejectsOrderURLReplay(t *testing.T) { + t.Parallel() + e := easyPayPoCProvider() + + createParams := map[string]string{ + "pid": "1000", + "type": "alipay", + "out_trade_no": "ORDER123", + "notify_url": e.config["notifyUrl"], + "return_url": "https://site.example.com/payment/result", + "name": "balance recharge", + "money": "650.00", + } + createParams["sign"] = easyPaySign(createParams, e.config["pkey"]) + createParams["sign_type"] = signTypeMD5 + q := url.Values{} + for k, v := range createParams { + q.Set(k, v) + } + + if _, err := e.VerifyNotification(context.Background(), q.Encode(), nil); err == nil { + t.Fatal("replayed order URL must be rejected") + } +} + +// TestEasyPayNotifyAcceptsGenuineCallback locks in the legitimate notify +// contract: the canonical parameter set with a valid signature succeeds. +func TestEasyPayNotifyAcceptsGenuineCallback(t *testing.T) { + t.Parallel() + e := easyPayPoCProvider() + + params := map[string]string{ + "pid": "1000", + "trade_no": "2026100622001400000001", + "out_trade_no": "ORDER123", + "type": "alipay", + "name": "balance recharge", + "money": "650.00", + "trade_status": tradeStatusSuccess, + } + sign := easyPaySign(params, e.config["pkey"]) + params["sign"] = sign + params["sign_type"] = signTypeMD5 + q := url.Values{} + for k, v := range params { + q.Set(k, v) + } + + n, err := e.VerifyNotification(context.Background(), q.Encode(), nil) + if err != nil { + t.Fatalf("genuine callback rejected: %v", err) + } + if n.Status != payment.ProviderStatusSuccess { + t.Fatalf("status = %v, want success", n.Status) + } + if n.OrderID != "ORDER123" || n.TradeNo != "2026100622001400000001" || n.Amount != 650.00 { + t.Fatalf("unexpected notification: %+v", n) + } +} + +// TestEasyPayNotifyRejectsUnknownParam ensures any parameter outside the +// canonical notify set fails closed, including empty-valued ones that the +// signer itself would skip. +func TestEasyPayNotifyRejectsUnknownParam(t *testing.T) { + t.Parallel() + e := easyPayPoCProvider() + + params := map[string]string{ + "pid": "1000", + "trade_no": "T1", + "out_trade_no": "ORDER123", + "type": "alipay", + "name": "balance recharge", + "money": "650.00", + "trade_status": tradeStatusSuccess, + } + sign := easyPaySign(params, e.config["pkey"]) + q := url.Values{} + for k, v := range params { + q.Set(k, v) + } + q.Set("sign", sign) + q.Set("sign_type", signTypeMD5) + q.Set("device", "") // empty value: invisible to the signer, still rejected + + if _, err := e.VerifyNotification(context.Background(), q.Encode(), nil); err == nil { + t.Fatal("callback with unknown param must be rejected") + } +} diff --git a/backend/internal/service/payment_resume_service.go b/backend/internal/service/payment_resume_service.go index 9ae62fde33b8..7bc76c800962 100644 --- a/backend/internal/service/payment_resume_service.go +++ b/backend/internal/service/payment_resume_service.go @@ -245,6 +245,13 @@ func CanonicalizeReturnURL(raw string, srcHost string, srcURL string) (string, e return "", infraerrors.BadRequest("INVALID_RETURN_URL", "return_url must use http or https") } parsed.Fragment = "" + // Client-supplied query parameters are dropped: buildPaymentReturnURL adds + // its own signed set, and user-controlled pairs such as + // "trade_status=TRADE_SUCCESS" inside the signed return_url value can be + // smuggled into EasyPay-style sign base strings (values are concatenated + // unescaped), letting an order-creation signature be replayed as a forged + // payment-success callback. See issue #7881. + parsed.RawQuery = "" if parsed.Path == "" { parsed.Path = "/" } diff --git a/backend/internal/service/payment_resume_service_test.go b/backend/internal/service/payment_resume_service_test.go index 17b637fa23fe..54b85b4a374d 100644 --- a/backend/internal/service/payment_resume_service_test.go +++ b/backend/internal/service/payment_resume_service_test.go @@ -10,6 +10,7 @@ import ( "encoding/json" "net/url" "strconv" + "strings" "testing" "time" @@ -81,12 +82,30 @@ func TestNormalizePaymentSource(t *testing.T) { func TestCanonicalizeReturnURL(t *testing.T) { t.Parallel() + // Client-supplied query and fragment are both dropped: user-controlled + // pairs inside the signed return_url value could be smuggled into + // EasyPay-style sign base strings (issue #7881). got, err := CanonicalizeReturnURL("https://example.com/payment/result?b=2#a", "example.com", "") if err != nil { t.Fatalf("CanonicalizeReturnURL returned error: %v", err) } - if got != "https://example.com/payment/result?b=2" { - t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://example.com/payment/result?b=2") + if got != "https://example.com/payment/result" { + t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://example.com/payment/result") + } +} + +func TestCanonicalizeReturnURLStripsSmuggledTradeStatus(t *testing.T) { + t.Parallel() + + got, err := CanonicalizeReturnURL( + "https://example.com/payment/result?trade_status=TRADE_SUCCESS", + "example.com", "", + ) + if err != nil { + t.Fatalf("CanonicalizeReturnURL returned error: %v", err) + } + if strings.Contains(got, "trade_status") { + t.Fatalf("CanonicalizeReturnURL kept smuggled trade_status: %q", got) } } @@ -117,8 +136,10 @@ func TestCanonicalizeReturnURLAllowsConfiguredFrontendHost(t *testing.T) { if err != nil { t.Fatalf("CanonicalizeReturnURL returned error: %v", err) } - if got != "https://app.example.com/payment/result?from=checkout" { - t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://app.example.com/payment/result?from=checkout") + // The host allowlist still applies, but client query parameters are + // always dropped (issue #7881: signed-value parameter smuggling). + if got != "https://app.example.com/payment/result" { + t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://app.example.com/payment/result") } } diff --git a/backend/internal/setup/cli.go b/backend/internal/setup/cli.go index 2b323acf4abe..8d4e9e419e9d 100644 --- a/backend/internal/setup/cli.go +++ b/backend/internal/setup/cli.go @@ -4,7 +4,6 @@ package setup import ( "bufio" "fmt" - "net/mail" "os" "regexp" "strconv" @@ -29,11 +28,6 @@ func cliValidateUsername(name string) bool { return validName.MatchString(name) && len(name) <= 63 } -func cliValidateEmail(email string) bool { - _, err := mail.ParseAddress(email) - return err == nil && len(email) <= 254 -} - func cliValidatePort(port int) bool { return port > 0 && port <= 65535 } @@ -163,9 +157,14 @@ func RunCLI() error { fmt.Println() fmt.Println("── Admin Account ──") + // 默认值使用随机邮箱,避免直接回车得到可被猜中的固定管理员用户名(issue #7850)。 + defaultAdminEmail, err := generateAdminEmail() + if err != nil { + return err + } for { - cfg.Admin.Email = promptString(reader, "Admin Email", "admin@example.com") - if cliValidateEmail(cfg.Admin.Email) { + cfg.Admin.Email = promptString(reader, "Admin Email (login username)", defaultAdminEmail) + if validateEmail(cfg.Admin.Email) { break } fmt.Println(" Invalid email format.") @@ -173,13 +172,9 @@ func RunCLI() error { for { cfg.Admin.Password = promptPassword("Admin Password") - // SECURITY: Match Web API requirement of 8 characters minimum - if len(cfg.Admin.Password) < 8 { - fmt.Println(" Password must be at least 8 characters") - continue - } - if len(cfg.Admin.Password) > 128 { - fmt.Println(" Password must be at most 128 characters") + // SECURITY: Match Web API password requirements + if err := validatePassword(cfg.Admin.Password); err != nil { + fmt.Printf(" Invalid password: %v\n", err) continue } confirm := promptPassword("Confirm Password") diff --git a/backend/internal/setup/handler.go b/backend/internal/setup/handler.go index 9207b4335c96..e54945061a37 100644 --- a/backend/internal/setup/handler.go +++ b/backend/internal/setup/handler.go @@ -13,6 +13,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/pkg/sysutil" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" ) // installMutex prevents concurrent installation attempts (TOCTOU protection) @@ -83,19 +84,29 @@ func validateUsername(name string) bool { return validName.MatchString(name) && len(name) <= 63 } -// validateEmail checks if email format is valid +// validateEmail checks if email format is valid. The address must also pass the +// login endpoint's `binding:"email"` rule, otherwise the admin could be created +// with an email (e.g. "a@b" or "Name ") that can never log in. func validateEmail(email string) bool { - _, err := mail.ParseAddress(email) - return err == nil && len(email) <= 254 + if _, err := mail.ParseAddress(email); err != nil || len(email) > 254 { + return false + } + loginReq := struct { + Email string `binding:"required,email"` + }{Email: email} + return binding.Validator.ValidateStruct(&loginReq) == nil } +// maxPasswordBytes is bcrypt's input limit; longer passwords fail to hash. +const maxPasswordBytes = 72 + // validatePassword checks password strength func validatePassword(password string) error { if len(password) < 8 { return fmt.Errorf("password must be at least 8 characters") } - if len(password) > 128 { - return fmt.Errorf("password must be at most 128 characters") + if len(password) > maxPasswordBytes { + return fmt.Errorf("password must be at most %d bytes", maxPasswordBytes) } return nil } diff --git a/backend/internal/setup/setup.go b/backend/internal/setup/setup.go index 963926fa4e02..922ab62469e0 100644 --- a/backend/internal/setup/setup.go +++ b/backend/internal/setup/setup.go @@ -417,6 +417,10 @@ func createAdminUser(cfg *SetupConfig) (bool, string, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() + return bootstrapAdminUser(ctx, db, cfg) +} + +func bootstrapAdminUser(ctx context.Context, db *sql.DB, cfg *SetupConfig) (bool, string, error) { var totalUsers int64 if err := db.QueryRowContext(ctx, "SELECT COUNT(1) FROM users").Scan(&totalUsers); err != nil { return false, "", err @@ -430,12 +434,14 @@ func createAdminUser(cfg *SetupConfig) (bool, string, error) { return false, decision.reason, nil } - if strings.TrimSpace(cfg.Admin.Password) == "" { - password, genErr := generateSecret(16) - if genErr != nil { - return false, "", fmt.Errorf("failed to generate admin password: %w", genErr) - } - cfg.Admin.Password = password + emailGenerated, passwordGenerated, err := prepareAdminCredentials(&cfg.Admin) + if err != nil { + return false, "", err + } + if emailGenerated { + fmt.Printf("Generated admin email (login username): %s\n", cfg.Admin.Email) + } + if passwordGenerated { fmt.Printf("Generated admin password (one-time): %s\n", cfg.Admin.Password) fmt.Println("IMPORTANT: Save this password! It will not be shown again.") } @@ -473,6 +479,47 @@ func createAdminUser(cfg *SetupConfig) (bool, string, error) { return true, decision.reason, nil } +// prepareAdminCredentials fills in missing admin credentials with random values +// and rejects invalid user-supplied emails and weak passwords. It only runs +// when an admin is actually about to be created, so existing deployments are +// never affected. A random email keeps fresh installs off the well-known +// default username that brute-force scanners target (issue #7850). +func prepareAdminCredentials(admin *AdminConfig) (emailGenerated, passwordGenerated bool, err error) { + admin.Email = strings.TrimSpace(admin.Email) + if admin.Email == "" { + email, genErr := generateAdminEmail() + if genErr != nil { + return false, false, genErr + } + admin.Email = email + emailGenerated = true + } else if !validateEmail(admin.Email) { + return false, false, fmt.Errorf("invalid admin email: %q is not a valid login email", admin.Email) + } + + if strings.TrimSpace(admin.Password) == "" { + password, genErr := generateSecret(16) + if genErr != nil { + return false, false, fmt.Errorf("failed to generate admin password: %w", genErr) + } + admin.Password = password + passwordGenerated = true + } else if validateErr := validatePassword(admin.Password); validateErr != nil { + return false, false, fmt.Errorf("invalid admin password: %w", validateErr) + } + + return emailGenerated, passwordGenerated, nil +} + +// generateAdminEmail returns a random, non-guessable admin login email. +func generateAdminEmail() (string, error) { + suffix, err := generateSecret(6) + if err != nil { + return "", fmt.Errorf("failed to generate admin email: %w", err) + } + return fmt.Sprintf("admin-%s@sub2api.local", suffix), nil +} + func writeConfigFile(cfg *SetupConfig) error { // Ensure timezone has a default value tz := cfg.Timezone @@ -596,7 +643,7 @@ func AutoSetupFromEnv() error { EnableTLS: getEnvOrDefault("REDIS_ENABLE_TLS", "false") == "true", }, Admin: AdminConfig{ - Email: getEnvOrDefault("ADMIN_EMAIL", "admin@sub2api.local"), + Email: getEnvOrDefault("ADMIN_EMAIL", ""), Password: getEnvOrDefault("ADMIN_PASSWORD", ""), }, Server: ServerConfig{ diff --git a/backend/internal/setup/setup_test.go b/backend/internal/setup/setup_test.go index 7d4320ada0ae..33ffae25fd7e 100644 --- a/backend/internal/setup/setup_test.go +++ b/backend/internal/setup/setup_test.go @@ -1,15 +1,19 @@ package setup import ( + "context" "database/sql" "fmt" "os" "path/filepath" + "regexp" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/gin-gonic/gin/binding" "github.com/lib/pq" "gopkg.in/yaml.v3" ) @@ -398,3 +402,235 @@ func TestDatabaseConnectionDoesNotFallbackForTargetAuthenticationError(t *testin t.Fatalf("opened databases = %v, want only configured target %q", opened, cfg.DBName) } } + +func TestPrepareAdminCredentialsGeneratesMissingValues(t *testing.T) { + t.Parallel() + + admin := AdminConfig{Email: " ", Password: ""} + emailGenerated, passwordGenerated, err := prepareAdminCredentials(&admin) + if err != nil { + t.Fatalf("prepareAdminCredentials() error = %v", err) + } + if !emailGenerated || !passwordGenerated { + t.Fatalf("generated flags = (%v, %v), want (true, true)", emailGenerated, passwordGenerated) + } + if !regexp.MustCompile(`^admin-[0-9a-f]{12}@sub2api\.local$`).MatchString(admin.Email) { + t.Fatalf("generated email = %q, want admin-<12 hex>@sub2api.local", admin.Email) + } + // 生成的邮箱必须能通过登录接口的 binding:"required,email" 校验。 + loginReq := struct { + Email string `binding:"required,email"` + }{Email: admin.Email} + if err := binding.Validator.ValidateStruct(&loginReq); err != nil { + t.Fatalf("generated email %q rejected by login validator: %v", admin.Email, err) + } + if len(admin.Password) != 32 { + t.Fatalf("generated password length = %d, want 32", len(admin.Password)) + } + + other := AdminConfig{} + if _, _, err := prepareAdminCredentials(&other); err != nil { + t.Fatalf("prepareAdminCredentials() second call error = %v", err) + } + if other.Email == admin.Email { + t.Fatalf("generated emails should be random, got %q twice", admin.Email) + } +} + +func TestPrepareAdminCredentialsKeepsProvidedValues(t *testing.T) { + t.Parallel() + + admin := AdminConfig{Email: "owner@example.com", Password: "a-strong-password"} + emailGenerated, passwordGenerated, err := prepareAdminCredentials(&admin) + if err != nil { + t.Fatalf("prepareAdminCredentials() error = %v", err) + } + if emailGenerated || passwordGenerated { + t.Fatalf("generated flags = (%v, %v), want (false, false)", emailGenerated, passwordGenerated) + } + if admin.Email != "owner@example.com" || admin.Password != "a-strong-password" { + t.Fatalf("provided credentials were modified: %+v", admin) + } +} + +func TestPrepareAdminCredentialsRejectsWeakPassword(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + password string + }{ + {name: "too short", password: "123456"}, + {name: "seven chars", password: "1234567"}, + {name: "exceeds bcrypt limit", password: strings.Repeat("a", 73)}, + {name: "former 128 limit", password: strings.Repeat("a", 128)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + admin := AdminConfig{Email: "owner@example.com", Password: tt.password} + _, _, err := prepareAdminCredentials(&admin) + if err == nil || !strings.Contains(err.Error(), "invalid admin password") { + t.Fatalf("prepareAdminCredentials(%q) error = %v, want invalid admin password error", tt.password, err) + } + }) + } +} + +func TestPrepareAdminCredentialsRejectsUnloginableEmail(t *testing.T) { + t.Parallel() + + for _, email := range []string{"admin", "a@b", "Owner ", ""} { + t.Run(email, func(t *testing.T) { + t.Parallel() + + admin := AdminConfig{Email: email, Password: "a-strong-password"} + _, _, err := prepareAdminCredentials(&admin) + if err == nil || !strings.Contains(err.Error(), "invalid admin email") { + t.Fatalf("prepareAdminCredentials(%q) error = %v, want invalid admin email error", email, err) + } + }) + } +} + +func TestPrepareAdminCredentialsTrimsProvidedEmail(t *testing.T) { + t.Parallel() + + admin := AdminConfig{Email: " owner@example.com\n", Password: "a-strong-password"} + if _, _, err := prepareAdminCredentials(&admin); err != nil { + t.Fatalf("prepareAdminCredentials() error = %v", err) + } + if admin.Email != "owner@example.com" { + t.Fatalf("email = %q, want trimmed owner@example.com", admin.Email) + } +} + +func TestPrepareAdminCredentialsAcceptsBcryptMaxLengthPassword(t *testing.T) { + t.Parallel() + + admin := AdminConfig{Email: "owner@example.com", Password: strings.Repeat("a", 72)} + if _, _, err := prepareAdminCredentials(&admin); err != nil { + t.Fatalf("prepareAdminCredentials() error = %v", err) + } + // 校验上限必须与 bcrypt 实际可哈希的上限一致,否则通过校验后仍会在建号时失败。 + user := service.User{} + if err := user.SetPassword(admin.Password); err != nil { + t.Fatalf("SetPassword() with max-length password error = %v", err) + } +} + +func expectAdminBootstrapCounts(mock sqlmock.Sqlmock, totalUsers, adminUsers int64) { + mock.ExpectQuery(regexp.QuoteMeta("SELECT COUNT(1) FROM users")). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(totalUsers)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT COUNT(1) FROM users WHERE role = $1")). + WithArgs(service.RoleAdmin). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(adminUsers)) +} + +func TestBootstrapAdminUserSkipsValidationWhenNotCreating(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + totalUsers int64 + adminUsers int64 + reason string + }{ + {name: "admin exists", totalUsers: 3, adminUsers: 1, reason: adminBootstrapReasonAdminExists}, + {name: "users exist without admin", totalUsers: 3, adminUsers: 0, reason: adminBootstrapReasonUsersExistWithoutAdmin}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New() error = %v", err) + } + defer func() { _ = db.Close() }() + expectAdminBootstrapCounts(mock, tt.totalUsers, tt.adminUsers) + + // 已有部署里遗留的弱密码/非法邮箱不能阻断启动。 + cfg := &SetupConfig{Admin: AdminConfig{Email: "admin", Password: "123456"}} + created, reason, err := bootstrapAdminUser(context.Background(), db, cfg) + if err != nil || created || reason != tt.reason { + t.Fatalf("bootstrapAdminUser() = (%v, %q, %v), want (false, %q, nil)", created, reason, err, tt.reason) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unexpected database interaction: %v", err) + } + }) + } +} + +func TestBootstrapAdminUserRejectsWeakPasswordWithoutInsert(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New() error = %v", err) + } + defer func() { _ = db.Close() }() + expectAdminBootstrapCounts(mock, 0, 0) + + cfg := &SetupConfig{Admin: AdminConfig{Password: "123456"}} + created, _, err := bootstrapAdminUser(context.Background(), db, cfg) + if err == nil || created || !strings.Contains(err.Error(), "invalid admin password") { + t.Fatalf("bootstrapAdminUser() = (%v, %v), want invalid admin password error", created, err) + } + // 未设置 INSERT 期望:若发生插入,sqlmock 会返回非预期调用错误,上面的错误断言即失败。 + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unexpected database interaction: %v", err) + } +} + +func TestBootstrapAdminUserCreatesAdminWithGeneratedCredentials(t *testing.T) { + t.Parallel() + + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New() error = %v", err) + } + defer func() { _ = db.Close() }() + expectAdminBootstrapCounts(mock, 0, 0) + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO users")). + WithArgs( + sqlmock.AnyArg(), sqlmock.AnyArg(), service.RoleAdmin, sqlmock.AnyArg(), + sqlmock.AnyArg(), service.StatusActive, sqlmock.AnyArg(), sqlmock.AnyArg(), + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + + cfg := &SetupConfig{} + created, reason, err := bootstrapAdminUser(context.Background(), db, cfg) + if err != nil || !created || reason != adminBootstrapReasonEmptyDatabase { + t.Fatalf("bootstrapAdminUser() = (%v, %q, %v), want (true, %q, nil)", created, reason, err, adminBootstrapReasonEmptyDatabase) + } + if !regexp.MustCompile(`^admin-[0-9a-f]{12}@sub2api\.local$`).MatchString(cfg.Admin.Email) { + t.Fatalf("admin email = %q, want generated admin-<12 hex>@sub2api.local", cfg.Admin.Email) + } + if len(cfg.Admin.Password) != 32 { + t.Fatalf("admin password length = %d, want generated 32", len(cfg.Admin.Password)) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("database expectations not met: %v", err) + } +} + +func TestPrepareAdminCredentialsAcceptsMinimumLengthPassword(t *testing.T) { + t.Parallel() + + admin := AdminConfig{Password: "12345678"} + emailGenerated, passwordGenerated, err := prepareAdminCredentials(&admin) + if err != nil { + t.Fatalf("prepareAdminCredentials() error = %v", err) + } + if !emailGenerated || passwordGenerated { + t.Fatalf("generated flags = (%v, %v), want (true, false)", emailGenerated, passwordGenerated) + } + if admin.Password != "12345678" { + t.Fatalf("password = %q, want unchanged", admin.Password) + } +} diff --git a/deploy/.env.example b/deploy/.env.example index 3b60b71da126..464d4fb8f802 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -206,10 +206,12 @@ REDIS_ENABLE_TLS=false # ----------------------------------------------------------------------------- # Admin Account # ----------------------------------------------------------------------------- -# Email for the admin account -ADMIN_EMAIL=admin@sub2api.local +# Email for the admin account (used as the login username) +# Leave empty to auto-generate a random one (will be shown in logs on first run). +# Avoid well-known values such as admin@sub2api.local: they are brute-force targets. +ADMIN_EMAIL= -# Password for admin account +# Password for admin account (8-72 bytes) # Leave empty to auto-generate (will be shown in logs on first run) ADMIN_PASSWORD= diff --git a/deploy/APPLE_CONTAINER.md b/deploy/APPLE_CONTAINER.md index 9e44f244086b..f0b86b33d93a 100644 --- a/deploy/APPLE_CONTAINER.md +++ b/deploy/APPLE_CONTAINER.md @@ -41,7 +41,7 @@ nano .env ./apple-container.sh status ``` -Open `http://localhost:8080`. If `ADMIN_PASSWORD` is empty, retrieve the generated password with: +Open `http://localhost:8080`. If `ADMIN_EMAIL` / `ADMIN_PASSWORD` are empty, retrieve the generated admin email (login username) and password with: ```bash ./apple-container.sh logs app diff --git a/deploy/README.md b/deploy/README.md index 068ed8c1446b..a40feaf23cc1 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -79,8 +79,8 @@ docker compose -f docker-compose.local.yml up -d # View logs docker compose -f docker-compose.local.yml logs -f sub2api -# If admin password was auto-generated, find it in logs: -docker compose -f docker-compose.local.yml logs sub2api | grep "admin password" +# If admin email/password were auto-generated, find them in logs: +docker compose -f docker-compose.local.yml logs sub2api | grep "Generated admin" # Access Web UI # http://localhost:8080 @@ -112,7 +112,7 @@ mkdir -p data postgres_data redis_data # Start all services using local directory version docker compose -f docker-compose.local.yml up -d -# View logs (check for auto-generated admin password) +# View logs (check for auto-generated admin email and password) docker compose -f docker-compose.local.yml logs -f sub2api # Access Web UI @@ -136,14 +136,14 @@ When using Docker Compose with `AUTO_SETUP=true`: - Connects to PostgreSQL and Redis - Applies database migrations (SQL files in `backend/migrations/*.sql`) and records them in `schema_migrations` - Generates JWT secret (if not provided) - - Creates admin account (password auto-generated if not provided) + - Creates admin account (email and password auto-generated if not provided; a provided password must be 8-72 bytes) - Writes config.yaml 2. No manual Setup Wizard needed - just configure `.env` and start -3. If `ADMIN_PASSWORD` is not set, check logs for the generated password: +3. If `ADMIN_EMAIL` / `ADMIN_PASSWORD` are not set, check logs for the generated admin email (login username) and password: ```bash - docker compose logs sub2api | grep "admin password" + docker compose logs sub2api | grep "Generated admin" ``` ### Startup and Database Recovery @@ -283,8 +283,8 @@ docker compose down -v | `JWT_SECRET` | **Recommended** | *(auto-generated)* | JWT secret (fixed for persistent sessions) | | `TOTP_ENCRYPTION_KEY` | **Recommended** | *(auto-generated)* | TOTP encryption key (fixed for persistent 2FA) | | `SERVER_PORT` | No | `8080` | Server port | -| `ADMIN_EMAIL` | No | `admin@sub2api.local` | Admin email | -| `ADMIN_PASSWORD` | No | *(auto-generated)* | Admin password | +| `ADMIN_EMAIL` | No | *(auto-generated)* | Admin email (login username) | +| `ADMIN_PASSWORD` | No | *(auto-generated)* | Admin password (8-72 bytes) | | `TZ` | No | `Asia/Shanghai` | Timezone | | `UPDATE_GITHUB_TOKEN` | No | *(empty)* | Token for `api.github.com` release checks only; asset downloads remain anonymous. | | `GEMINI_OAUTH_CLIENT_ID` | No | *(builtin)* | Google OAuth client ID (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. | diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml index 30bd8778cab1..28d30d119a3a 100644 --- a/deploy/docker-compose.dev.yml +++ b/deploy/docker-compose.dev.yml @@ -43,7 +43,7 @@ services: - REDIS_USERNAME=${REDIS_USERNAME:-} - REDIS_PASSWORD=${REDIS_PASSWORD:-} - REDIS_DB=${REDIS_DB:-0} - - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local} + - ADMIN_EMAIL=${ADMIN_EMAIL:-} - ADMIN_PASSWORD=${ADMIN_PASSWORD:-} - JWT_SECRET=${JWT_SECRET:-} - SETUP_MIGRATION_TIMEOUT_SECONDS=${SETUP_MIGRATION_TIMEOUT_SECONDS:-0} diff --git a/deploy/docker-compose.local.yml b/deploy/docker-compose.local.yml index 88188b5c20c9..ab683769de8c 100644 --- a/deploy/docker-compose.local.yml +++ b/deploy/docker-compose.local.yml @@ -88,7 +88,7 @@ services: # ======================================================================= # Admin Account (auto-created on first run) # ======================================================================= - - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local} + - ADMIN_EMAIL=${ADMIN_EMAIL:-} - ADMIN_PASSWORD=${ADMIN_PASSWORD:-} # ======================================================================= diff --git a/deploy/docker-compose.standalone.yml b/deploy/docker-compose.standalone.yml index 26b94756ff46..bb100ee1b2ea 100644 --- a/deploy/docker-compose.standalone.yml +++ b/deploy/docker-compose.standalone.yml @@ -74,7 +74,7 @@ services: # ======================================================================= # Admin Account (auto-created on first run) # ======================================================================= - - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local} + - ADMIN_EMAIL=${ADMIN_EMAIL:-} - ADMIN_PASSWORD=${ADMIN_PASSWORD:-} # ======================================================================= diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 22640c310c39..dd579b0be58b 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -85,7 +85,7 @@ services: # ======================================================================= # Admin Account (auto-created on first run) # ======================================================================= - - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local} + - ADMIN_EMAIL=${ADMIN_EMAIL:-} - ADMIN_PASSWORD=${ADMIN_PASSWORD:-} # ======================================================================= diff --git a/deploy/docker-deploy.sh b/deploy/docker-deploy.sh index a07f4f417a45..97e8e8e04c2e 100644 --- a/deploy/docker-deploy.sh +++ b/deploy/docker-deploy.sh @@ -5,6 +5,7 @@ # This script prepares deployment files for Sub2API: # - Downloads docker-compose.local.yml and .env.example # - Generates secure secrets (JWT_SECRET, TOTP_ENCRYPTION_KEY, POSTGRES_PASSWORD) +# - Generates a random admin login email (ADMIN_EMAIL) # - Creates necessary data directories # # After running this script, you can start services with: @@ -45,6 +46,14 @@ generate_secret() { openssl rand -hex 32 } +# Generate a random admin login email so new installs never use a guessable default +generate_admin_email() { + local suffix + suffix=$(openssl rand -hex 6) || return 1 + [ -n "$suffix" ] || return 1 + echo "admin-${suffix}@sub2api.local" +} + # Check if command exists command_exists() { command -v "$1" >/dev/null 2>&1 @@ -104,6 +113,7 @@ main() { JWT_SECRET=$(generate_secret) TOTP_ENCRYPTION_KEY=$(generate_secret) POSTGRES_PASSWORD=$(generate_secret) + ADMIN_EMAIL=$(generate_admin_email) # Create .env from .env.example cp .env.example .env @@ -114,11 +124,13 @@ main() { sed -i "s/^JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env sed -i "s/^TOTP_ENCRYPTION_KEY=.*/TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}/" .env sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=${POSTGRES_PASSWORD}/" .env + sed -i "s/^ADMIN_EMAIL=.*/ADMIN_EMAIL=${ADMIN_EMAIL}/" .env else # BSD sed (macOS) sed -i '' "s/^JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env sed -i '' "s/^TOTP_ENCRYPTION_KEY=.*/TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY}/" .env sed -i '' "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=${POSTGRES_PASSWORD}/" .env + sed -i '' "s/^ADMIN_EMAIL=.*/ADMIN_EMAIL=${ADMIN_EMAIL}/" .env fi # Create data directories @@ -139,6 +151,7 @@ main() { echo " POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}" echo " JWT_SECRET: ${JWT_SECRET}" echo " TOTP_ENCRYPTION_KEY: ${TOTP_ENCRYPTION_KEY}" + echo " ADMIN_EMAIL: ${ADMIN_EMAIL} (admin login username)" echo "" print_warning "These credentials have been saved to .env file." print_warning "Please keep them secure and do not share publicly!" diff --git a/frontend/package.json b/frontend/package.json index 3bb1377d8707..7b891ca2972d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "marked": "^17.0.1", "pinia": "^2.1.7", "qrcode": "^1.5.4", - "vue": "^3.4.0", + "vue": "^3.5.43", "vue-chartjs": "^5.3.0", "vue-draggable-plus": "^0.6.1", "vue-i18n": "^9.14.5", @@ -67,7 +67,8 @@ "postcss@<8.5.18": ">=8.5.18", "nanoid@<3.3.18": "3.3.18", "source-map-js@<1.2.2": ">=1.2.2", - "vue@<3.5.42": ">=3.5.42" + "vue@<3.5.42": ">=3.5.42", + "dompurify@<3.4.14": ">=3.4.14" } } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ab680eca8559..b6326ac8d345 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,7 @@ overrides: nanoid@<3.3.18: 3.3.18 source-map-js@<1.2.2: '>=1.2.2' vue@<3.5.42: '>=3.5.42' + dompurify@<3.4.14: '>=3.4.14' importers: @@ -56,7 +57,7 @@ importers: specifier: ^1.5.4 version: 1.5.4 vue: - specifier: '>=3.5.42' + specifier: ^3.5.43 version: 3.5.43(typescript@5.6.3) vue-chartjs: specifier: ^5.3.0 diff --git a/frontend/src/components/keys/UseKeyModal.vue b/frontend/src/components/keys/UseKeyModal.vue index 8cd081bbbb92..4913d2cd8faf 100644 --- a/frontend/src/components/keys/UseKeyModal.vue +++ b/frontend/src/components/keys/UseKeyModal.vue @@ -1099,7 +1099,7 @@ ${codexModelCatalogMode.value === 'remote' ? `model_catalog_url = "${escapeTomlB ${generateCodexProviderAuthConfig(apiKey)} [features] -goals = true` +${codexModelCatalogMode.value === 'remote' ? 'api_key_model_discovery = true\n' : ''}goals = true` return buildOpenAICodexFileConfigs(configDir, configContent, apiKey) } @@ -1342,8 +1342,7 @@ requires_openai_auth = false # Grok/Sub2API path is HTTP/SSE; disable WS (Codex may otherwise try WebSocket first) supports_websockets = false -# Optional: -# [features] +${codexModelCatalogMode.value === 'remote' ? '[features]\napi_key_model_discovery = true\n\n# Optional:' : '# Optional:\n# [features]'} # goals = true` return [ @@ -1409,7 +1408,7 @@ base_url = "${baseUrl}" ${codexModelCatalogMode.value === 'remote' ? `model_catalog_url = "${escapeTomlBasicString(buildCodexModelCatalogUrl(baseUrl))}"\n` : ''}env_key = "SUB2API_API_KEY" wire_api = "responses" requires_openai_auth = false -supports_websockets = false` +supports_websockets = false${codexModelCatalogMode.value === 'remote' ? '\n\n[features]\napi_key_model_discovery = true' : ''}` return [ { path: isWindows ? 'PowerShell' : 'Terminal', content: envContent }, @@ -1447,7 +1446,7 @@ supports_websockets = true ${generateCodexProviderAuthConfig(apiKey)} [features] -responses_websockets_v2 = true +${codexModelCatalogMode.value === 'remote' ? 'api_key_model_discovery = true\n' : ''}responses_websockets_v2 = true goals = true` return buildOpenAICodexFileConfigs(configDir, configContent, apiKey) diff --git a/frontend/src/components/keys/__tests__/UseKeyModal.spec.ts b/frontend/src/components/keys/__tests__/UseKeyModal.spec.ts index 5eb4c842b335..c89c8c8aae10 100644 --- a/frontend/src/components/keys/__tests__/UseKeyModal.spec.ts +++ b/frontend/src/components/keys/__tests__/UseKeyModal.spec.ts @@ -34,6 +34,17 @@ function readBlobAsText(blob: Blob): Promise { }) } +function expectApiKeyModelDiscovery(config: string | undefined, enabled: boolean) { + expect(config).toBeDefined() + expect(config!.match(/^api_key_model_discovery = true$/gm) ?? []).toHaveLength(enabled ? 1 : 0) + if (enabled) { + expect(config).toContain('[features]\napi_key_model_discovery = true') + expect(config!.match(/^\[features\]$/gm)).toHaveLength(1) + } else { + expect(config).not.toContain('api_key_model_discovery') + } +} + describe('UseKeyModal', () => { afterEach(() => { vi.unstubAllGlobals() @@ -354,6 +365,7 @@ describe('UseKeyModal', () => { expect(configToml).toContain('base_url = "https://example.com/v1"') expect(configToml).toContain('env_key = "SUB2API_API_KEY"') expect(configToml).toContain('wire_api = "responses"') + expectApiKeyModelDiscovery(configToml, true) // API-key provider: Codex must not require a ChatGPT OAuth login. expect(configToml).toContain('requires_openai_auth = false') expect(configToml).toContain('supports_websockets = false') @@ -377,6 +389,17 @@ describe('UseKeyModal', () => { codeBlocks = wrapper.findAll('pre code').map((code) => code.text()) expect(wrapper.text().toLowerCase()).toContain('%userprofile%\\.codex\\config.toml'.toLowerCase()) expect(codeBlocks.join('\n')).toContain('experimental_bearer_token = "sk-grok-codex-test"') + + await wrapper.get('[data-testid="codex-model-catalog-mode"]').setValue('file') + const fileConfig = wrapper.findAll('pre code').map((code) => code.text()) + .find((content) => content.includes('[model_providers.sub2api]')) + expectApiKeyModelDiscovery(fileConfig, false) + expect(fileConfig).toContain('model_catalog_json = "~/.codex/codex-models.json"') + expect(fileConfig).not.toContain('model_catalog_url') + expect(fileConfig).toContain('env_key = "SUB2API_API_KEY"') + expect(fileConfig).toContain('requires_openai_auth = false') + expect(fileConfig).toContain('supports_websockets = false') + expect(fileConfig).not.toMatch(/^goals =/m) }) it('keeps legacy OpenAI Codex config as the default', () => { @@ -415,7 +438,7 @@ describe('UseKeyModal', () => { expect(configToml).not.toContain('image_generation') expect(configToml).not.toContain('supports_websockets') expect(configToml).not.toContain('responses_websockets_v2') - expect(configToml).toContain('[features]\ngoals = true') + expect(configToml).toContain('[features]\napi_key_model_discovery = true\ngoals = true') expect(configToml).not.toContain('model_reasoning_effort = "xhigh"') expect(codeBlocks).toContain('{\n "OPENAI_API_KEY": "sk-test"\n}') expect(wrapper.text()).toContain('auth.json') @@ -495,6 +518,7 @@ describe('UseKeyModal', () => { expect(configToml).toContain('http_headers = { "x-openai-actor-authorization" = "local-image-extension" }') expect(configToml).toContain('model_catalog_url = "https://example.com/v1/models"') expect(configToml).not.toContain('model_catalog_json') + expectApiKeyModelDiscovery(configToml, true) expect(configToml).not.toContain('env_key') expect(configToml).not.toContain('image_generation') expect(codeBlocks).not.toContain('{\n "OPENAI_API_KEY": "sk-test"\n}') @@ -557,7 +581,7 @@ describe('UseKeyModal', () => { expect(configToml).not.toContain('env_key') expect(configToml).not.toContain('image_generation') expect(configToml).toContain('supports_websockets = true') - expect(configToml).toContain('[features]\nresponses_websockets_v2 = true\ngoals = true') + expect(configToml).toContain('[features]\napi_key_model_discovery = true\nresponses_websockets_v2 = true\ngoals = true') expect(codeBlocks).toContain('{\n "OPENAI_API_KEY": "sk-test"\n}') expect(wrapper.text()).toContain('auth.json') }) @@ -596,6 +620,7 @@ describe('UseKeyModal', () => { const configToml = codeBlocks.find((content) => content.includes('supports_websockets = true')) expect(configToml).toContain('model_catalog_url = "https://example.com/v1/models"') expect(configToml).not.toContain('model_catalog_json') + expectApiKeyModelDiscovery(configToml, true) expect(wrapper.get('[data-testid="codex-auth-mode-api-key"]').attributes('aria-checked')).toBe('true') expect(configToml).toBeDefined() @@ -605,7 +630,7 @@ describe('UseKeyModal', () => { expect(configToml).not.toContain('env_key') expect(configToml).not.toContain('image_generation') expect(configToml).toContain('supports_websockets = true') - expect(configToml).toContain('[features]\nresponses_websockets_v2 = true\ngoals = true') + expect(configToml).toContain('[features]\napi_key_model_discovery = true\nresponses_websockets_v2 = true\ngoals = true') expect(codeBlocks).not.toContain('{\n "OPENAI_API_KEY": "sk-test"\n}') expect(wrapper.text()).not.toContain('auth.json') }) @@ -931,6 +956,7 @@ describe('UseKeyModal', () => { expect(unixConfig).toContain('[model_providers.sub2api]\nname = "Sub2API Composite"\nbase_url = "https://example.com/v1"\nmodel_catalog_url = "https://example.com/v1/models"') expect(unixConfig).not.toContain('model_catalog_json') expect(unixConfig).toContain('env_key = "SUB2API_API_KEY"') + expectApiKeyModelDiscovery(unixConfig, true) expect(fetchMock).not.toHaveBeenCalled() await wrapper.get('[data-testid="codex-model-catalog-mode"]').setValue('file') @@ -955,6 +981,7 @@ describe('UseKeyModal', () => { expect(loadedUnixConfig).not.toContain('model = "gpt-5.5"') expect(loadedUnixConfig).toContain('model_catalog_json = "~/.codex/codex-models.json"') expect(loadedUnixConfig).not.toContain('model_catalog_url') + expectApiKeyModelDiscovery(loadedUnixConfig, false) const downloadButton = wrapper.findAll('button').find((button) => button.text().includes('keys.useKeyModal.codexModelCatalog.download') @@ -1017,6 +1044,17 @@ describe('UseKeyModal', () => { expect(config).not.toContain('model_catalog_json') expect(config).toContain('base_url = "https://example.com/v1"') expect(config).toContain('wire_api = "responses"') + expectApiKeyModelDiscovery(config, true) + + await wrapper.get('[data-testid="codex-model-catalog-mode"]').setValue('file') + const fileConfig = wrapper.findAll('pre code').map((code) => code.text()) + .find((content) => content.includes('[model_providers.sub2api]')) + expectApiKeyModelDiscovery(fileConfig, false) + expect(fileConfig).toContain('model_catalog_json = "~/.codex/codex-models.json"') + expect(fileConfig).not.toContain('model_catalog_url') + expect(fileConfig).toContain('env_key = "SUB2API_API_KEY"') + expect(fileConfig).toContain('requires_openai_auth = false') + expect(fileConfig).toContain('supports_websockets = false') } ) @@ -1101,6 +1139,7 @@ describe('UseKeyModal', () => { expect(configToml).not.toContain('model_catalog_json') expect(configToml).toContain('requires_openai_auth = true') expect(configToml).not.toContain('client_version') + expectApiKeyModelDiscovery(configToml, true) expect(wrapper.find('[data-testid="codex-model-catalog"]').exists()).toBe(true) await wrapper.get('[data-testid="codex-model-catalog-mode"]').setValue('file') @@ -1110,6 +1149,12 @@ describe('UseKeyModal', () => { expect(fileConfig).toContain('model_catalog_json = "~/.codex/codex-models.json"') expect(fileConfig).not.toContain('model_catalog_url') expect(fileConfig.indexOf('model_catalog_json')).toBeLessThan(fileConfig.indexOf('[model_providers.OpenAI]')) + expectApiKeyModelDiscovery(fileConfig, false) + // Catalog mode must not change authentication, capacity, goals or WS options. + expect(fileConfig.replace(/^model_catalog_json = .*\n/m, '')).toBe( + configToml!.replace(/^model_catalog_url = .*\n/m, '') + .replace(/^api_key_model_discovery = true\n/m, '') + ) await wrapper.get('[data-testid="codex-model-catalog-mode"]').setValue('remote') } } @@ -1151,6 +1196,7 @@ describe('UseKeyModal', () => { expect((mode.get('option[value="remote"]').element as HTMLOptionElement).disabled).toBe(oversized) const config = wrapper.findAll('pre code').map((code) => code.text()) .find((content) => content.includes('model_provider = "OpenAI"'))! + expectApiKeyModelDiscovery(config, !oversized) if (oversized) { expect(config).toContain('model_catalog_json = "~/.codex/codex-models.json"') expect(config).not.toContain('model_catalog_url')