From 72125ec4fc87136af2373b8c6f2341a73d684df9 Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:34:30 +0000 Subject: [PATCH] feat: sync merge-xbin-2 updates as a single squashed commit --- src/config/settings.py | 88 +++ src/core/auto_registration.py | 297 ++++++++ src/core/db_logs.py | 3 +- src/core/openai/token_refresh.py | 5 +- src/core/register.py | 50 +- src/core/timezone_utils.py | 4 + src/core/upload/cpa_upload.py | 67 +- src/database/crud.py | 7 +- src/database/models.py | 45 +- src/web/app.py | 128 ++-- src/web/routes/accounts.py | 22 +- src/web/routes/email.py | 5 +- src/web/routes/logs.py | 4 +- src/web/routes/payment.py | 61 +- src/web/routes/registration.py | 260 ++++++- src/web/routes/settings.py | 124 ++- src/web/routes/upload/cpa_services.py | 5 +- src/web/routes/upload/sub2api_services.py | 5 +- src/web/routes/upload/tm_services.py | 5 +- src/web/routes/websocket.py | 2 + src/web/task_manager.py | 11 +- static/js/app.js | 346 ++++++++- templates/index.html | 105 ++- tests/test_auto_registration_merge.py | 711 ++++++++++++++++++ .../test_settings_registration_auto_fields.py | 149 ++++ tmp_app_core.js | 11 - tmp_redirectToPage.js | 2 - 27 files changed, 2321 insertions(+), 201 deletions(-) create mode 100644 src/core/auto_registration.py create mode 100644 tests/test_auto_registration_merge.py create mode 100644 tests/test_settings_registration_auto_fields.py delete mode 100644 tmp_app_core.js delete mode 100644 tmp_redirectToPage.js diff --git a/src/config/settings.py b/src/config/settings.py index f137153b..4d559201 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -254,6 +254,72 @@ class SettingDefinition: category=SettingCategory.REGISTRATION, description="注册入口链路(native=原本链路, abcard=ABCard入口链路;Outlook 邮箱会自动走 Outlook 链路)" ), + "registration_auto_enabled": SettingDefinition( + db_key="registration.auto.enabled", + default_value=False, + category=SettingCategory.REGISTRATION, + description="是否启用自动注册补货" + ), + "registration_auto_check_interval": SettingDefinition( + db_key="registration.auto.check_interval", + default_value=60, + category=SettingCategory.REGISTRATION, + description="自动注册库存检查间隔(秒)" + ), + "registration_auto_min_ready_auth_files": SettingDefinition( + db_key="registration.auto.min_ready_auth_files", + default_value=1, + category=SettingCategory.REGISTRATION, + description="自动注册保底可用认证文件数量" + ), + "registration_auto_email_service_type": SettingDefinition( + db_key="registration.auto.email_service_type", + default_value="tempmail", + category=SettingCategory.REGISTRATION, + description="自动注册使用的邮箱服务类型" + ), + "registration_auto_email_service_id": SettingDefinition( + db_key="registration.auto.email_service_id", + default_value=0, + category=SettingCategory.REGISTRATION, + description="自动注册绑定的邮箱服务 ID(0 表示自动选择)" + ), + "registration_auto_proxy": SettingDefinition( + db_key="registration.auto.proxy", + default_value="", + category=SettingCategory.REGISTRATION, + description="自动注册固定代理地址(留空则沿用系统策略)" + ), + "registration_auto_interval_min": SettingDefinition( + db_key="registration.auto.interval_min", + default_value=5, + category=SettingCategory.REGISTRATION, + description="自动注册批量任务最小启动间隔(秒)" + ), + "registration_auto_interval_max": SettingDefinition( + db_key="registration.auto.interval_max", + default_value=30, + category=SettingCategory.REGISTRATION, + description="自动注册批量任务最大启动间隔(秒)" + ), + "registration_auto_concurrency": SettingDefinition( + db_key="registration.auto.concurrency", + default_value=1, + category=SettingCategory.REGISTRATION, + description="自动注册批量任务并发数" + ), + "registration_auto_mode": SettingDefinition( + db_key="registration.auto.mode", + default_value="pipeline", + category=SettingCategory.REGISTRATION, + description="自动注册批量任务模式" + ), + "registration_auto_cpa_service_id": SettingDefinition( + db_key="registration.auto.cpa_service_id", + default_value=0, + category=SettingCategory.REGISTRATION, + description="自动注册监控并回传的 CPA 服务 ID" + ), # 邮箱服务配置 "email_service_priority": SettingDefinition( @@ -450,6 +516,17 @@ class SettingDefinition: "registration_sleep_min": int, "registration_sleep_max": int, "registration_entry_flow": str, + "registration_auto_enabled": bool, + "registration_auto_check_interval": int, + "registration_auto_min_ready_auth_files": int, + "registration_auto_email_service_type": str, + "registration_auto_email_service_id": int, + "registration_auto_proxy": str, + "registration_auto_interval_min": int, + "registration_auto_interval_max": int, + "registration_auto_concurrency": int, + "registration_auto_mode": str, + "registration_auto_cpa_service_id": int, "email_service_priority": dict, "tempmail_enabled": bool, "tempmail_timeout": int, @@ -718,6 +795,17 @@ def proxy_url(self) -> Optional[str]: registration_sleep_min: int = 5 registration_sleep_max: int = 30 registration_entry_flow: str = "native" + registration_auto_enabled: bool = False + registration_auto_check_interval: int = 60 + registration_auto_min_ready_auth_files: int = 1 + registration_auto_email_service_type: str = "tempmail" + registration_auto_email_service_id: int = 0 + registration_auto_proxy: str = "" + registration_auto_interval_min: int = 5 + registration_auto_interval_max: int = 30 + registration_auto_concurrency: int = 1 + registration_auto_mode: str = "pipeline" + registration_auto_cpa_service_id: int = 0 # 邮箱服务配置 email_service_priority: Dict[str, int] = {"tempmail": 0, "yyds_mail": 1, "outlook": 2, "moe_mail": 3} diff --git a/src/core/auto_registration.py b/src/core/auto_registration.py new file mode 100644 index 00000000..8535e9cd --- /dev/null +++ b/src/core/auto_registration.py @@ -0,0 +1,297 @@ +import asyncio +import logging +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import Any, Awaitable, Callable, Optional + +from ..config.settings import Settings, get_settings +from .upload.cpa_upload import count_ready_cpa_auth_files, list_cpa_auth_files +from ..database import crud +from ..database.session import get_db + +logger = logging.getLogger(__name__) +AUTO_REGISTRATION_CHANNEL = "auto-registration" +_auto_registration_state = { + "enabled": False, + "status": "idle", + "message": "自动注册未启动", + "current_batch_id": None, + "current_ready_count": None, + "target_ready_count": None, + "last_checked_at": None, + "last_triggered_at": None, +} +_coordinator_instance = None + + +def _timestamp() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _remaining_delay(target_time: float, now: float) -> float: + return max(0.0, target_time - now) + + +def update_auto_registration_state(**kwargs) -> dict: + _auto_registration_state.update(kwargs) + return get_auto_registration_state() + + +def get_auto_registration_state() -> dict: + return dict(_auto_registration_state) + + +def register_auto_registration_coordinator( + coordinator: Optional["AutoRegistrationCoordinator"], +) -> None: + global _coordinator_instance + _coordinator_instance = coordinator + + +def trigger_auto_registration_check() -> None: + coordinator = _coordinator_instance + if coordinator is not None: + coordinator.request_immediate_check() + + +def add_auto_registration_log(message: str) -> None: + from ..web.task_manager import task_manager + + task_manager.add_batch_log(AUTO_REGISTRATION_CHANNEL, message) + + +def get_auto_registration_logs() -> list[str]: + from ..web.task_manager import task_manager + + return task_manager.get_batch_logs(AUTO_REGISTRATION_CHANNEL) + + +@dataclass +class AutoRegistrationPlan: + deficit: int + ready_count: int + min_ready_auth_files: int + cpa_service_id: int + + +def get_auto_registration_inventory( + settings: Settings, +) -> Optional[tuple[int, int, int]]: + cpa_service_id = int(settings.registration_auto_cpa_service_id or 0) + if cpa_service_id <= 0: + logger.warning("自动注册已启用,但未配置 CPA 服务 ID,跳过库存检查") + return None + + with get_db() as db: + cpa_service = crud.get_cpa_service_by_id(db, cpa_service_id) + + if not cpa_service: + logger.warning("自动注册目标 CPA 服务不存在: %s", cpa_service_id) + return None + + if not cpa_service.enabled: + logger.warning("自动注册目标 CPA 服务已禁用: %s", cpa_service.name) + return None + + success, payload, message = list_cpa_auth_files( + str(cpa_service.api_url), + str(cpa_service.api_token), + ) + if not success: + logger.warning("自动注册读取 auth-files 库存失败: %s", message) + return None + + ready_count = count_ready_cpa_auth_files(payload) + min_ready_auth_files = max(1, int(settings.registration_auto_min_ready_auth_files)) + deficit = max(0, min_ready_auth_files - ready_count) + return ready_count, min_ready_auth_files, deficit + + +def build_auto_registration_plan(settings: Settings) -> Optional[AutoRegistrationPlan]: + if not settings.registration_auto_enabled: + return None + + cpa_service_id = int(settings.registration_auto_cpa_service_id or 0) + inventory = get_auto_registration_inventory(settings) + if inventory is None: + return None + + ready_count, min_ready_auth_files, deficit = inventory + if deficit <= 0: + logger.info( + "自动注册库存充足,当前可用 %s / 目标 %s", + ready_count, + min_ready_auth_files, + ) + + return AutoRegistrationPlan( + deficit=deficit, + ready_count=ready_count, + min_ready_auth_files=min_ready_auth_files, + cpa_service_id=cpa_service_id, + ) + + +class AutoRegistrationCoordinator: + def __init__( + self, + trigger_callback: Callable[[AutoRegistrationPlan, Settings], Awaitable[Any]], + settings_getter: Callable[[], Settings] = get_settings, + plan_builder: Callable[ + [Settings], Optional[AutoRegistrationPlan] + ] = build_auto_registration_plan, + ): + self._trigger_callback = trigger_callback + self._settings_getter = settings_getter + self._plan_builder = plan_builder + self._task: Optional[asyncio.Task] = None + self._cycle_lock = asyncio.Lock() + self._wake_event = asyncio.Event() + + def start(self) -> None: + if self._task and not self._task.done(): + return + update_auto_registration_state( + enabled=bool(self._settings_getter().registration_auto_enabled), + status="idle", + message="自动注册协调器已启动", + ) + self._task = asyncio.create_task( + self._run_forever(), name="auto-registration-loop" + ) + + async def stop(self) -> None: + if not self._task: + return + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + finally: + self._task = None + + def request_immediate_check(self) -> None: + self._wake_event.set() + + async def run_once(self) -> Optional[AutoRegistrationPlan]: + if self._cycle_lock.locked(): + logger.info("自动注册上一轮仍在执行,跳过重入检查") + add_auto_registration_log("[自动注册] 上一轮补货仍在执行,跳过本次重入检查") + return None + + async with self._cycle_lock: + settings = self._settings_getter() + update_auto_registration_state( + enabled=bool(settings.registration_auto_enabled), + status="disabled" + if not settings.registration_auto_enabled + else "checking", + message="自动注册已禁用" + if not settings.registration_auto_enabled + else "正在检查 auth-files 库存", + last_checked_at=_timestamp() + if not settings.registration_auto_enabled + else None, + current_batch_id=None + if not settings.registration_auto_enabled + else _auto_registration_state.get("current_batch_id"), + current_ready_count=None + if not settings.registration_auto_enabled + else _auto_registration_state.get("current_ready_count"), + ) + if not settings.registration_auto_enabled: + return None + + add_auto_registration_log("[自动注册] 开始检查 CPA auth-files 库存") + plan = await asyncio.to_thread(self._plan_builder, settings) + if not plan: + update_auto_registration_state( + status="idle", + message="检查完成,当前无需补货或配置不可用", + last_checked_at=_timestamp(), + current_batch_id=None, + current_ready_count=None, + ) + add_auto_registration_log("[自动注册] 检查完成,当前无需补货") + return None + + if plan.deficit <= 0: + update_auto_registration_state( + status="idle", + message=f"检查完成,当前 codex 库存充足 ({plan.ready_count}/{plan.min_ready_auth_files})", + current_ready_count=plan.ready_count, + target_ready_count=plan.min_ready_auth_files, + last_checked_at=_timestamp(), + current_batch_id=None, + ) + add_auto_registration_log( + f"[自动注册] 检查完成,当前 codex 库存充足 ({plan.ready_count}/{plan.min_ready_auth_files})" + ) + return None + + logger.info( + "自动注册准备补货,当前可用 %s / 目标 %s,计划新增 %s", + plan.ready_count, + plan.min_ready_auth_files, + plan.deficit, + ) + add_auto_registration_log( + f"[自动注册] 库存不足,当前可用 {plan.ready_count} / 目标 {plan.min_ready_auth_files},开始补货 {plan.deficit} 个" + ) + update_auto_registration_state( + status="running", + message="自动补货任务运行中", + current_ready_count=plan.ready_count, + target_ready_count=plan.min_ready_auth_files, + last_checked_at=_timestamp(), + last_triggered_at=_timestamp(), + ) + await self._trigger_callback(plan, settings) + return plan + + async def _run_forever(self) -> None: + loop = asyncio.get_running_loop() + next_check_at: Optional[float] = None + + while True: + settings = self._settings_getter() + interval = max(5, int(settings.registration_auto_check_interval or 60)) + update_auto_registration_state( + enabled=bool(settings.registration_auto_enabled), + target_ready_count=max( + 1, int(settings.registration_auto_min_ready_auth_files or 1) + ), + ) + + scheduled_start = ( + next_check_at if next_check_at is not None else loop.time() + ) + wait_seconds = _remaining_delay(scheduled_start, loop.time()) + if wait_seconds > 0: + try: + await asyncio.wait_for( + self._wake_event.wait(), timeout=wait_seconds + ) + self._wake_event.clear() + except asyncio.TimeoutError: + pass + elif self._wake_event.is_set(): + self._wake_event.clear() + + try: + await self.run_once() + except asyncio.CancelledError: + raise + except Exception: + logger.exception("自动注册循环执行失败") + update_auto_registration_state( + status="error", + message="自动注册循环执行失败,请查看服务端日志", + last_checked_at=_timestamp(), + ) + add_auto_registration_log( + "[自动注册] 自动注册循环执行失败,请检查服务端日志" + ) + + next_check_at = loop.time() + interval diff --git a/src/core/db_logs.py b/src/core/db_logs.py index 00257d98..1bf94d37 100644 --- a/src/core/db_logs.py +++ b/src/core/db_logs.py @@ -15,6 +15,7 @@ from ..config.settings import get_settings from ..database.models import AppLog from ..database.session import get_db +from .timezone_utils import utcnow_naive _INSTALL_LOCK = threading.Lock() @@ -120,7 +121,7 @@ def cleanup_database_logs( keep_days = int(retention_days if retention_days is not None else settings.log_retention_days or 30) keep_days = max(1, keep_days) max_rows = max(1000, int(max_rows)) - cutoff = datetime.utcnow() - timedelta(days=keep_days) + cutoff = utcnow_naive() - timedelta(days=keep_days) deleted_by_age = 0 deleted_by_limit = 0 diff --git a/src/core/openai/token_refresh.py b/src/core/openai/token_refresh.py index 13e8ec8c..0626b707 100644 --- a/src/core/openai/token_refresh.py +++ b/src/core/openai/token_refresh.py @@ -16,6 +16,7 @@ from ...config.settings import get_settings from ...config.constants import AccountStatus from ...database.session import get_db +from ..timezone_utils import utcnow_naive from ...database import crud from ...database.models import Account @@ -250,7 +251,7 @@ def _request_once(session: cffi_requests.Session): return result # 计算过期时间 - expires_at = datetime.utcnow() + timedelta(seconds=expires_in) + expires_at = utcnow_naive() + timedelta(seconds=expires_in) result.success = True result.access_token = access_token @@ -370,7 +371,7 @@ def refresh_account_token(account_id: int, proxy_url: Optional[str] = None) -> T # 更新数据库 update_data = { "access_token": result.access_token, - "last_refresh": datetime.utcnow() + "last_refresh": utcnow_naive() } if result.refresh_token: diff --git a/src/core/register.py b/src/core/register.py index f4a098c9..de0f98df 100644 --- a/src/core/register.py +++ b/src/core/register.py @@ -96,7 +96,8 @@ def __init__( email_service: BaseEmailService, proxy_url: Optional[str] = None, callback_logger: Optional[Callable[[str], None]] = None, - task_uuid: Optional[str] = None + task_uuid: Optional[str] = None, + cancel_requested: Optional[Callable[[], bool]] = None ): """ 初始化注册引擎 @@ -111,6 +112,7 @@ def __init__( self.proxy_url = proxy_url self.callback_logger = callback_logger or (lambda msg: logger.info(msg)) self.task_uuid = task_uuid + self.cancel_requested = cancel_requested or (lambda: False) # 创建 HTTP 客户端 self.http_client = OpenAIHTTPClient(proxy_url=proxy_url) @@ -153,6 +155,24 @@ def __init__( self._last_otp_validation_status_code: Optional[int] = None self._last_otp_validation_outcome: str = "" # success/http_non_200/network_timeout/network_error + def _is_cancelled(self) -> bool: + try: + return bool(self.cancel_requested()) + except Exception: + return False + + def _raise_if_cancelled(self) -> None: + if self._is_cancelled(): + raise RuntimeError("任务已取消") + + def _sleep_with_cancel(self, seconds: float) -> None: + remaining = max(0.0, float(seconds or 0)) + while remaining > 0: + self._raise_if_cancelled() + chunk = min(0.5, remaining) + time.sleep(chunk) + remaining -= chunk + def _log(self, message: str, level: str = "info"): """记录日志""" timestamp = datetime.now().strftime("%H:%M:%S") @@ -449,7 +469,7 @@ def _get_device_id(self) -> Optional[str]: ) if attempt < max_attempts: - time.sleep(attempt) + self._sleep_with_cancel(attempt) self.http_client.close() self.session = self.http_client.session @@ -536,7 +556,7 @@ def _submit_auth_start( f"{log_label}命中限流 429(第 {attempt}/{max_attempts} 次),{wait_seconds}s 后自动重试...", "warning", ) - time.sleep(wait_seconds) + self._sleep_with_cancel(wait_seconds) continue # 部分网络/会话边界情况下会返回 409,做自愈重试而非直接失败。 @@ -560,7 +580,7 @@ def _submit_auth_start( self.session.get(str(self.oauth_start.auth_url), timeout=12) except Exception: pass - time.sleep(wait_seconds) + self._sleep_with_cancel(wait_seconds) continue if response.status_code != 200: @@ -603,7 +623,7 @@ def _submit_auth_start( f"{log_label}异常(第 {attempt}/{max_attempts} 次): {e},准备重试...", "warning", ) - time.sleep(2 * attempt) + self._sleep_with_cancel(2 * attempt) continue self._log(f"{log_label}失败: {e}", "error") return SignupFormResult(success=False, error_message=str(e)) @@ -680,7 +700,7 @@ def _submit_login_password(self) -> SignupFormResult: f"提交登录密码命中限流 429(第 {attempt}/{max_attempts} 次),{wait_seconds}s 后自动重试...", "warning", ) - time.sleep(wait_seconds) + self._sleep_with_cancel(wait_seconds) continue if response.status_code == 401 and attempt < max_attempts: @@ -692,7 +712,7 @@ def _submit_login_password(self) -> SignupFormResult: f"疑似密码尚未生效或历史账号密码不一致,{wait_seconds}s 后自动重试...", "warning", ) - time.sleep(wait_seconds) + self._sleep_with_cancel(wait_seconds) continue if response.status_code != 200: @@ -723,7 +743,7 @@ def _submit_login_password(self) -> SignupFormResult: f"提交登录密码异常(第 {attempt}/{max_attempts} 次): {e},准备重试...", "warning", ) - time.sleep(2 * attempt) + self._sleep_with_cancel(2 * attempt) continue self._log(f"提交登录密码失败: {e}", "error") return SignupFormResult(success=False, error_message=str(e)) @@ -2103,6 +2123,7 @@ def _send_verification_code(self, referer: Optional[str] = None) -> bool: def _get_verification_code(self, timeout: Optional[int] = None) -> Optional[str]: """获取验证码""" try: + self._raise_if_cancelled() mailbox_email = str(self.inbox_email or self.email or "").strip() self._log(f"正在等待邮箱 {mailbox_email} 的验证码...") @@ -2239,7 +2260,7 @@ def _verify_email_otp_with_retry( f"{stage_label}第 {attempt}/{max_attempts} 次未取到验证码,稍后重试...", "warning", ) - time.sleep(2) + self._sleep_with_cancel(2) continue return False @@ -2257,7 +2278,7 @@ def _verify_email_otp_with_retry( if self._validate_verification_code(code): return True if attempt < max_attempts: - time.sleep(2) + self._sleep_with_cancel(2) continue return False @@ -2266,7 +2287,7 @@ def _verify_email_otp_with_retry( f"{stage_label}第 {attempt}/{max_attempts} 次命中重复验证码 {code},等待新邮件...", "warning", ) - time.sleep(2) + self._sleep_with_cancel(2) continue return False @@ -2280,7 +2301,7 @@ def _verify_email_otp_with_retry( f"{stage_label}第 {attempt}/{max_attempts} 次校验未通过,疑似旧验证码,自动重试下一封...", "warning", ) - time.sleep(2) + self._sleep_with_cancel(2) return False @@ -2638,6 +2659,7 @@ def run(self) -> RegistrationResult: self._create_account_refresh_token = None self._last_validate_otp_continue_url = None self._last_validate_otp_workspace_id = None + self._raise_if_cancelled() self._log("=" * 60) self._log("注册流程启动,开始替你敲门") @@ -2660,6 +2682,7 @@ def run(self) -> RegistrationResult: return result self._log(f"IP 位置: {location}") + self._raise_if_cancelled() # 2. 创建邮箱 self._log("2. 开个新邮箱,准备收信...") @@ -2668,6 +2691,7 @@ def run(self) -> RegistrationResult: return result result.email = self.email + self._raise_if_cancelled() # 3. 准备首轮授权流程 did, sen_token = self._prepare_authorize_flow("首次授权") @@ -2678,6 +2702,7 @@ def run(self) -> RegistrationResult: if not sen_token: result.error_message = "Sentinel POW 验证失败" return result + self._raise_if_cancelled() # 4. 提交注册入口邮箱 self._log("4. 递上邮箱,看看 OpenAI 这球怎么接...") @@ -2710,6 +2735,7 @@ def run(self) -> RegistrationResult: if not self._create_user_account(): result.error_message = "创建用户账户失败" return result + self._raise_if_cancelled() if effective_entry_flow in {"native", "outlook"}: login_ready, login_error = self._restart_login_flow() diff --git a/src/core/timezone_utils.py b/src/core/timezone_utils.py index 73051c92..ad87c671 100644 --- a/src/core/timezone_utils.py +++ b/src/core/timezone_utils.py @@ -38,6 +38,10 @@ def now_shanghai() -> datetime: return datetime.now(UTC).astimezone(SHANGHAI_TZ) +def utcnow_naive() -> datetime: + return datetime.now(UTC).replace(tzinfo=None) + + def to_utc(dt: datetime | None) -> datetime | None: if dt is None: return None diff --git a/src/core/upload/cpa_upload.py b/src/core/upload/cpa_upload.py index 900cff6a..4fe584df 100644 --- a/src/core/upload/cpa_upload.py +++ b/src/core/upload/cpa_upload.py @@ -14,6 +14,7 @@ from ...database.session import get_db from ...database.models import Account from ...config.settings import get_settings +from ..timezone_utils import utcnow_naive logger = logging.getLogger(__name__) @@ -239,7 +240,7 @@ def batch_upload_to_cpa( if success: # 更新数据库状态 account.cpa_uploaded = True - account.cpa_uploaded_at = datetime.utcnow() + account.cpa_uploaded_at = utcnow_naive() db.commit() results["success_count"] += 1 @@ -261,6 +262,70 @@ def batch_upload_to_cpa( return results +def list_cpa_auth_files(api_url: str, api_token: str) -> Tuple[bool, Any, str]: + """列出远端 CPA auth-files 清单。""" + if not api_url: + return False, None, "API URL 不能为空" + + if not api_token: + return False, None, "API Token 不能为空" + + list_url = _normalize_cpa_auth_files_url(api_url) + headers = _build_cpa_headers(api_token) + + try: + response = cffi_requests.get( + list_url, + headers=headers, + proxies=None, + timeout=10, + impersonate="chrome110", + ) + if response.status_code != 200: + return False, None, _extract_cpa_error(response) + return True, response.json(), "ok" + except cffi_requests.exceptions.ConnectionError as e: + return False, None, f"无法连接到服务器: {str(e)}" + except cffi_requests.exceptions.Timeout: + return False, None, "连接超时,请检查网络配置" + except Exception as e: + logger.error("获取 CPA auth-files 清单异常: %s", e) + return False, None, f"获取 auth-files 失败: {str(e)}" + + +def count_ready_cpa_auth_files(payload: Any) -> int: + """统计可用于补货判断的认证文件数量。""" + if isinstance(payload, dict): + files = payload.get("files", []) + elif isinstance(payload, list): + files = payload + else: + return 0 + + ready_count = 0 + for item in files: + if not isinstance(item, dict): + continue + + status = str(item.get("status", "")).strip().lower() + provider = str(item.get("provider") or item.get("type") or "").strip().lower() + disabled = bool(item.get("disabled", False)) + unavailable = bool(item.get("unavailable", False)) + + if disabled or unavailable: + continue + + if provider != "codex": + continue + + if status and status not in {"ready", "active"}: + continue + + ready_count += 1 + + return ready_count + + def test_cpa_connection(api_url: str, api_token: str, proxy: str = None) -> Tuple[bool, str]: """ 测试 CPA 连接(不走代理) diff --git a/src/database/crud.py b/src/database/crud.py index 3c6c8492..e4ecbb71 100644 --- a/src/database/crud.py +++ b/src/database/crud.py @@ -8,6 +8,7 @@ from sqlalchemy import and_, or_, desc, asc, func from .models import Account, EmailService, RegistrationTask, Setting, Proxy, CpaService, Sub2ApiService +from ..core.timezone_utils import utcnow_naive # ============================================================================ @@ -53,7 +54,7 @@ def create_account( extra_data=extra_data or {}, status=status or 'active', source=source or 'register', - registered_at=datetime.utcnow() + registered_at=utcnow_naive() ) db.add(db_account) db.commit() @@ -360,7 +361,7 @@ def set_setting( db_setting.value = value db_setting.description = description or db_setting.description db_setting.category = category - db_setting.updated_at = datetime.utcnow() + db_setting.updated_at = utcnow_naive() else: db_setting = Setting( key=key, @@ -480,7 +481,7 @@ def update_proxy_last_used(db: Session, proxy_id: int) -> bool: if not db_proxy: return False - db_proxy.last_used = datetime.utcnow() + db_proxy.last_used = utcnow_naive() db.commit() return True diff --git a/src/database/models.py b/src/database/models.py index ff0e4443..26b9be3a 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -2,17 +2,20 @@ SQLAlchemy ORM 模型定义 """ -from datetime import datetime +from datetime import datetime, UTC from typing import Optional, Dict, Any import json from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey -from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.types import TypeDecorator -from sqlalchemy.orm import relationship +from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() +def utcnow() -> datetime: + return datetime.now(UTC).replace(tzinfo=None) + + class JSONEncodedDict(TypeDecorator): """JSON 编码字典类型""" impl = Text @@ -45,7 +48,7 @@ class Account(Base): email_service = Column(String(50), nullable=False) # 'tempmail', 'outlook', 'moe_mail' email_service_id = Column(String(255)) # 邮箱服务中的ID proxy_used = Column(String(255)) - registered_at = Column(DateTime, default=datetime.utcnow) + registered_at = Column(DateTime, default=utcnow) last_refresh = Column(DateTime) # 最后刷新时间 expires_at = Column(DateTime) # Token 过期时间 status = Column(String(20), default='active') # 'active', 'expired', 'banned', 'failed' @@ -56,8 +59,8 @@ class Account(Base): subscription_type = Column(String(20)) # None / 'plus' / 'team' subscription_at = Column(DateTime) # 订阅开通时间 cookies = Column(Text) # 完整 cookie 字符串,用于支付请求 - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=utcnow) + updated_at = Column(DateTime, default=utcnow, onupdate=utcnow) bind_card_tasks = relationship("BindCardTask", back_populates="account") def to_dict(self) -> Dict[str, Any]: @@ -96,8 +99,8 @@ class EmailService(Base): enabled = Column(Boolean, default=True) priority = Column(Integer, default=0) # 使用优先级 last_used = Column(DateTime) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=utcnow) + updated_at = Column(DateTime, default=utcnow, onupdate=utcnow) class RegistrationTask(Base): @@ -112,7 +115,7 @@ class RegistrationTask(Base): logs = Column(Text) # 注册过程日志 result = Column(JSONEncodedDict) # 注册结果 error_message = Column(Text) - created_at = Column(DateTime, default=datetime.utcnow) + created_at = Column(DateTime, default=utcnow) started_at = Column(DateTime) completed_at = Column(DateTime) @@ -143,8 +146,8 @@ class BindCardTask(Base): opened_at = Column(DateTime) last_checked_at = Column(DateTime) completed_at = Column(DateTime) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=utcnow) + updated_at = Column(DateTime, default=utcnow, onupdate=utcnow) # 关系 account = relationship("Account", back_populates="bind_card_tasks") @@ -162,7 +165,7 @@ class AppLog(Base): lineno = Column(Integer) message = Column(Text, nullable=False) exception = Column(Text) - created_at = Column(DateTime, default=datetime.utcnow, index=True) + created_at = Column(DateTime, default=utcnow, index=True) def to_dict(self) -> Dict[str, Any]: return { @@ -186,7 +189,7 @@ class Setting(Base): value = Column(Text) description = Column(Text) category = Column(String(50), default='general') # 'general', 'email', 'proxy', 'openai' - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + updated_at = Column(DateTime, default=utcnow, onupdate=utcnow) class CpaService(Base): @@ -200,8 +203,8 @@ class CpaService(Base): proxy_url = Column(String(1000)) # ?? URL enabled = Column(Boolean, default=True) priority = Column(Integer, default=0) # 优先级 - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=utcnow) + updated_at = Column(DateTime, default=utcnow, onupdate=utcnow) class Sub2ApiService(Base): @@ -215,8 +218,8 @@ class Sub2ApiService(Base): target_type = Column(String(50), nullable=False, default='sub2api') # sub2api/newapi enabled = Column(Boolean, default=True) priority = Column(Integer, default=0) # 优先级 - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=utcnow) + updated_at = Column(DateTime, default=utcnow, onupdate=utcnow) class TeamManagerService(Base): @@ -229,8 +232,8 @@ class TeamManagerService(Base): api_key = Column(Text, nullable=False) # X-API-Key enabled = Column(Boolean, default=True) priority = Column(Integer, default=0) # 优先级 - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=utcnow) + updated_at = Column(DateTime, default=utcnow, onupdate=utcnow) class Proxy(Base): @@ -248,8 +251,8 @@ class Proxy(Base): is_default = Column(Boolean, default=False) # 是否为默认代理 priority = Column(Integer, default=0) # 优先级(保留字段) last_used = Column(DateTime) # 最后使用时间 - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime, default=utcnow) + updated_at = Column(DateTime, default=utcnow, onupdate=utcnow) def to_dict(self, include_password: bool = False) -> Dict[str, Any]: """转换为字典""" diff --git a/src/web/app.py b/src/web/app.py index b679341d..e551858b 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -3,11 +3,13 @@ 轻量级 Web UI,支持注册、账号管理、设置 """ +import asyncio import logging import sys import secrets import hmac import hashlib +from contextlib import asynccontextmanager from typing import Optional, Dict, Any from pathlib import Path @@ -24,6 +26,7 @@ from .task_manager import task_manager logger = logging.getLogger(__name__) +auto_registration_coordinator = None # 获取项目根目录 # PyInstaller 打包后静态资源在 sys._MEIPASS,开发时在源码根目录 @@ -51,12 +54,81 @@ def create_app() -> FastAPI: """创建 FastAPI 应用实例""" settings = get_settings() + @asynccontextmanager + async def lifespan(app: FastAPI): + from ..database.init_db import initialize_database + from ..core.db_logs import cleanup_database_logs + from ..core.auto_registration import ( + AutoRegistrationCoordinator, + register_auto_registration_coordinator, + ) + from .routes.registration import run_auto_registration_batch + + try: + initialize_database() + except Exception as e: + logger.warning(f"数据库初始化: {e}") + + loop = asyncio.get_running_loop() + task_manager.set_loop(loop) + + global auto_registration_coordinator + auto_registration_coordinator = AutoRegistrationCoordinator( + trigger_callback=run_auto_registration_batch, + ) + register_auto_registration_coordinator(auto_registration_coordinator) + auto_registration_coordinator.start() + + async def run_log_cleanup_once(): + try: + result = await asyncio.to_thread(cleanup_database_logs) + logger.info( + "后台日志清理完成: 删除 %s 条,剩余 %s 条", + result.get("deleted_total", 0), + result.get("remaining", 0), + ) + except Exception as exc: + logger.warning(f"后台日志清理失败: {exc}") + + async def periodic_log_cleanup(): + while True: + try: + await asyncio.sleep(3600) + await run_log_cleanup_once() + except asyncio.CancelledError: + break + except Exception as exc: + logger.warning(f"后台日志定时清理异常: {exc}") + + await run_log_cleanup_once() + app.state.log_cleanup_task = asyncio.create_task(periodic_log_cleanup()) + + logger.info("=" * 50) + logger.info(f"{settings.app_name} v{settings.app_version} 启动中,程序正在伸懒腰...") + logger.info(f"调试模式: {settings.debug}") + logger.info(f"数据库连接已接好线: {settings.database_url}") + logger.info("=" * 50) + + try: + yield + finally: + if auto_registration_coordinator is not None: + await auto_registration_coordinator.stop() + register_auto_registration_coordinator(None) + auto_registration_coordinator = None + + cleanup_task = getattr(app.state, "log_cleanup_task", None) + if cleanup_task: + cleanup_task.cancel() + logger.info("应用关闭,今天先收摊啦") + app = FastAPI( title=settings.app_name, version=settings.app_version, description="OpenAI/Codex CLI 自动注册系统 Web UI", docs_url="/api/docs" if settings.debug else None, redoc_url="/api/redoc" if settings.debug else None, + lifespan=lifespan, ) # CORS 中间件 @@ -228,62 +300,6 @@ async def logs_page(request: Request): return _redirect_to_login(request) return _render_template(request, "logs.html") - @app.on_event("startup") - async def startup_event(): - """应用启动事件""" - import asyncio - from ..database.init_db import initialize_database - from ..core.db_logs import cleanup_database_logs - - # 确保数据库已初始化(reload 模式下子进程也需要初始化) - try: - initialize_database() - except Exception as e: - logger.warning(f"数据库初始化: {e}") - - # 设置 TaskManager 的事件循环 - loop = asyncio.get_event_loop() - task_manager.set_loop(loop) - - async def run_log_cleanup_once(): - try: - result = await asyncio.to_thread(cleanup_database_logs) - logger.info( - "后台日志清理完成: 删除 %s 条,剩余 %s 条", - result.get("deleted_total", 0), - result.get("remaining", 0), - ) - except Exception as exc: - logger.warning(f"后台日志清理失败: {exc}") - - async def periodic_log_cleanup(): - while True: - try: - await asyncio.sleep(3600) # 每小时清理一次 - await run_log_cleanup_once() - except asyncio.CancelledError: - break - except Exception as exc: - logger.warning(f"后台日志定时清理异常: {exc}") - - # 启动时先执行一次,再开启定时任务 - await run_log_cleanup_once() - app.state.log_cleanup_task = asyncio.create_task(periodic_log_cleanup()) - - logger.info("=" * 50) - logger.info(f"{settings.app_name} v{settings.app_version} 启动中,程序正在伸懒腰...") - logger.info(f"调试模式: {settings.debug}") - logger.info(f"数据库连接已接好线: {settings.database_url}") - logger.info("=" * 50) - - @app.on_event("shutdown") - async def shutdown_event(): - """应用关闭事件""" - cleanup_task = getattr(app.state, "log_cleanup_task", None) - if cleanup_task: - cleanup_task.cancel() - logger.info("应用关闭,今天先收摊啦") - return app diff --git a/src/web/routes/accounts.py b/src/web/routes/accounts.py index 4024d90e..699ce731 100644 --- a/src/web/routes/accounts.py +++ b/src/web/routes/accounts.py @@ -14,7 +14,7 @@ from fastapi import APIRouter, HTTPException, Query, BackgroundTasks, Body from fastapi.responses import StreamingResponse -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from sqlalchemy import func from ...config.constants import AccountStatus @@ -27,6 +27,7 @@ from ...core.upload.sub2api_upload import batch_upload_to_sub2api, upload_to_sub2api from ...core.dynamic_proxy import get_proxy_url_for_task +from ...core.timezone_utils import utcnow_naive from ...database import crud from ...database.models import Account from ...database.session import get_db @@ -99,8 +100,7 @@ class AccountResponse(BaseModel): created_at: Optional[str] = None updated_at: Optional[str] = None - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) class AccountListResponse(BaseModel): @@ -587,7 +587,7 @@ def _get_account_overview_data( # 避免把本地已确认的付费订阅(plus/team)被远端偶发 free/basic 覆盖降级。 if detected_sub and current_sub != detected_sub: account.subscription_type = detected_sub - account.subscription_at = datetime.utcnow() if detected_sub else None + account.subscription_at = utcnow_naive() if detected_sub else None updated = True elif not detected_sub and current_sub in PAID_SUBSCRIPTION_TYPES: logger.info( @@ -660,7 +660,7 @@ async def create_manual_account(request: ManualAccountCreateRequest): ) if subscription_type: account.subscription_type = subscription_type - account.subscription_at = datetime.utcnow() + account.subscription_at = utcnow_naive() db.commit() db.refresh(account) except Exception as exc: @@ -821,14 +821,14 @@ def _safe_text(value: Optional[str]) -> Optional[str]: "proxy_used": _safe_text(item.proxy_used), "source": source, "extra_data": metadata, - "last_refresh": datetime.utcnow(), + "last_refresh": utcnow_naive(), } clean_update_payload = {k: v for k, v in update_payload.items() if v is not None} account = crud.update_account(db, exists.id, **clean_update_payload) if account is None: raise RuntimeError("更新账号失败") account.subscription_type = subscription_type - account.subscription_at = datetime.utcnow() if subscription_type else None + account.subscription_at = utcnow_naive() if subscription_type else None db.commit() result["updated"] += 1 continue @@ -853,7 +853,7 @@ def _safe_text(value: Optional[str]) -> Optional[str]: ) if subscription_type: account.subscription_type = subscription_type - account.subscription_at = datetime.utcnow() + account.subscription_at = utcnow_naive() db.commit() result["created"] += 1 except Exception as exc: @@ -1341,7 +1341,7 @@ async def get_account_tokens(account_id: int): # 若 DB 为空但 cookies 可解析到 session_token,自动回写,避免后续重复解析。 if resolved_session_token and not str(account.session_token or "").strip(): account.session_token = resolved_session_token - account.last_refresh = datetime.utcnow() + account.last_refresh = utcnow_naive() db.commit() db.refresh(account) @@ -1384,7 +1384,7 @@ async def update_account(account_id: int, request: AccountUpdateRequest): if request.session_token is not None: # 留空则清空,非空则更新 update_data["session_token"] = request.session_token or None - update_data["last_refresh"] = datetime.utcnow() + update_data["last_refresh"] = utcnow_naive() account = crud.update_account(db, account_id, **update_data) return account_to_response(account) @@ -2045,7 +2045,7 @@ async def upload_account_to_cpa(account_id: int, request: Optional[CPAUploadRequ if success: account.cpa_uploaded = True - account.cpa_uploaded_at = datetime.utcnow() + account.cpa_uploaded_at = utcnow_naive() db.commit() return {"success": True, "message": message} else: diff --git a/src/web/routes/email.py b/src/web/routes/email.py index 8dd4ef1f..b9d7f13f 100644 --- a/src/web/routes/email.py +++ b/src/web/routes/email.py @@ -6,7 +6,7 @@ from typing import List, Optional, Dict, Any from fastapi import APIRouter, HTTPException, Query -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from sqlalchemy import func from ...database import crud @@ -53,8 +53,7 @@ class EmailServiceResponse(BaseModel): created_at: Optional[str] = None updated_at: Optional[str] = None - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) class EmailServiceListResponse(BaseModel): diff --git a/src/web/routes/logs.py b/src/web/routes/logs.py index 5a4131ac..a86c1ba3 100644 --- a/src/web/routes/logs.py +++ b/src/web/routes/logs.py @@ -12,7 +12,7 @@ from sqlalchemy import func, or_ from ...core.db_logs import cleanup_database_logs -from ...core.timezone_utils import to_shanghai_iso +from ...core.timezone_utils import to_shanghai_iso, utcnow_naive from ...database.models import AppLog from ...database.session import get_db @@ -60,7 +60,7 @@ def list_logs( ) if since_minutes: - since_at = datetime.utcnow() - timedelta(minutes=since_minutes) + since_at = utcnow_naive() - timedelta(minutes=since_minutes) query = query.filter(AppLog.created_at >= since_at) total = query.count() diff --git a/src/web/routes/payment.py b/src/web/routes/payment.py index 17c30729..3c8302d9 100644 --- a/src/web/routes/payment.py +++ b/src/web/routes/payment.py @@ -31,6 +31,7 @@ open_url_incognito, check_subscription_status_detail, ) +from ...core.timezone_utils import utcnow_naive from ...core.openai.browser_bind import auto_bind_checkout_with_playwright from ...core.openai.random_billing import generate_random_billing_profile from ...core.openai.token_refresh import TokenRefreshManager @@ -945,7 +946,7 @@ def _bootstrap_session_token_by_relogin(db, account: Account, proxy: Optional[st account.cookies = fresh_cookies if forced_access: account.access_token = forced_access - account.last_refresh = datetime.utcnow() + account.last_refresh = utcnow_naive() db.commit() return "" @@ -954,7 +955,7 @@ def _bootstrap_session_token_by_relogin(db, account: Account, proxy: Optional[st if fresh_cookies: account.cookies = fresh_cookies account.session_token = session_token - account.last_refresh = datetime.utcnow() + account.last_refresh = utcnow_naive() db.commit() db.refresh(account) logger.info("会话补全登录成功: account_id=%s email=%s", account.id, account.email) @@ -1210,7 +1211,7 @@ def _request_session_token( account.refresh_token = refresh_result.refresh_token if refresh_result.expires_at: account.expires_at = refresh_result.expires_at - account.last_refresh = datetime.utcnow() + account.last_refresh = utcnow_naive() db.commit() db.refresh(account) for proxy_item in proxy_candidates: @@ -1284,7 +1285,7 @@ def _mask_card_number(number: Optional[str]) -> str: def _mark_task_paid_pending_sync(task: BindCardTask, reason: str) -> None: - now = datetime.utcnow() + now = utcnow_naive() task.status = "paid_pending_sync" task.completed_at = None task.last_checked_at = now @@ -1791,7 +1792,7 @@ def _refresh_account_token_for_subscription_check(account: Account, proxy: Optio account.refresh_token = refresh_result.refresh_token if refresh_result.expires_at: account.expires_at = refresh_result.expires_at - account.last_refresh = datetime.utcnow() + account.last_refresh = utcnow_naive() return True, None @@ -2167,7 +2168,7 @@ def get_account_session_diagnostic( "probe": probe_result, "notes": notes, "recommendation": recommendation, - "checked_at": datetime.utcnow().isoformat(), + "checked_at": utcnow_naive().isoformat(), }, } @@ -2227,7 +2228,7 @@ def save_account_session_token( account.session_token = token if request.merge_cookie: account.cookies = _upsert_cookie(account.cookies, "__Secure-next-auth.session-token", token) - account.last_refresh = datetime.utcnow() + account.last_refresh = utcnow_naive() db.commit() db.refresh(account) @@ -2376,7 +2377,7 @@ def create_bind_card_task(request: CreateBindCardTaskRequest): opened = open_url_incognito(link, account.cookies if account else None) if opened: task.status = "opened" - task.opened_at = datetime.utcnow() + task.opened_at = utcnow_naive() db.commit() db.refresh(task) logger.info("绑卡任务自动打开成功: task_id=%s mode=%s", task.id, bind_mode) @@ -2420,7 +2421,7 @@ def list_bind_card_tasks( tasks = query.order_by(BindCardTask.created_at.desc()).offset(offset).limit(page_size).all() # 自动收敛任务状态:如果账号已是 plus/team,任务自动标记完成。 - now = datetime.utcnow() + now = utcnow_naive() changed = False changed_count = 0 for task in tasks: @@ -2460,7 +2461,7 @@ def open_bind_card_task(task_id: int): if opened: if str(task.status or "") not in ("paid_pending_sync", "completed"): task.status = "opened" - task.opened_at = datetime.utcnow() + task.opened_at = utcnow_naive() task.last_error = None db.commit() db.refresh(task) @@ -2547,7 +2548,7 @@ def auto_bind_bind_card_task_third_party(task_id: int, request: ThirdPartyAutoBi task.bind_mode = "third_party" task.status = "verifying" task.last_error = None - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() db.commit() try: @@ -2580,7 +2581,7 @@ def auto_bind_bind_card_task_third_party(task_id: int, request: ThirdPartyAutoBi if assess_state == "failed": task.status = "failed" task.last_error = f"第三方返回失败: {assess_reason or 'unknown'}" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() db.commit() logger.warning( "第三方自动绑卡返回业务失败: task_id=%s account_id=%s endpoint=%s reason=%s response=%s", @@ -2600,7 +2601,7 @@ def auto_bind_bind_card_task_third_party(task_id: int, request: ThirdPartyAutoBi # 若第三方明确返回 challenge/requires_action,直接切换待用户完成,避免无意义轮询超时。 if _is_third_party_challenge_pending(third_party_assessment): task.status = "waiting_user_action" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() hint_reason = assess_reason or "requires_action" hint_payment_status = payment_status or "unknown" task.last_error = ( @@ -2660,13 +2661,13 @@ def auto_bind_bind_card_task_third_party(task_id: int, request: ThirdPartyAutoBi if poll_state == "failed": task.status = "failed" task.last_error = f"第三方状态失败: {poll_reason or 'unknown'}" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() db.commit() raise HTTPException(status_code=400, detail=f"第三方状态失败: {poll_reason or 'unknown'}") if poll_state != "success": task.status = "waiting_user_action" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() hint_reason = poll_reason or assess_reason or "pending_confirmation" hint_payment_status = poll_payment_status or payment_status or "unknown" task.last_error = ( @@ -2735,7 +2736,7 @@ def auto_bind_bind_card_task_third_party(task_id: int, request: ThirdPartyAutoBi except Exception as exc: task.status = "failed" task.last_error = f"第三方绑卡提交失败: {exc}" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() db.commit() logger.warning("第三方自动绑卡提交失败: task_id=%s error=%s", task.id, exc) raise HTTPException(status_code=500, detail=str(exc)) @@ -2773,7 +2774,7 @@ def auto_bind_bind_card_task_local(task_id: int, request: LocalAutoBindRequest): task.bind_mode = "local_auto" task.status = "verifying" task.last_error = None - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() db.commit() logger.info( @@ -2794,7 +2795,7 @@ def auto_bind_bind_card_task_local(task_id: int, request: LocalAutoBindRequest): ) if not resolved_session_token and not runtime_proxy: task.status = "failed" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() task.last_error = ( "当前账号缺少 session_token,且未检测到可用代理。" "请在设置中配置代理(或为本次任务传入 proxy)后重试全自动。" @@ -2815,7 +2816,7 @@ def auto_bind_bind_card_task_local(task_id: int, request: LocalAutoBindRequest): ) if not resolved_session_token: task.status = "failed" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() task.last_error = ( "会话补全未拿到 session_token。" "请先在支付页执行“会话诊断/自动补会话”," @@ -2855,7 +2856,7 @@ def auto_bind_bind_card_task_local(task_id: int, request: LocalAutoBindRequest): except Exception as exc: task.status = "failed" task.last_error = f"本地自动绑卡执行异常: {exc}" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() db.commit() logger.warning("本地自动绑卡执行异常: task_id=%s account_id=%s error=%s", task.id, account.id, exc) raise HTTPException(status_code=500, detail=f"本地自动绑卡执行失败: {exc}") @@ -2870,7 +2871,7 @@ def auto_bind_bind_card_task_local(task_id: int, request: LocalAutoBindRequest): if not success: if "playwright not installed" in message.lower(): task.status = "failed" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() task.last_error = ( "本地自动绑卡环境缺少 Playwright/Chromium。" "请先执行: pip install playwright && playwright install chromium" @@ -2905,7 +2906,7 @@ def auto_bind_bind_card_task_local(task_id: int, request: LocalAutoBindRequest): if manual_opened: hint += " 已自动为你打开手动验证窗口。" task.status = "waiting_user_action" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() task.last_error = hint db.commit() db.refresh(task) @@ -2931,7 +2932,7 @@ def auto_bind_bind_card_task_local(task_id: int, request: LocalAutoBindRequest): } task.status = "failed" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() task.last_error = f"本地自动绑卡失败: {message or 'unknown_error'}" db.commit() logger.warning( @@ -2983,7 +2984,7 @@ def sync_bind_card_task_subscription(task_id: int, request: SyncBindCardTaskRequ raise HTTPException(status_code=404, detail="任务关联账号不存在") proxy = _resolve_runtime_proxy(request.proxy, account) - now = datetime.utcnow() + now = utcnow_naive() try: detail, refreshed = _check_subscription_detail_with_retry( db=db, @@ -3093,7 +3094,7 @@ def mark_bind_card_task_user_action(task_id: int, request: MarkUserActionRequest ) previous_status = str(task.status or "") - now = datetime.utcnow() + now = utcnow_naive() task.status = "verifying" task.last_error = None task.last_checked_at = now @@ -3122,7 +3123,7 @@ def mark_bind_card_task_user_action(task_id: int, request: MarkUserActionRequest task.id, checks, status, detail.get("source"), detail.get("confidence"), bool(detail.get("token_refreshed")) ) except Exception as exc: - failed_at = datetime.utcnow() + failed_at = utcnow_naive() task.status = "failed" task.last_error = f"订阅检测失败: {exc}" task.last_checked_at = failed_at @@ -3130,7 +3131,7 @@ def mark_bind_card_task_user_action(task_id: int, request: MarkUserActionRequest logger.warning("绑卡任务验证失败: task_id=%s attempt=%s error=%s", task.id, checks, exc) raise HTTPException(status_code=500, detail=f"订阅检测失败: {exc}") - checked_at = datetime.utcnow() + checked_at = utcnow_naive() if status in ("plus", "team"): account.subscription_type = status account.subscription_at = checked_at @@ -3197,7 +3198,7 @@ def mark_bind_card_task_user_action(task_id: int, request: MarkUserActionRequest else: task.status = "waiting_user_action" task.last_error = timeout_msg + "请稍后点击“同步订阅”重试。" - task.last_checked_at = datetime.utcnow() + task.last_checked_at = utcnow_naive() task.completed_at = None db.commit() db.refresh(task) @@ -3268,7 +3269,7 @@ def batch_check_subscription(request: BatchCheckSubscriptionRequest): if status in ("plus", "team"): account.subscription_type = status - account.subscription_at = datetime.utcnow() + account.subscription_at = utcnow_naive() elif status == "free" and confidence == "high": account.subscription_type = None account.subscription_at = None @@ -3308,7 +3309,7 @@ def mark_subscription(account_id: int, request: MarkSubscriptionRequest): raise HTTPException(status_code=404, detail="账号不存在") account.subscription_type = None if request.subscription_type == "free" else request.subscription_type - account.subscription_at = datetime.utcnow() if request.subscription_type != "free" else None + account.subscription_at = utcnow_naive() if request.subscription_type != "free" else None db.commit() return {"success": True, "subscription_type": request.subscription_type} diff --git a/src/web/routes/registration.py b/src/web/routes/registration.py index cb89f056..e4e120a2 100644 --- a/src/web/routes/registration.py +++ b/src/web/routes/registration.py @@ -6,18 +6,26 @@ import logging import uuid import random -from datetime import datetime +from datetime import datetime, timezone from typing import List, Optional, Dict, Tuple from fastapi import APIRouter, HTTPException, Query, BackgroundTasks -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from ...database import crud from ...database.session import get_db from ...database.models import RegistrationTask, Proxy from ...core.register import RegistrationEngine, RegistrationResult from ...services import EmailServiceFactory, EmailServiceType -from ...config.settings import get_settings +from ...config.settings import get_settings, Settings +from ...core.auto_registration import ( + add_auto_registration_log, + get_auto_registration_inventory, + get_auto_registration_logs, + get_auto_registration_state, + update_auto_registration_state, +) +from ...core.timezone_utils import utcnow_naive from ..task_manager import task_manager logger = logging.getLogger(__name__) @@ -29,6 +37,23 @@ batch_tasks: Dict[str, dict] = {} +def _cancel_batch_tasks(batch_id: str) -> None: + batch = batch_tasks.get(batch_id) + if not batch: + return + + for task_uuid in batch.get("task_uuids", []): + task_manager.cancel_task(task_uuid) + + auto_state = get_auto_registration_state() + if auto_state.get("current_batch_id") == batch_id: + update_auto_registration_state( + status="cancelling", + message=f"自动补货取消中: {batch_id}", + ) + add_auto_registration_log(f"[自动注册] 已提交补货批量任务取消请求: {batch_id}") + + # ============== Proxy Helper Functions ============== def get_proxy_for_registration(db) -> Tuple[Optional[str], Optional[int]]: @@ -112,8 +137,7 @@ class RegistrationTaskResponse(BaseModel): started_at: Optional[str] = None completed_at: Optional[str] = None - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) class BatchRegistrationResponse(BaseModel): @@ -241,7 +265,7 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: task = crud.update_registration_task( db, task_uuid, status="running", - started_at=datetime.utcnow() + started_at=utcnow_naive() ) if not task: @@ -415,12 +439,26 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: email_service=email_service, proxy_url=actual_proxy_url, callback_logger=log_callback, - task_uuid=task_uuid + task_uuid=task_uuid, + cancel_requested=lambda: task_manager.is_cancelled(task_uuid) ) # 执行注册 result = engine.run() + if task_manager.is_cancelled(task_uuid): + cancellation_message = result.error_message or "任务已取消" + crud.update_registration_task( + db, + task_uuid, + status="cancelled", + completed_at=utcnow_naive(), + error_message=cancellation_message, + ) + task_manager.update_status(task_uuid, "cancelled", error=cancellation_message) + logger.info(f"注册任务已取消: {task_uuid}") + return + if result.success: # 更新代理使用时间 update_proxy_usage(db, proxy_id) @@ -451,7 +489,7 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: _ok, _msg = upload_to_cpa(token_data, api_url=_svc.api_url, api_token=_svc.api_token) if _ok: saved_account.cpa_uploaded = True - saved_account.cpa_uploaded_at = datetime.utcnow() + saved_account.cpa_uploaded_at = utcnow_naive() db.commit() log_callback(f"[CPA] 投递成功,服务站已签收: {_svc.name}") else: @@ -515,7 +553,7 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: crud.update_registration_task( db, task_uuid, status="completed", - completed_at=datetime.utcnow(), + completed_at=utcnow_naive(), result=result.to_dict() ) @@ -528,7 +566,7 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: crud.update_registration_task( db, task_uuid, status="failed", - completed_at=datetime.utcnow(), + completed_at=utcnow_naive(), error_message=result.error_message ) @@ -542,10 +580,21 @@ def _run_sync_registration_task(task_uuid: str, email_service_type: str, proxy: try: with get_db() as db: + if task_manager.is_cancelled(task_uuid): + crud.update_registration_task( + db, + task_uuid, + status="cancelled", + completed_at=utcnow_naive(), + error_message=str(e) or "任务已取消", + ) + task_manager.update_status(task_uuid, "cancelled", error=str(e) or "任务已取消") + return + crud.update_registration_task( db, task_uuid, status="failed", - completed_at=datetime.utcnow(), + completed_at=utcnow_naive(), error_message=str(e) ) @@ -626,6 +675,42 @@ def update_batch_status(**kwargs): return add_batch_log, update_batch_status +async def _wait_for_batch_delay(batch_id: str, seconds: int) -> bool: + remaining = max(0, int(seconds)) + while remaining > 0: + if task_manager.is_batch_cancelled(batch_id) or batch_tasks[batch_id]["cancelled"]: + return False + await asyncio.sleep(min(0.5, remaining)) + remaining -= 0.5 + return True + + +def _mark_batch_tasks_cancelled(batch_id: str, task_uuids: List[str]) -> None: + if not task_uuids: + return + + terminal_statuses = {"completed", "failed", "cancelled"} + task_statuses = {} + with get_db() as db: + for task_uuid in task_uuids: + task = crud.get_registration_task(db, task_uuid) + current_status = getattr(task, "status", None) + task_statuses[task_uuid] = current_status + if current_status not in terminal_statuses: + crud.update_registration_task(db, task_uuid, status="cancelled") + + current_completed = batch_tasks[batch_id]["completed"] + update_count = 0 + for task_uuid in task_uuids: + if not task_manager.is_cancelled(task_uuid): + task_manager.cancel_task(task_uuid) + if task_statuses.get(task_uuid) not in terminal_statuses: + update_count += 1 + + batch_tasks[batch_id]["completed"] = current_completed + update_count + task_manager.update_batch_status(batch_id, completed=batch_tasks[batch_id]["completed"]) + + async def run_batch_parallel( batch_id: str, task_uuids: List[str], @@ -653,6 +738,9 @@ async def run_batch_parallel( async def _run_one(idx: int, uuid: str): prefix = f"[任务{idx + 1}]" async with semaphore: + if task_manager.is_batch_cancelled(batch_id) or batch_tasks[batch_id]["cancelled"]: + _mark_batch_tasks_cancelled(batch_id, [uuid]) + return await run_registration_task( uuid, email_service_type, proxy, email_service_config, email_service_id, log_prefix=prefix, batch_id=batch_id, @@ -670,6 +758,8 @@ async def _run_one(idx: int, uuid: str): if t.status == "completed": new_success += 1 add_batch_log(f"{prefix} [成功] 注册成功") + elif t.status == "cancelled": + add_batch_log(f"{prefix} [取消] 注册已取消") elif t.status == "failed": new_failed += 1 add_batch_log(f"{prefix} [失败] 注册失败: {t.error_message}") @@ -736,6 +826,8 @@ async def _run_and_release(idx: int, uuid: str, pfx: str): if t.status == "completed": new_success += 1 add_batch_log(f"{pfx} [成功] 注册成功") + elif t.status == "cancelled": + add_batch_log(f"{pfx} [取消] 注册已取消") elif t.status == "failed": new_failed += 1 add_batch_log(f"{pfx} [失败] 注册失败: {t.error_message}") @@ -746,9 +838,7 @@ async def _run_and_release(idx: int, uuid: str, pfx: str): try: for i, task_uuid in enumerate(task_uuids): if task_manager.is_batch_cancelled(batch_id) or batch_tasks[batch_id]["cancelled"]: - with get_db() as db: - for remaining_uuid in task_uuids[i:]: - crud.update_registration_task(db, remaining_uuid, status="cancelled") + _mark_batch_tasks_cancelled(batch_id, task_uuids[i:]) add_batch_log("[取消] 批量任务已取消") update_batch_status(finished=True, status="cancelled") break @@ -763,7 +853,11 @@ async def _run_and_release(idx: int, uuid: str, pfx: str): if i < len(task_uuids) - 1 and not task_manager.is_batch_cancelled(batch_id): wait_time = random.randint(interval_min, interval_max) logger.info(f"批量任务 {batch_id}: 等待 {wait_time} 秒后启动下一个任务") - await asyncio.sleep(wait_time) + if not await _wait_for_batch_delay(batch_id, wait_time): + _mark_batch_tasks_cancelled(batch_id, task_uuids[i + 1:]) + add_batch_log("[取消] 批量任务在等待下一个任务期间已取消") + update_batch_status(finished=True, status="cancelled") + break if running_tasks_list: await asyncio.gather(*running_tasks_list, return_exceptions=True) @@ -817,6 +911,112 @@ async def run_batch_registration( ) +async def run_auto_registration_batch(plan, settings: Settings) -> str: + email_service_type = settings.registration_auto_email_service_type + try: + EmailServiceType(email_service_type) + except ValueError as exc: + raise ValueError(f"自动注册邮箱服务类型无效: {email_service_type}") from exc + + mode = settings.registration_auto_mode or "pipeline" + if mode not in ("parallel", "pipeline"): + raise ValueError(f"自动注册模式无效: {mode}") + + interval_min = max(0, int(settings.registration_auto_interval_min)) + interval_max = max(interval_min, int(settings.registration_auto_interval_max)) + concurrency = max(1, int(settings.registration_auto_concurrency)) + email_service_id = int(settings.registration_auto_email_service_id or 0) or None + proxy = settings.registration_auto_proxy.strip() or None + + batch_id = str(uuid.uuid4()) + task_uuids = [] + + with get_db() as db: + for _ in range(plan.deficit): + task_uuid = str(uuid.uuid4()) + crud.create_registration_task( + db, + task_uuid=task_uuid, + proxy=proxy, + email_service_id=email_service_id, + ) + task_uuids.append(task_uuid) + + update_auto_registration_state( + status="running", + message=f"自动补货任务运行中: {batch_id}", + current_batch_id=batch_id, + ) + add_auto_registration_log( + f"[自动注册] 已创建补货批量任务 {batch_id},计划注册 {len(task_uuids)} 个账号" + ) + logger.info( + "自动注册批量任务已创建: batch=%s, count=%s, cpa_service_id=%s", + batch_id, + len(task_uuids), + plan.cpa_service_id, + ) + + await run_batch_registration( + batch_id=batch_id, + task_uuids=task_uuids, + email_service_type=email_service_type, + proxy=proxy, + email_service_config=None, + email_service_id=email_service_id, + interval_min=interval_min, + interval_max=interval_max, + concurrency=concurrency, + mode=mode, + auto_upload_cpa=True, + cpa_service_ids=[plan.cpa_service_id], + auto_upload_sub2api=False, + sub2api_service_ids=[], + auto_upload_tm=False, + tm_service_ids=[], + ) + + batch = batch_tasks.get(batch_id) + if batch: + batch_cancelled = bool(batch.get("cancelled")) + current_auto_state = get_auto_registration_state() + refreshed_inventory = await asyncio.to_thread( + get_auto_registration_inventory, settings + ) + refreshed_ready_count = ( + refreshed_inventory[0] + if refreshed_inventory + else current_auto_state.get("current_ready_count") + ) + refreshed_target_count = ( + refreshed_inventory[1] + if refreshed_inventory + else max(1, int(settings.registration_auto_min_ready_auth_files or 1)) + ) + final_status = "cancelled" if batch_cancelled else "idle" + final_message = ( + f"自动补货批量任务已取消: {batch_id}" + if batch_cancelled + else f"自动补货批量任务已完成: {batch_id}" + ) + final_log_message = ( + f"[自动注册] 补货批量任务已取消:成功 {batch.get('success', 0)},失败 {batch.get('failed', 0)}" + if batch_cancelled + else f"[自动注册] 补货批量任务已完成:成功 {batch.get('success', 0)},失败 {batch.get('failed', 0)}" + ) + update_auto_registration_state( + status=final_status, + message=final_message, + current_batch_id=None, + current_ready_count=refreshed_ready_count, + target_ready_count=refreshed_target_count, + last_checked_at=datetime.now(timezone.utc).isoformat(), + ) + add_auto_registration_log(final_log_message) + + return batch_id + + # ============== API Endpoints ============== @router.post("/start", response_model=RegistrationTaskResponse) @@ -972,6 +1172,32 @@ async def get_batch_status(batch_id: str): } +@router.get("/auto-monitor") +async def get_auto_registration_monitor(): + auto_state = get_auto_registration_state() + current_batch_id = auto_state.get("current_batch_id") + batch = batch_tasks.get(current_batch_id) if current_batch_id else None + logs = get_auto_registration_logs().copy() + if batch and current_batch_id: + logs.extend(task_manager.get_batch_logs(current_batch_id)) + + return { + **auto_state, + "logs": logs, + "batch": { + "batch_id": current_batch_id, + "total": batch["total"], + "completed": batch["completed"], + "success": batch["success"], + "failed": batch["failed"], + "current_index": batch["current_index"], + "cancelled": batch["cancelled"], + "finished": batch.get("finished", False), + "progress": f"{batch['completed']}/{batch['total']}", + } if batch else None, + } + + @router.post("/batch/{batch_id}/cancel") async def cancel_batch(batch_id: str): """取消批量任务""" @@ -984,6 +1210,7 @@ async def cancel_batch(batch_id: str): batch["cancelled"] = True task_manager.cancel_batch(batch_id) + _cancel_batch_tasks(batch_id) return {"success": True, "message": "批量任务取消请求已提交,正在让它们有序收工"} @@ -1053,6 +1280,7 @@ async def cancel_task(task_uuid: str): raise HTTPException(status_code=400, detail="任务已完成或已取消") task = crud.update_registration_task(db, task_uuid, status="cancelled") + task_manager.cancel_task(task_uuid) return {"success": True, "message": "任务已取消"} @@ -1086,7 +1314,7 @@ async def get_registration_stats(): ).group_by(RegistrationTask.status).all() # 今日统计 - today = datetime.utcnow().date() + today = utcnow_naive().date() today_status_stats = db.query( RegistrationTask.status, func.count(RegistrationTask.id) diff --git a/src/web/routes/settings.py b/src/web/routes/settings.py index a41799c5..33754f7a 100644 --- a/src/web/routes/settings.py +++ b/src/web/routes/settings.py @@ -10,8 +10,13 @@ from pydantic import BaseModel from ...config.settings import get_settings, update_settings +from ...core.auto_registration import ( + trigger_auto_registration_check, + update_auto_registration_state, +) from ...database import crud from ...database.session import get_db +from ...services import EmailServiceType logger = logging.getLogger(__name__) router = APIRouter() @@ -50,6 +55,17 @@ class RegistrationSettings(BaseModel): sleep_min: int = 5 sleep_max: int = 30 entry_flow: str = "native" + auto_enabled: bool = False + auto_check_interval: int = 60 + auto_min_ready_auth_files: int = 1 + auto_email_service_type: str = "tempmail" + auto_email_service_id: int = 0 + auto_proxy: Optional[str] = None + auto_interval_min: int = 5 + auto_interval_max: int = 30 + auto_concurrency: int = 1 + auto_mode: str = "pipeline" + auto_cpa_service_id: int = 0 class WebUISettings(BaseModel): @@ -98,6 +114,17 @@ async def get_all_settings(): "sleep_min": settings.registration_sleep_min, "sleep_max": settings.registration_sleep_max, "entry_flow": entry_flow, + "auto_enabled": settings.registration_auto_enabled, + "auto_check_interval": settings.registration_auto_check_interval, + "auto_min_ready_auth_files": settings.registration_auto_min_ready_auth_files, + "auto_email_service_type": settings.registration_auto_email_service_type, + "auto_email_service_id": settings.registration_auto_email_service_id, + "auto_proxy": settings.registration_auto_proxy, + "auto_interval_min": settings.registration_auto_interval_min, + "auto_interval_max": settings.registration_auto_interval_max, + "auto_concurrency": settings.registration_auto_concurrency, + "auto_mode": settings.registration_auto_mode, + "auto_cpa_service_id": settings.registration_auto_cpa_service_id, }, "webui": { "host": settings.webui_host, @@ -228,18 +255,81 @@ async def get_registration_settings(): "sleep_min": settings.registration_sleep_min, "sleep_max": settings.registration_sleep_max, "entry_flow": entry_flow, + "auto_enabled": settings.registration_auto_enabled, + "auto_check_interval": settings.registration_auto_check_interval, + "auto_min_ready_auth_files": settings.registration_auto_min_ready_auth_files, + "auto_email_service_type": settings.registration_auto_email_service_type, + "auto_email_service_id": settings.registration_auto_email_service_id, + "auto_proxy": settings.registration_auto_proxy, + "auto_interval_min": settings.registration_auto_interval_min, + "auto_interval_max": settings.registration_auto_interval_max, + "auto_concurrency": settings.registration_auto_concurrency, + "auto_mode": settings.registration_auto_mode, + "auto_cpa_service_id": settings.registration_auto_cpa_service_id, } @router.post("/registration") async def update_registration_settings(request: RegistrationSettings): """更新注册设置""" + if request.timeout < 30 or request.timeout > 600: + raise HTTPException(status_code=400, detail="注册超时时间必须在 30-600 秒之间") + + if request.default_password_length < 8 or request.default_password_length > 64: + raise HTTPException(status_code=400, detail="密码长度必须在 8-64 之间") + + if request.sleep_min < 1 or request.sleep_max < request.sleep_min: + raise HTTPException(status_code=400, detail="注册等待时间参数无效") + flow_raw = (request.entry_flow or "native").strip().lower() # 兼容旧前端历史值:outlook -> native(Outlook 邮箱会在运行时自动走 outlook 链路)。 flow = "native" if flow_raw == "outlook" else flow_raw if flow not in {"native", "abcard"}: raise HTTPException(status_code=400, detail="entry_flow 仅支持 native / abcard") + if request.auto_check_interval < 5 or request.auto_check_interval > 3600: + raise HTTPException(status_code=400, detail="自动注册检查间隔必须在 5-3600 秒之间") + + if request.auto_min_ready_auth_files < 1 or request.auto_min_ready_auth_files > 10000: + raise HTTPException(status_code=400, detail="自动注册保底数量必须在 1-10000 之间") + + try: + EmailServiceType(request.auto_email_service_type) + except ValueError as exc: + raise HTTPException(status_code=400, detail="自动注册邮箱服务类型无效") from exc + + normalized_auto_email_service_type = ( + "imap_mail" if request.auto_email_service_type == "catchall_imap" else request.auto_email_service_type + ) + + if request.auto_interval_min < 0 or request.auto_interval_max < request.auto_interval_min: + raise HTTPException(status_code=400, detail="自动注册间隔时间参数无效") + + if request.auto_concurrency < 1 or request.auto_concurrency > 100: + raise HTTPException(status_code=400, detail="自动注册并发数必须在 1-100 之间") + + if request.auto_mode not in ("parallel", "pipeline"): + raise HTTPException(status_code=400, detail="自动注册模式必须为 parallel 或 pipeline") + + if request.auto_enabled and request.auto_cpa_service_id <= 0: + raise HTTPException(status_code=400, detail="启用自动注册时必须选择一个 CPA 服务") + + with get_db() as db: + if request.auto_enabled: + cpa_service = crud.get_cpa_service_by_id(db, request.auto_cpa_service_id) + if not cpa_service or not cpa_service.enabled: + raise HTTPException(status_code=400, detail="自动注册选择的 CPA 服务不存在或已禁用") + + if request.auto_email_service_id > 0: + email_service = crud.get_email_service_by_id(db, request.auto_email_service_id) + if not email_service or not email_service.enabled: + raise HTTPException(status_code=400, detail="自动注册选择的邮箱服务不存在或已禁用") + normalized_service_type = ( + "imap_mail" if email_service.service_type == "catchall_imap" else email_service.service_type + ) + if normalized_service_type != normalized_auto_email_service_type: + raise HTTPException(status_code=400, detail="自动注册邮箱服务类型与指定服务不匹配") + update_settings( registration_max_retries=request.max_retries, registration_timeout=request.timeout, @@ -247,8 +337,37 @@ async def update_registration_settings(request: RegistrationSettings): registration_sleep_min=request.sleep_min, registration_sleep_max=request.sleep_max, registration_entry_flow=flow, + registration_auto_enabled=request.auto_enabled, + registration_auto_check_interval=request.auto_check_interval, + registration_auto_min_ready_auth_files=request.auto_min_ready_auth_files, + registration_auto_email_service_type=normalized_auto_email_service_type, + registration_auto_email_service_id=max(0, request.auto_email_service_id), + registration_auto_proxy=(request.auto_proxy or "").strip(), + registration_auto_interval_min=request.auto_interval_min, + registration_auto_interval_max=request.auto_interval_max, + registration_auto_concurrency=request.auto_concurrency, + registration_auto_mode=request.auto_mode, + registration_auto_cpa_service_id=max(0, request.auto_cpa_service_id), ) + if request.auto_enabled: + update_auto_registration_state( + enabled=True, + status="checking", + message="自动注册设置已更新,正在立即检查库存", + target_ready_count=request.auto_min_ready_auth_files, + ) + trigger_auto_registration_check() + else: + update_auto_registration_state( + enabled=False, + status="disabled", + message="自动注册已禁用", + current_batch_id=None, + current_ready_count=None, + target_ready_count=request.auto_min_ready_auth_files, + ) + return {"success": True, "message": "注册设置已更新"} @@ -432,9 +551,10 @@ async def cleanup_database( keep_failed: bool = True ): """清理过期数据""" - from datetime import datetime, timedelta + from datetime import timedelta + from ...core.timezone_utils import utcnow_naive - cutoff_date = datetime.utcnow() - timedelta(days=days) + cutoff_date = utcnow_naive() - timedelta(days=days) with get_db() as db: from ...database.models import RegistrationTask diff --git a/src/web/routes/upload/cpa_services.py b/src/web/routes/upload/cpa_services.py index 9c636cb7..29076046 100644 --- a/src/web/routes/upload/cpa_services.py +++ b/src/web/routes/upload/cpa_services.py @@ -4,7 +4,7 @@ from typing import List, Optional from fastapi import APIRouter, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from ....database import crud from ....database.session import get_db @@ -44,8 +44,7 @@ class CpaServiceResponse(BaseModel): created_at: Optional[str] = None updated_at: Optional[str] = None - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) class CpaServiceTestRequest(BaseModel): diff --git a/src/web/routes/upload/sub2api_services.py b/src/web/routes/upload/sub2api_services.py index 653f4b19..91a23236 100644 --- a/src/web/routes/upload/sub2api_services.py +++ b/src/web/routes/upload/sub2api_services.py @@ -4,7 +4,7 @@ from typing import List, Optional from fastapi import APIRouter, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from ....database import crud from ....database.session import get_db @@ -43,8 +43,7 @@ class Sub2ApiServiceResponse(BaseModel): created_at: Optional[str] = None updated_at: Optional[str] = None - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) class Sub2ApiTestRequest(BaseModel): diff --git a/src/web/routes/upload/tm_services.py b/src/web/routes/upload/tm_services.py index b363139e..9fe61e20 100644 --- a/src/web/routes/upload/tm_services.py +++ b/src/web/routes/upload/tm_services.py @@ -4,7 +4,7 @@ from typing import List, Optional from fastapi import APIRouter, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from ....database import crud from ....database.session import get_db @@ -41,8 +41,7 @@ class TmServiceResponse(BaseModel): created_at: Optional[str] = None updated_at: Optional[str] = None - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) class TmTestRequest(BaseModel): diff --git a/src/web/routes/websocket.py b/src/web/routes/websocket.py index d864f837..63bf9ab3 100644 --- a/src/web/routes/websocket.py +++ b/src/web/routes/websocket.py @@ -8,6 +8,7 @@ from fastapi import APIRouter, WebSocket, WebSocketDisconnect from ..task_manager import task_manager +from .registration import _cancel_batch_tasks logger = logging.getLogger(__name__) router = APIRouter() @@ -145,6 +146,7 @@ async def batch_websocket(websocket: WebSocket, batch_id: str): # 处理取消请求 elif data.get("type") == "cancel": task_manager.cancel_batch(batch_id) + _cancel_batch_tasks(batch_id) await websocket.send_json({ "type": "status", "batch_id": batch_id, diff --git a/src/web/task_manager.py b/src/web/task_manager.py index fde9adf7..538adf00 100644 --- a/src/web/task_manager.py +++ b/src/web/task_manager.py @@ -9,7 +9,8 @@ from concurrent.futures import ThreadPoolExecutor from typing import Dict, Optional, List, Callable, Any from collections import defaultdict -from datetime import datetime + +from ..core.timezone_utils import utcnow_naive logger = logging.getLogger(__name__) @@ -115,7 +116,7 @@ async def _broadcast_log(self, task_uuid: str, log_message: str): "type": "log", "task_uuid": task_uuid, "message": log_message, - "timestamp": datetime.utcnow().isoformat() + "timestamp": utcnow_naive().isoformat() }) # 发送成功后更新 sent_index with _ws_lock: @@ -134,7 +135,7 @@ async def broadcast_status(self, task_uuid: str, status: str, **kwargs): "type": "status", "task_uuid": task_uuid, "status": status, - "timestamp": datetime.utcnow().isoformat(), + "timestamp": utcnow_naive().isoformat(), **kwargs } @@ -264,7 +265,7 @@ async def _broadcast_batch_log(self, batch_id: str, log_message: str): "type": "log", "batch_id": batch_id, "message": log_message, - "timestamp": datetime.utcnow().isoformat() + "timestamp": utcnow_naive().isoformat() }) # 发送成功后更新 sent_index with _ws_lock: @@ -304,7 +305,7 @@ async def _broadcast_batch_status(self, batch_id: str): await ws.send_json({ "type": "status", "batch_id": batch_id, - "timestamp": datetime.utcnow().isoformat(), + "timestamp": utcnow_naive().isoformat(), **status }) except Exception as e: diff --git a/static/js/app.js b/static/js/app.js index 8fd65b6d..32e4afa6 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -8,11 +8,13 @@ let currentTask = null; let currentBatch = null; let logPollingInterval = null; let batchPollingInterval = null; +let autoMonitorPollingInterval = null; let accountsPollingInterval = null; let todayStatsPollingInterval = null; let todayStatsResetInterval = null; let isBatchMode = false; let isOutlookBatchMode = false; +let isAutoMode = false; let outlookAccounts = []; let taskCompleted = false; // 标记任务是否已完成 let batchCompleted = false; // 标记批量任务是否已完成 @@ -44,6 +46,7 @@ let wsReconnectAttempts = 0; let batchWsReconnectAttempts = 0; let wsManualClose = false; let batchWsManualClose = false; +let autoMonitorLastLogIndex = 0; const WS_RECONNECT_BASE_DELAY = 1000; const WS_RECONNECT_MAX_DELAY = 10000; @@ -52,6 +55,7 @@ const WS_RECONNECT_MAX_DELAY = 10000; const elements = { form: document.getElementById('registration-form'), emailService: document.getElementById('email-service'), + emailServiceGroup: document.getElementById('email-service')?.closest('.form-group'), regMode: document.getElementById('reg-mode'), regModeGroup: document.getElementById('reg-mode-group'), batchCountGroup: document.getElementById('batch-count-group'), @@ -71,6 +75,10 @@ const elements = { taskStatus: document.getElementById('task-status'), taskService: document.getElementById('task-service'), taskStatusBadge: document.getElementById('task-status-badge'), + autoMonitorStatusBadge: document.getElementById('auto-monitor-status-badge'), + autoMonitorLastChecked: document.getElementById('auto-monitor-last-checked'), + taskLastChecked: document.getElementById('task-last-checked'), + taskInventory: document.getElementById('task-inventory'), // 批量状态 batchProgressText: document.getElementById('batch-progress-text'), batchProgressPercent: document.getElementById('batch-progress-percent'), @@ -112,13 +120,29 @@ const elements = { autoUploadTm: document.getElementById('auto-upload-tm'), tmServiceSelectGroup: document.getElementById('tm-service-select-group'), tmServiceSelect: document.getElementById('tm-service-select'), + autoRegistrationSection: document.getElementById('auto-registration-section'), + autoRegistrationEnabled: document.getElementById('auto-registration-enabled'), + autoRegistrationCheckInterval: document.getElementById('auto-registration-check-interval'), + autoRegistrationMinReady: document.getElementById('auto-registration-min-ready'), + autoRegistrationCpaServiceId: document.getElementById('auto-registration-cpa-service-id'), + autoRegistrationEmailServiceType: document.getElementById('auto-registration-email-service-type'), + autoRegistrationEmailServiceId: document.getElementById('auto-registration-email-service-id'), + autoRegistrationProxy: document.getElementById('auto-registration-proxy'), + autoRegistrationMode: document.getElementById('auto-registration-mode'), + autoRegistrationConcurrency: document.getElementById('auto-registration-concurrency'), + autoRegistrationIntervalGroup: document.getElementById('auto-registration-interval-group'), + autoRegistrationIntervalMin: document.getElementById('auto-registration-interval-min'), + autoRegistrationIntervalMax: document.getElementById('auto-registration-interval-max'), }; // 初始化 document.addEventListener('DOMContentLoaded', () => { initEventListeners(); + handleModeChange({ target: elements.regMode }); loadAvailableServices(); loadRecentAccounts(); + loadAutoRegistrationSettings(); + loadAutoRegistrationCpaOptions(); startAccountsPolling(); loadTodayStats(true); startTodayStatsPolling(); @@ -216,6 +240,18 @@ function initEventListeners() { // 邮箱服务切换 elements.emailService.addEventListener('change', handleServiceChange); + if (elements.autoRegistrationEmailServiceType) { + elements.autoRegistrationEmailServiceType.addEventListener('change', () => populateAutoRegistrationEmailServiceOptions(0)); + } + if (elements.autoRegistrationMode) { + elements.autoRegistrationMode.addEventListener('change', () => { + handleConcurrencyModeChange( + elements.autoRegistrationMode, + elements.concurrencyHint, + elements.autoRegistrationIntervalGroup + ); + }); + } // 取消按钮 elements.cancelBtn.addEventListener('click', handleCancelTask); @@ -249,6 +285,7 @@ async function loadAvailableServices() { // 更新邮箱服务选择框 updateEmailServiceOptions(); + populateAutoRegistrationEmailServiceOptions(parseInt(elements.autoRegistrationEmailServiceId?.value || '0', 10) || 0); addLog('info', '[系统] 邮箱服务列表已加载'); } catch (error) { @@ -467,10 +504,30 @@ function handleServiceChange(e) { // 模式切换 function handleModeChange(e) { const mode = e.target.value; + isAutoMode = mode === 'auto'; isBatchMode = mode === 'batch'; elements.batchCountGroup.style.display = isBatchMode ? 'block' : 'none'; elements.batchOptions.style.display = isBatchMode ? 'block' : 'none'; + if (elements.autoRegistrationSection) { + elements.autoRegistrationSection.style.display = isAutoMode ? 'block' : 'none'; + } + if (elements.emailServiceGroup) { + elements.emailServiceGroup.style.display = isAutoMode ? 'none' : 'block'; + } + const autoUploadGroup = elements.autoUploadCpa?.closest('#auto-upload-group'); + if (autoUploadGroup) { + autoUploadGroup.style.display = isAutoMode ? 'none' : 'block'; + } + elements.startBtn.textContent = isAutoMode ? '💾 保存自动注册设置' : '🚀 开始注册'; + + if (isAutoMode) { + elements.cancelBtn.disabled = false; + } else { + stopAutoRegistrationMonitor(); + updateAutoMonitorHeader('idle', null); + elements.cancelBtn.disabled = true; + } } // 并发模式切换(批量) @@ -489,6 +546,11 @@ function handleConcurrencyModeChange(selectEl, hintEl, intervalGroupEl) { async function handleStartRegistration(e) { e.preventDefault(); + if (isAutoMode) { + await handleSaveAutoRegistration(); + return; + } + const selectedValue = elements.emailService.value; if (!selectedValue) { toast.error('请选择一个邮箱服务'); @@ -533,6 +595,142 @@ async function handleStartRegistration(e) { } } + +async function loadAutoRegistrationSettings() { + if (!elements.autoRegistrationEnabled) return; + try { + const data = await api.get('/settings'); + const reg = data.registration || {}; + elements.autoRegistrationEnabled.checked = reg.auto_enabled || false; + elements.autoRegistrationCheckInterval.value = reg.auto_check_interval || 60; + elements.autoRegistrationMinReady.value = reg.auto_min_ready_auth_files || 1; + elements.autoRegistrationEmailServiceType.value = reg.auto_email_service_type || 'tempmail'; + elements.autoRegistrationProxy.value = reg.auto_proxy || ''; + elements.autoRegistrationMode.value = reg.auto_mode || 'pipeline'; + elements.autoRegistrationConcurrency.value = reg.auto_concurrency || 1; + elements.autoRegistrationIntervalMin.value = reg.auto_interval_min || 5; + elements.autoRegistrationIntervalMax.value = reg.auto_interval_max || 30; + handleConcurrencyModeChange( + elements.autoRegistrationMode, + elements.concurrencyHint, + elements.autoRegistrationIntervalGroup + ); + elements.autoRegistrationEmailServiceId.dataset.selectedId = String(reg.auto_email_service_id || 0); + elements.autoRegistrationCpaServiceId.dataset.selectedId = String(reg.auto_cpa_service_id || 0); + populateAutoRegistrationEmailServiceOptions(reg.auto_email_service_id || 0); + } catch (error) { + console.error('加载自动注册设置失败:', error); + } +} + +async function loadAutoRegistrationCpaOptions() { + if (!elements.autoRegistrationCpaServiceId) return; + try { + const services = await api.get('/cpa-services?enabled=true'); + const options = ['']; + services.forEach(service => { + options.push(``); + }); + elements.autoRegistrationCpaServiceId.innerHTML = options.join(''); + elements.autoRegistrationCpaServiceId.value = elements.autoRegistrationCpaServiceId.dataset.selectedId || '0'; + } catch (error) { + console.error('加载 CPA 服务失败:', error); + } +} + +function populateAutoRegistrationEmailServiceOptions(selectedId = 0) { + if (!elements.autoRegistrationEmailServiceId || !elements.autoRegistrationEmailServiceType) return; + const selectedType = elements.autoRegistrationEmailServiceType.value || 'tempmail'; + const options = ['']; + const bucket = availableServices[selectedType]; + if (bucket && Array.isArray(bucket.services)) { + bucket.services.forEach(service => { + options.push(``); + }); + } + elements.autoRegistrationEmailServiceId.innerHTML = options.join(''); + elements.autoRegistrationEmailServiceId.value = String(selectedId || elements.autoRegistrationEmailServiceId.dataset.selectedId || 0); +} + +async function handleSaveAutoRegistration() { + const autoCheckInterval = parseInt(elements.autoRegistrationCheckInterval.value, 10) || 60; + const autoMinReady = parseInt(elements.autoRegistrationMinReady.value, 10) || 1; + const autoEmailServiceId = parseInt(elements.autoRegistrationEmailServiceId.value, 10) || 0; + const autoConcurrency = parseInt(elements.autoRegistrationConcurrency.value, 10) || 1; + const autoIntervalMin = parseInt(elements.autoRegistrationIntervalMin.value, 10) || 0; + const autoIntervalMax = parseInt(elements.autoRegistrationIntervalMax.value, 10) || 0; + const autoCpaServiceId = parseInt(elements.autoRegistrationCpaServiceId.value, 10) || 0; + + if (autoCheckInterval < 5 || autoCheckInterval > 3600) { + toast.error('自动注册检查间隔必须在 5-3600 秒之间'); + return; + } + if (autoMinReady < 1 || autoMinReady > 10000) { + toast.error('自动注册保底数量必须在 1-10000 之间'); + return; + } + if (autoIntervalMin < 0 || autoIntervalMax < autoIntervalMin) { + toast.error('自动注册启动间隔参数无效'); + return; + } + if (autoConcurrency < 1 || autoConcurrency > 100) { + toast.error('自动注册并发数必须在 1-100 之间'); + return; + } + if (elements.autoRegistrationEnabled.checked && autoCpaServiceId <= 0) { + toast.error('启用自动注册前请先选择一个 CPA 服务'); + return; + } + + const data = await api.get('/settings'); + const reg = data.registration || {}; + const payload = { + max_retries: reg.max_retries || 3, + timeout: reg.timeout || 120, + default_password_length: reg.default_password_length || 12, + entry_flow: reg.entry_flow || 'native', + sleep_min: reg.sleep_min || 5, + sleep_max: reg.sleep_max || 30, + auto_enabled: elements.autoRegistrationEnabled.checked, + auto_check_interval: autoCheckInterval, + auto_min_ready_auth_files: autoMinReady, + auto_email_service_type: elements.autoRegistrationEmailServiceType.value, + auto_email_service_id: autoEmailServiceId, + auto_proxy: elements.autoRegistrationProxy.value.trim(), + auto_interval_min: autoIntervalMin, + auto_interval_max: autoIntervalMax, + auto_concurrency: autoConcurrency, + auto_mode: elements.autoRegistrationMode.value, + auto_cpa_service_id: autoCpaServiceId, + }; + + await api.post('/settings/registration', payload); + toast.success('自动注册设置已保存'); + + if (elements.autoRegistrationEnabled.checked) { + sessionStorage.setItem('activeTask', JSON.stringify({ mode: 'auto' })); + autoMonitorLastLogIndex = 0; + displayedLogs.clear(); + elements.consoleLog.innerHTML = ''; + addLog('info', '[系统] 自动注册监控已启动'); + startAutoRegistrationMonitor(); + } else { + stopAutoRegistrationMonitor(); + const saved = sessionStorage.getItem('activeTask'); + if (saved) { + try { + const parsed = JSON.parse(saved); + if (parsed.mode === 'auto') { + sessionStorage.removeItem('activeTask'); + } + } catch { + sessionStorage.removeItem('activeTask'); + } + } + addLog('info', '[系统] 自动注册已禁用'); + } +} + // 单次注册 async function handleSingleRegistration(requestData) { // 重置任务状态 @@ -838,7 +1036,7 @@ async function handleCancelTask() { try { // 批量任务取消(包括普通批量模式和 Outlook 批量模式) - if (currentBatch && (isBatchMode || isOutlookBatchMode)) { + if (currentBatch && (isBatchMode || isOutlookBatchMode || isAutoMode)) { // 优先通过 WebSocket 取消 if (batchWebSocket && batchWebSocket.readyState === WebSocket.OPEN) { batchWebSocket.send(JSON.stringify({ type: 'cancel' })); @@ -853,8 +1051,10 @@ async function handleCancelTask() { await api.post(endpoint); addLog('warning', '[警告] 批量任务取消请求已提交'); toast.info('任务取消请求已提交'); - stopBatchPolling(); - resetButtons(); + if (!isAutoMode) { + stopBatchPolling(); + resetButtons(); + } } } // 单次任务取消 @@ -1005,6 +1205,12 @@ function showTaskStatus(task) { elements.taskId.textContent = task.task_uuid.substring(0, 8) + '...'; elements.taskEmail.textContent = '-'; elements.taskService.textContent = '-'; + if (elements.taskLastChecked) { + elements.taskLastChecked.textContent = '-'; + } + if (elements.taskInventory) { + elements.taskInventory.textContent = '-'; + } } // 更新任务状态 @@ -1014,7 +1220,12 @@ function updateTaskStatus(status) { running: { text: '运行中', class: 'running' }, completed: { text: '已完成', class: 'completed' }, failed: { text: '失败', class: 'failed' }, - cancelled: { text: '已取消', class: 'disabled' } + cancelled: { text: '已取消', class: 'disabled' }, + checking: { text: '检查中', class: 'running' }, + idle: { text: '空闲', class: 'completed' }, + disabled: { text: '已禁用', class: 'disabled' }, + error: { text: '异常', class: 'failed' }, + cancelling: { text: '取消中', class: 'running' }, }; const info = statusInfo[status] || { text: status, class: '' }; @@ -1026,8 +1237,8 @@ function updateTaskStatus(status) { // 显示批量状态 function showBatchStatus(batch) { elements.batchProgressSection.style.display = 'block'; - elements.taskStatusRow.style.display = 'none'; - elements.taskStatusBadge.style.display = 'none'; + elements.taskStatusRow.style.display = isAutoMode ? 'grid' : 'none'; + elements.taskStatusBadge.style.display = isAutoMode ? 'inline-flex' : 'none'; elements.batchProgressText.textContent = `0/${batch.count}`; elements.batchProgressPercent.textContent = '0%'; elements.progressBar.style.width = '0%'; @@ -1040,6 +1251,113 @@ function showBatchStatus(batch) { elements.batchFailed.dataset.last = '0'; } +function formatAutoMonitorTimestamp(value) { + if (!value) return '-'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString('zh-CN', { + hour12: false, + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); +} + +function updateAutoMonitorHeader(status, lastCheckedAt) { + if (!elements.autoMonitorStatusBadge || !elements.autoMonitorLastChecked) return; + + if (!isAutoMode) { + elements.autoMonitorStatusBadge.style.display = 'none'; + elements.autoMonitorLastChecked.style.display = 'none'; + return; + } + + const statusInfo = { + pending: { text: '自动等待', class: 'pending' }, + checking: { text: '自动检查中', class: 'running' }, + running: { text: '自动补货中', class: 'running' }, + idle: { text: '自动空闲', class: 'completed' }, + disabled: { text: '自动已禁用', class: 'disabled' }, + error: { text: '自动异常', class: 'failed' }, + cancelling: { text: '自动取消中', class: 'running' }, + }; + + const info = statusInfo[status] || { text: `自动${status || '未知'}`, class: 'pending' }; + elements.autoMonitorStatusBadge.style.display = 'inline-flex'; + elements.autoMonitorStatusBadge.textContent = info.text; + elements.autoMonitorStatusBadge.className = `status-badge ${info.class}`; + elements.autoMonitorLastChecked.style.display = 'inline'; + elements.autoMonitorLastChecked.textContent = `最近检查: ${formatAutoMonitorTimestamp(lastCheckedAt)}`; +} + +async function pollAutoRegistrationStatus() { + try { + const data = await api.get('/registration/auto-monitor'); + + elements.taskStatusRow.style.display = 'grid'; + elements.taskId.textContent = data.current_batch_id || 'auto-registration'; + elements.taskStatus.textContent = data.message || data.status || '-'; + if (elements.taskLastChecked) { + elements.taskLastChecked.textContent = formatAutoMonitorTimestamp(data.last_checked_at); + } + if (elements.taskInventory) { + const readyCount = data.current_ready_count ?? '-'; + const targetCount = data.target_ready_count ?? '-'; + elements.taskInventory.textContent = `${readyCount} / ${targetCount}`; + } + const effectiveStatus = data.batch && data.batch.cancelled && !data.batch.finished + ? 'cancelling' + : (data.status || 'pending'); + updateAutoMonitorHeader(effectiveStatus, data.last_checked_at); + updateTaskStatus(effectiveStatus); + + const logs = data.logs || []; + for (let i = autoMonitorLastLogIndex; i < logs.length; i++) { + addLog(getLogType(logs[i]), logs[i]); + } + autoMonitorLastLogIndex = logs.length; + + if (data.batch) { + currentBatch = data.batch; + activeBatchId = data.batch.batch_id; + batchCompleted = !!data.batch.finished; + elements.cancelBtn.disabled = !!data.batch.finished; + showBatchStatus({ count: data.batch.total }); + updateBatchProgress(data.batch); + if ((!batchWebSocket || batchWebSocket.readyState === WebSocket.CLOSED) && !data.batch.finished) { + connectBatchWebSocket(data.batch.batch_id); + } + } else { + currentBatch = null; + activeBatchId = null; + elements.cancelBtn.disabled = true; + elements.batchProgressSection.style.display = 'none'; + } + } catch (error) { + console.error('加载自动注册监控失败:', error); + updateAutoMonitorHeader('error', null); + elements.taskStatus.textContent = '自动注册监控获取失败'; + addLog('warning', '[警告] 自动注册监控获取失败'); + } +} + +function startAutoRegistrationMonitor() { + stopAutoRegistrationMonitor(); + pollAutoRegistrationStatus(); + autoMonitorPollingInterval = setInterval(() => { + pollAutoRegistrationStatus(); + }, 2000); +} + +function stopAutoRegistrationMonitor() { + if (autoMonitorPollingInterval) { + clearInterval(autoMonitorPollingInterval); + autoMonitorPollingInterval = null; + } +} + // 更新批量进度 function updateBatchProgress(data) { const progress = ((data.completed / data.total) * 100).toFixed(0); @@ -1266,9 +1584,12 @@ function getLogType(log) { // 重置按钮状态 function resetButtons() { elements.startBtn.disabled = false; - elements.cancelBtn.disabled = true; + elements.cancelBtn.disabled = isAutoMode; stopLogPolling(); stopBatchPolling(); + if (!isAutoMode) { + stopAutoRegistrationMonitor(); + } clearWebSocketReconnect(); clearBatchWebSocketReconnect(); currentTask = null; @@ -1284,7 +1605,9 @@ function resetButtons() { activeTaskUuid = null; activeBatchId = null; // 清除 sessionStorage 持久化状态 - sessionStorage.removeItem('activeTask'); + if (!isAutoMode) { + sessionStorage.removeItem('activeTask'); + } // 断开 WebSocket disconnectWebSocket(); disconnectBatchWebSocket(); @@ -1683,6 +2006,11 @@ function initVisibilityReconnect() { addLog('info', '[系统] 页面重新激活,正在重连批量任务监控...'); connectBatchWebSocket(activeBatchId); } + + if (isAutoMode && !autoMonitorPollingInterval) { + addLog('info', '[系统] 页面重新激活,正在恢复自动注册监控...'); + startAutoRegistrationMonitor(); + } }); } @@ -1753,6 +2081,8 @@ async function restoreActiveTask() { } catch { sessionStorage.removeItem('activeTask'); } + } else if (mode === 'auto') { + sessionStorage.removeItem('activeTask'); } } diff --git a/templates/index.html b/templates/index.html index 0cf09508..bf7a83c9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -366,6 +366,7 @@

📝 注册设置

@@ -402,6 +403,88 @@

📝 注册设置

+ +
@@ -450,13 +533,15 @@

📝 注册设置

-
-

💻 监控台

-
- - +
+

💻 监控台

+
+ + + + +
-
+
+
最近检查
+
-
+
+
+
Codex 库存
+
-
+
diff --git a/tests/test_auto_registration_merge.py b/tests/test_auto_registration_merge.py new file mode 100644 index 00000000..b13e1205 --- /dev/null +++ b/tests/test_auto_registration_merge.py @@ -0,0 +1,711 @@ +import asyncio +from contextlib import contextmanager + +from src.config.settings import Settings +from src.core import auto_registration +from src.core.auto_registration import AutoRegistrationPlan +from src.web.task_manager import task_manager +from src.web.routes import registration + + +def test_settings_exposes_auto_registration_fields(): + settings = Settings() + + assert settings.registration_auto_enabled is False + assert settings.registration_auto_check_interval == 60 + assert settings.registration_auto_email_service_type == "tempmail" + assert settings.registration_auto_mode == "pipeline" + + +def test_run_auto_registration_batch_exists(): + assert callable(registration.run_auto_registration_batch) + + +def test_run_auto_registration_batch_rejects_invalid_email_type(): + plan = AutoRegistrationPlan( + deficit=1, + ready_count=0, + min_ready_auth_files=1, + cpa_service_id=123, + ) + settings = Settings(registration_auto_email_service_type="invalid-service") + + try: + import asyncio + + asyncio.run(registration.run_auto_registration_batch(plan, settings)) + except ValueError as exc: + assert "邮箱服务类型无效" in str(exc) + else: + raise AssertionError("expected ValueError for invalid email service type") + + +def test_build_auto_registration_plan_keeps_cpa_service_id(monkeypatch): + settings = Settings( + registration_auto_enabled=True, + registration_auto_cpa_service_id=321, + registration_auto_min_ready_auth_files=3, + ) + + monkeypatch.setattr( + auto_registration, + "get_auto_registration_inventory", + lambda current_settings: (1, 3, 2), + ) + + plan = auto_registration.build_auto_registration_plan(settings) + + assert plan is not None + assert plan.cpa_service_id == 321 + assert plan.ready_count == 1 + assert plan.min_ready_auth_files == 3 + assert plan.deficit == 2 + + +def test_auto_registration_immediate_check_keeps_regular_interval(monkeypatch): + class MutableSettings: + registration_auto_enabled = False + registration_auto_check_interval = 5 + registration_auto_min_ready_auth_files = 1 + + settings = MutableSettings() + plan_calls = [] + + def fake_plan_builder(current_settings): + plan_calls.append(current_settings.registration_auto_enabled) + return AutoRegistrationPlan( + deficit=0, + ready_count=1, + min_ready_auth_files=1, + cpa_service_id=1, + ) + + async def fake_trigger_callback(plan, current_settings): + return None + + monkeypatch.setattr(auto_registration, "add_auto_registration_log", lambda message: None) + + async def scenario(): + coordinator = auto_registration.AutoRegistrationCoordinator( + trigger_callback=fake_trigger_callback, + settings_getter=lambda: settings, + plan_builder=fake_plan_builder, + ) + + coordinator.start() + try: + await asyncio.sleep(0.1) + settings.registration_auto_enabled = True + coordinator.request_immediate_check() + await asyncio.sleep(5.5) + finally: + await coordinator.stop() + + asyncio.run(scenario()) + + assert len(plan_calls) >= 2 + assert auto_registration.get_auto_registration_state()["last_checked_at"] is not None + + +def test_cancel_batch_tasks_marks_all_batch_tasks_cancelled(monkeypatch): + batch_id = "batch-auto-cancel" + task_uuids = ["task-1", "task-2"] + registration.batch_tasks[batch_id] = { + "task_uuids": task_uuids, + "cancelled": False, + "finished": False, + } + + auto_registration.update_auto_registration_state( + current_batch_id=batch_id, + status="running", + message="自动补货任务运行中", + ) + + log_messages = [] + monkeypatch.setattr(registration, "add_auto_registration_log", log_messages.append) + + for task_uuid in task_uuids: + task_manager.cleanup_task(task_uuid) + + registration._cancel_batch_tasks(batch_id) + + for task_uuid in task_uuids: + assert task_manager.is_cancelled(task_uuid) is True + task_manager.cleanup_task(task_uuid) + + state = auto_registration.get_auto_registration_state() + assert state["status"] == "cancelling" + assert batch_id in state["message"] + assert any(batch_id in message for message in log_messages) + + +def test_auto_registration_running_batch_can_be_cancelled(monkeypatch): + plan = AutoRegistrationPlan( + deficit=2, + ready_count=0, + min_ready_auth_files=2, + cpa_service_id=123, + ) + settings = Settings( + registration_auto_email_service_type="tempmail", + registration_auto_mode="pipeline", + registration_auto_interval_min=1, + registration_auto_interval_max=1, + registration_auto_concurrency=1, + ) + + created_tasks = [] + batch_started = asyncio.Event() + + @contextmanager + def fake_get_db(): + yield object() + + def fake_create_registration_task(db, task_uuid, proxy=None, email_service_id=None): + created_tasks.append(task_uuid) + return {"task_uuid": task_uuid} + + async def fake_run_batch_registration(**kwargs): + batch_id = kwargs["batch_id"] + task_uuids = kwargs["task_uuids"] + registration._init_batch_state(batch_id, task_uuids) + batch_started.set() + + while not all(task_manager.is_cancelled(task_uuid) for task_uuid in task_uuids): + await asyncio.sleep(0.05) + + registration.batch_tasks[batch_id]["cancelled"] = True + registration.batch_tasks[batch_id]["finished"] = True + + monkeypatch.setattr(registration, "get_db", fake_get_db) + monkeypatch.setattr(registration.crud, "create_registration_task", fake_create_registration_task) + monkeypatch.setattr(registration, "run_batch_registration", fake_run_batch_registration) + monkeypatch.setattr(registration, "add_auto_registration_log", lambda message: None) + + async def scenario(): + worker = asyncio.create_task(registration.run_auto_registration_batch(plan, settings)) + await asyncio.wait_for(batch_started.wait(), timeout=2) + + batch_id = auto_registration.get_auto_registration_state()["current_batch_id"] + assert batch_id + + response = await registration.cancel_batch(batch_id) + assert response["success"] is True + + finished_batch_id = await asyncio.wait_for(worker, timeout=2) + assert finished_batch_id == batch_id + + return batch_id + + batch_id = asyncio.run(scenario()) + + assert len(created_tasks) == 2 + assert batch_id in registration.batch_tasks + assert registration.batch_tasks[batch_id]["cancelled"] is True + assert registration.batch_tasks[batch_id]["finished"] is True + assert all(task_manager.is_cancelled(task_uuid) for task_uuid in created_tasks) + + state = auto_registration.get_auto_registration_state() + assert state["status"] == "cancelled" + assert batch_id in state["message"] + + for task_uuid in created_tasks: + task_manager.cleanup_task(task_uuid) + + +def test_auto_registration_batch_refreshes_monitor_state_after_completion(monkeypatch): + plan = AutoRegistrationPlan( + deficit=2, + ready_count=1, + min_ready_auth_files=3, + cpa_service_id=123, + ) + settings = Settings( + registration_auto_email_service_type="tempmail", + registration_auto_mode="pipeline", + registration_auto_min_ready_auth_files=3, + ) + + @contextmanager + def fake_get_db(): + yield object() + + def fake_create_registration_task(db, task_uuid, proxy=None, email_service_id=None): + return {"task_uuid": task_uuid} + + async def fake_run_batch_registration(**kwargs): + batch_id = kwargs["batch_id"] + registration._init_batch_state(batch_id, kwargs["task_uuids"]) + registration.batch_tasks[batch_id]["success"] = 2 + registration.batch_tasks[batch_id]["failed"] = 0 + registration.batch_tasks[batch_id]["finished"] = True + + monkeypatch.setattr(registration, "get_db", fake_get_db) + monkeypatch.setattr(registration.crud, "create_registration_task", fake_create_registration_task) + monkeypatch.setattr(registration, "run_batch_registration", fake_run_batch_registration) + monkeypatch.setattr(registration, "add_auto_registration_log", lambda message: None) + monkeypatch.setattr( + registration, + "get_auto_registration_inventory", + lambda current_settings: (3, 3, 0), + ) + + asyncio.run(registration.run_auto_registration_batch(plan, settings)) + + state = auto_registration.get_auto_registration_state() + assert state["status"] == "idle" + assert state["current_ready_count"] == 3 + assert state["target_ready_count"] == 3 + assert state["last_checked_at"] is not None + + +def test_auto_registration_batch_refresh_preserves_inventory_when_refresh_fails(monkeypatch): + plan = AutoRegistrationPlan( + deficit=1, + ready_count=1, + min_ready_auth_files=3, + cpa_service_id=123, + ) + settings = Settings( + registration_auto_email_service_type="tempmail", + registration_auto_mode="pipeline", + registration_auto_min_ready_auth_files=3, + ) + + @contextmanager + def fake_get_db(): + yield object() + + def fake_create_registration_task(db, task_uuid, proxy=None, email_service_id=None): + return {"task_uuid": task_uuid} + + async def fake_run_batch_registration(**kwargs): + batch_id = kwargs["batch_id"] + registration._init_batch_state(batch_id, kwargs["task_uuids"]) + registration.batch_tasks[batch_id]["success"] = 1 + registration.batch_tasks[batch_id]["failed"] = 0 + registration.batch_tasks[batch_id]["finished"] = True + + monkeypatch.setattr(registration, "get_db", fake_get_db) + monkeypatch.setattr(registration.crud, "create_registration_task", fake_create_registration_task) + monkeypatch.setattr(registration, "run_batch_registration", fake_run_batch_registration) + monkeypatch.setattr(registration, "add_auto_registration_log", lambda message: None) + monkeypatch.setattr(registration, "get_auto_registration_inventory", lambda current_settings: None) + + auto_registration.update_auto_registration_state(current_ready_count=2, target_ready_count=3) + + asyncio.run(registration.run_auto_registration_batch(plan, settings)) + + state = auto_registration.get_auto_registration_state() + assert state["status"] == "idle" + assert state["current_ready_count"] == 2 + assert state["target_ready_count"] == 3 + assert state["last_checked_at"] is not None + + +def test_cancelled_registration_task_persists_cancelled_status(monkeypatch): + task_uuid = "cancelled-task-uuid" + updates = [] + + @contextmanager + def fake_get_db(): + class FakeDb: + def query(self, *args, **kwargs): + class Query: + def filter(self, *filter_args, **filter_kwargs): + return self + + def order_by(self, *order_args, **order_kwargs): + return self + + def first(self): + return None + + def all(self): + return [] + + return Query() + + yield FakeDb() + + class FakeEngine: + def __init__(self, **kwargs): + self._cancel_requested = kwargs["cancel_requested"] + + def run(self): + task_manager.cancel_task(task_uuid) + assert self._cancel_requested() is True + + class Result: + success = False + error_message = "任务已取消" + + return Result() + + def fake_update_registration_task(db, current_task_uuid, **kwargs): + updates.append((current_task_uuid, kwargs)) + return {"task_uuid": current_task_uuid, **kwargs} + + monkeypatch.setattr(registration, "get_db", fake_get_db) + monkeypatch.setattr(registration.crud, "update_registration_task", fake_update_registration_task) + monkeypatch.setattr(registration, "RegistrationEngine", FakeEngine) + monkeypatch.setattr(registration, "get_settings", lambda: Settings()) + + task_manager.cleanup_task(task_uuid) + + registration._run_sync_registration_task( + task_uuid=task_uuid, + email_service_type="tempmail", + proxy=None, + email_service_config={}, + ) + + assert any(kwargs.get("status") == "running" for _, kwargs in updates) + assert any(kwargs.get("status") == "cancelled" for _, kwargs in updates) + assert not any(kwargs.get("status") == "failed" for _, kwargs in updates) + + task_manager.cleanup_task(task_uuid) + + +def test_parallel_batch_cancel_prevents_queued_tasks_from_starting(monkeypatch): + batch_id = "parallel-cancel-batch" + task_uuids = ["parallel-task-1", "parallel-task-2", "parallel-task-3"] + started = [] + statuses = {task_uuid: "pending" for task_uuid in task_uuids} + release_first = asyncio.Event() + + @contextmanager + def fake_get_db(): + class FakeTask: + def __init__(self, task_uuid): + self.status = statuses[task_uuid] + self.error_message = "" + + class FakeDb: + pass + + yield FakeDb() + + async def fake_run_registration_task(task_uuid, *args, **kwargs): + started.append(task_uuid) + statuses[task_uuid] = "running" + if task_uuid == task_uuids[0]: + await release_first.wait() + statuses[task_uuid] = "cancelled" + else: + statuses[task_uuid] = "completed" + + def fake_get_registration_task(db, task_uuid): + class FakeTask: + def __init__(self, status): + self.status = status + self.error_message = "" + + return FakeTask(statuses[task_uuid]) + + def fake_update_registration_task(db, task_uuid, **kwargs): + statuses[task_uuid] = kwargs.get("status", statuses[task_uuid]) + return None + + monkeypatch.setattr(registration, "get_db", fake_get_db) + monkeypatch.setattr(registration, "run_registration_task", fake_run_registration_task) + monkeypatch.setattr(registration.crud, "get_registration_task", fake_get_registration_task) + monkeypatch.setattr(registration.crud, "update_registration_task", fake_update_registration_task) + + async def scenario(): + worker = asyncio.create_task( + registration.run_batch_parallel( + batch_id=batch_id, + task_uuids=task_uuids, + email_service_type="tempmail", + proxy=None, + email_service_config=None, + email_service_id=None, + concurrency=1, + ) + ) + + while not started: + await asyncio.sleep(0.05) + + await registration.cancel_batch(batch_id) + release_first.set() + await asyncio.wait_for(worker, timeout=2) + + asyncio.run(scenario()) + + assert started == [task_uuids[0]] + assert statuses[task_uuids[1]] == "cancelled" + assert statuses[task_uuids[2]] == "cancelled" + + for task_uuid in task_uuids: + task_manager.cleanup_task(task_uuid) + + +def test_pipeline_wait_can_be_interrupted_by_cancel(monkeypatch): + batch_id = "pipeline-cancel-batch" + task_uuids = ["pipeline-task-1", "pipeline-task-2"] + started = [] + + async def fake_run_registration_task(task_uuid, *args, **kwargs): + started.append(task_uuid) + + monkeypatch.setattr(registration, "run_registration_task", fake_run_registration_task) + + async def scenario(): + worker = asyncio.create_task( + registration.run_batch_pipeline( + batch_id=batch_id, + task_uuids=task_uuids, + email_service_type="tempmail", + proxy=None, + email_service_config=None, + email_service_id=None, + interval_min=5, + interval_max=5, + concurrency=1, + ) + ) + + while not started: + await asyncio.sleep(0.05) + + await asyncio.sleep(0.2) + await registration.cancel_batch(batch_id) + await asyncio.wait_for(worker, timeout=2) + + asyncio.run(scenario()) + + assert started == [task_uuids[0]] + + for task_uuid in task_uuids: + task_manager.cleanup_task(task_uuid) + + +def test_pipeline_batch_updates_progress_for_completed_tasks(monkeypatch): + batch_id = "pipeline-progress-batch" + task_uuids = ["pipeline-progress-1", "pipeline-progress-2"] + statuses = {task_uuid: "pending" for task_uuid in task_uuids} + + @contextmanager + def fake_get_db(): + class FakeDb: + pass + + yield FakeDb() + + async def fake_run_registration_task(task_uuid, *args, **kwargs): + statuses[task_uuid] = "completed" + + def fake_get_registration_task(db, task_uuid): + class FakeTask: + def __init__(self, status): + self.status = status + self.error_message = "" + + return FakeTask(statuses[task_uuid]) + + monkeypatch.setattr(registration, "get_db", fake_get_db) + monkeypatch.setattr(registration, "run_registration_task", fake_run_registration_task) + monkeypatch.setattr(registration.crud, "get_registration_task", fake_get_registration_task) + + asyncio.run( + registration.run_batch_pipeline( + batch_id=batch_id, + task_uuids=task_uuids, + email_service_type="tempmail", + proxy=None, + email_service_config=None, + email_service_id=None, + interval_min=0, + interval_max=0, + concurrency=1, + ) + ) + + batch = registration.batch_tasks[batch_id] + assert batch["completed"] == 2 + assert batch["success"] == 2 + assert batch["failed"] == 0 + assert batch["finished"] is True + + for task_uuid in task_uuids: + task_manager.cleanup_task(task_uuid) + + +def test_parallel_cancelled_tasks_contribute_to_completed_progress(monkeypatch): + batch_id = "parallel-progress-cancel-batch" + task_uuids = ["parallel-progress-1", "parallel-progress-2", "parallel-progress-3"] + started = [] + statuses = {task_uuid: "pending" for task_uuid in task_uuids} + release_first = asyncio.Event() + + @contextmanager + def fake_get_db(): + class FakeDb: + pass + + yield FakeDb() + + async def fake_run_registration_task(task_uuid, *args, **kwargs): + started.append(task_uuid) + statuses[task_uuid] = "running" + await release_first.wait() + statuses[task_uuid] = "cancelled" + + def fake_get_registration_task(db, task_uuid): + class FakeTask: + def __init__(self, status): + self.status = status + self.error_message = "" + + return FakeTask(statuses[task_uuid]) + + def fake_update_registration_task(db, task_uuid, **kwargs): + statuses[task_uuid] = kwargs.get("status", statuses[task_uuid]) + return None + + monkeypatch.setattr(registration, "get_db", fake_get_db) + monkeypatch.setattr(registration, "run_registration_task", fake_run_registration_task) + monkeypatch.setattr(registration.crud, "get_registration_task", fake_get_registration_task) + monkeypatch.setattr(registration.crud, "update_registration_task", fake_update_registration_task) + + async def scenario(): + worker = asyncio.create_task( + registration.run_batch_parallel( + batch_id=batch_id, + task_uuids=task_uuids, + email_service_type="tempmail", + proxy=None, + email_service_config=None, + email_service_id=None, + concurrency=1, + ) + ) + + while not started: + await asyncio.sleep(0.05) + + await registration.cancel_batch(batch_id) + release_first.set() + await asyncio.wait_for(worker, timeout=2) + + asyncio.run(scenario()) + + batch = registration.batch_tasks[batch_id] + assert batch["completed"] == 3 + assert batch["success"] == 0 + assert batch["failed"] == 0 + assert batch["finished"] is True + + for task_uuid in task_uuids: + task_manager.cleanup_task(task_uuid) + + +def test_pipeline_cancelled_remaining_tasks_contribute_to_completed_progress(monkeypatch): + batch_id = "pipeline-progress-cancel-batch" + task_uuids = ["pipeline-progress-1", "pipeline-progress-2", "pipeline-progress-3"] + started = [] + statuses = {task_uuid: "pending" for task_uuid in task_uuids} + + @contextmanager + def fake_get_db(): + class FakeDb: + pass + + yield FakeDb() + + async def fake_run_registration_task(task_uuid, *args, **kwargs): + started.append(task_uuid) + statuses[task_uuid] = "completed" + + def fake_get_registration_task(db, task_uuid): + class FakeTask: + def __init__(self, status): + self.status = status + self.error_message = "" + + return FakeTask(statuses[task_uuid]) + + def fake_update_registration_task(db, task_uuid, **kwargs): + statuses[task_uuid] = kwargs.get("status", statuses[task_uuid]) + return None + + monkeypatch.setattr(registration, "get_db", fake_get_db) + monkeypatch.setattr(registration, "run_registration_task", fake_run_registration_task) + monkeypatch.setattr(registration.crud, "get_registration_task", fake_get_registration_task) + monkeypatch.setattr(registration.crud, "update_registration_task", fake_update_registration_task) + + async def scenario(): + worker = asyncio.create_task( + registration.run_batch_pipeline( + batch_id=batch_id, + task_uuids=task_uuids, + email_service_type="tempmail", + proxy=None, + email_service_config=None, + email_service_id=None, + interval_min=5, + interval_max=5, + concurrency=1, + ) + ) + + while not started: + await asyncio.sleep(0.05) + + await registration.cancel_batch(batch_id) + await asyncio.wait_for(worker, timeout=2) + + asyncio.run(scenario()) + + batch = registration.batch_tasks[batch_id] + assert batch["completed"] == 3 + assert batch["success"] == 1 + assert batch["failed"] == 0 + assert batch["finished"] is True + + for task_uuid in task_uuids: + task_manager.cleanup_task(task_uuid) + + +def test_mark_batch_tasks_cancelled_is_idempotent(monkeypatch): + batch_id = "idempotent-cancel-batch" + task_uuids = ["idempotent-task-1", "idempotent-task-2"] + statuses = {task_uuid: "pending" for task_uuid in task_uuids} + + @contextmanager + def fake_get_db(): + class FakeDb: + pass + + yield FakeDb() + + def fake_get_registration_task(db, task_uuid): + class FakeTask: + def __init__(self, status): + self.status = status + + return FakeTask(statuses[task_uuid]) + + def fake_update_registration_task(db, task_uuid, **kwargs): + statuses[task_uuid] = kwargs.get("status", statuses[task_uuid]) + return None + + monkeypatch.setattr(registration, "get_db", fake_get_db) + monkeypatch.setattr(registration.crud, "get_registration_task", fake_get_registration_task) + monkeypatch.setattr(registration.crud, "update_registration_task", fake_update_registration_task) + + registration._init_batch_state(batch_id, task_uuids) + + registration._mark_batch_tasks_cancelled(batch_id, task_uuids) + registration._mark_batch_tasks_cancelled(batch_id, task_uuids) + + batch = registration.batch_tasks[batch_id] + assert batch["completed"] == 2 + assert statuses[task_uuids[0]] == "cancelled" + assert statuses[task_uuids[1]] == "cancelled" + + for task_uuid in task_uuids: + task_manager.cleanup_task(task_uuid) diff --git a/tests/test_settings_registration_auto_fields.py b/tests/test_settings_registration_auto_fields.py new file mode 100644 index 00000000..cb07e7b9 --- /dev/null +++ b/tests/test_settings_registration_auto_fields.py @@ -0,0 +1,149 @@ +import asyncio +from contextlib import contextmanager +from pathlib import Path + +from src.database.models import Base, CpaService, EmailService +from src.database.session import DatabaseSessionManager +from src.web.routes import settings as settings_routes + + +class DummySettings: + registration_max_retries = 3 + registration_timeout = 120 + registration_default_password_length = 12 + registration_sleep_min = 5 + registration_sleep_max = 30 + registration_entry_flow = "abcard" + registration_auto_enabled = True + registration_auto_check_interval = 90 + registration_auto_min_ready_auth_files = 3 + registration_auto_email_service_type = "tempmail" + registration_auto_email_service_id = 7 + registration_auto_proxy = "http://127.0.0.1:7890" + registration_auto_interval_min = 8 + registration_auto_interval_max = 18 + registration_auto_concurrency = 2 + registration_auto_mode = "parallel" + registration_auto_cpa_service_id = 9 + + +def test_get_registration_settings_includes_auto_fields(monkeypatch): + monkeypatch.setattr(settings_routes, "get_settings", lambda: DummySettings()) + + result = asyncio.run(settings_routes.get_registration_settings()) + + assert result["entry_flow"] == "abcard" + assert result["auto_enabled"] is True + assert result["auto_check_interval"] == 90 + assert result["auto_min_ready_auth_files"] == 3 + assert result["auto_email_service_type"] == "tempmail" + assert result["auto_email_service_id"] == 7 + assert result["auto_proxy"] == "http://127.0.0.1:7890" + assert result["auto_interval_min"] == 8 + assert result["auto_interval_max"] == 18 + assert result["auto_concurrency"] == 2 + assert result["auto_mode"] == "parallel" + assert result["auto_cpa_service_id"] == 9 + + +def test_update_registration_settings_persists_auto_fields(monkeypatch): + runtime_dir = Path("tests_runtime") + runtime_dir.mkdir(exist_ok=True) + db_path = runtime_dir / "settings_registration_auto.db" + if db_path.exists(): + db_path.unlink() + + manager = DatabaseSessionManager(f"sqlite:///{db_path}") + Base.metadata.create_all(bind=manager.engine) + + with manager.session_scope() as session: + cpa_service = CpaService( + name="CPA Auto", + api_url="https://cpa.example.com", + api_token="token", + enabled=True, + ) + session.add(cpa_service) + email_service = EmailService( + service_type="tempmail", + name="Tempmail Auto", + config={}, + enabled=True, + priority=0, + ) + session.add(email_service) + session.flush() + cpa_service_id = cpa_service.id + email_service_id = email_service.id + + @contextmanager + def fake_get_db(): + session = manager.SessionLocal() + try: + yield session + finally: + session.close() + + update_calls = [] + state_calls = [] + trigger_calls = [] + + monkeypatch.setattr(settings_routes, "get_db", fake_get_db) + monkeypatch.setattr(settings_routes, "update_settings", lambda **kwargs: update_calls.append(kwargs)) + monkeypatch.setattr(settings_routes, "update_auto_registration_state", lambda **kwargs: state_calls.append(kwargs)) + monkeypatch.setattr(settings_routes, "trigger_auto_registration_check", lambda: trigger_calls.append(True)) + + request = settings_routes.RegistrationSettings( + max_retries=4, + timeout=180, + default_password_length=16, + sleep_min=7, + sleep_max=15, + entry_flow="abcard", + auto_enabled=True, + auto_check_interval=120, + auto_min_ready_auth_files=5, + auto_email_service_type="tempmail", + auto_email_service_id=email_service_id, + auto_proxy=" http://proxy.local:8080 ", + auto_interval_min=9, + auto_interval_max=21, + auto_concurrency=3, + auto_mode="pipeline", + auto_cpa_service_id=cpa_service_id, + ) + + result = asyncio.run(settings_routes.update_registration_settings(request)) + + assert result["success"] is True + assert len(update_calls) == 1 + payload = update_calls[0] + assert payload["registration_entry_flow"] == "abcard" + assert payload["registration_auto_enabled"] is True + assert payload["registration_auto_check_interval"] == 120 + assert payload["registration_auto_min_ready_auth_files"] == 5 + assert payload["registration_auto_email_service_type"] == "tempmail" + assert payload["registration_auto_email_service_id"] == email_service_id + assert payload["registration_auto_proxy"] == "http://proxy.local:8080" + assert payload["registration_auto_interval_min"] == 9 + assert payload["registration_auto_interval_max"] == 21 + assert payload["registration_auto_concurrency"] == 3 + assert payload["registration_auto_mode"] == "pipeline" + assert payload["registration_auto_cpa_service_id"] == cpa_service_id + assert state_calls[-1]["status"] == "checking" + assert trigger_calls == [True] + + +def test_update_registration_settings_rejects_missing_cpa_when_enabled(): + request = settings_routes.RegistrationSettings( + auto_enabled=True, + auto_cpa_service_id=0, + ) + + try: + asyncio.run(settings_routes.update_registration_settings(request)) + except settings_routes.HTTPException as exc: + assert exc.status_code == 400 + assert "必须选择一个 CPA 服务" in exc.detail + else: + raise AssertionError("expected HTTPException for missing CPA service") diff --git a/tmp_app_core.js b/tmp_app_core.js deleted file mode 100644 index bec7ac2f..00000000 --- a/tmp_app_core.js +++ /dev/null @@ -1,11 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/segment-D0siyHcc.js","assets/datadog-BTgRrIvw.js","assets/react-intl-CIPQd2Ij.js","assets/statsig-CDDMC_8Z.js"])))=>i.map(i=>d[i]); -var Xn=Object.defineProperty;var Ct=e=>{throw TypeError(e)};var Zn=(e,t,n)=>t in e?Xn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var L=(e,t,n)=>Zn(e,typeof t!="symbol"?t+"":t,n),pt=(e,t,n)=>t.has(e)||Ct("Cannot "+n);var je=(e,t,n)=>(pt(e,t,"read from private field"),n?n.call(e):t.get(e)),gt=(e,t,n)=>t.has(e)?Ct("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),At=(e,t,n,a)=>(pt(e,t,"write to private field"),a?a.call(e,n):t.set(e,n),n);import{a as st,b as He,r as g,j,s as se}from"./statsig-CDDMC_8Z.js";import{z as r}from"./zod-BeXw7Xwl.js";import{d as H,D as Qn,_ as tn}from"./datadog-BTgRrIvw.js";import{AnalyticsBrowser as er}from"./segment-D0siyHcc.js";import{r as tr,b as nr,c as rr,d as ar,e as ir,i as sr}from"./react-router-Dv7ITjcY.js";import{u as or,b as _r}from"./app-components-DjlQZIFe.js";import{d as cr,a as nn,u as ur}from"./react-intl-CIPQd2Ij.js";import{s as lr,b as Er,a as dr}from"./app-components-old-app-DTUyxsIZ.js";import{j as Sr,k as Cr,l as pr}from"./phone-number-C6DHDTQA.js";import{U as W}from"./styles-BHk20sZa.js";var rn=typeof global=="object"&&global&&global.Object===Object&&global,gr=typeof self=="object"&&self&&self.Object===Object&&self,x=rn||gr||Function("return this")(),de=x.Symbol,an=Object.prototype,Ar=an.hasOwnProperty,hr=an.toString,Ae=de?de.toStringTag:void 0;function fr(e){var t=Ar.call(e,Ae),n=e[Ae];try{e[Ae]=void 0;var a=!0}catch{}var s=hr.call(e);return a&&(t?e[Ae]=n:delete e[Ae]),s}var Tr=Object.prototype,Or=Tr.toString;function mr(e){return Or.call(e)}var Ir="[object Null]",Nr="[object Undefined]",ht=de?de.toStringTag:void 0;function te(e){return e==null?e===void 0?Nr:Ir:ht&&ht in Object(e)?fr(e):mr(e)}function Q(e){return e!=null&&typeof e=="object"}var me=Array.isArray;function sn(e){var t=typeof e;return e!=null&&(t=="object"||t=="function")}var Pr="[object AsyncFunction]",Lr="[object Function]",br="[object GeneratorFunction]",Rr="[object Proxy]";function on(e){if(!sn(e))return!1;var t=te(e);return t==Lr||t==br||t==Pr||t==Rr}var Ve=x["__core-js_shared__"],ft=function(){var e=/[^.]+$/.exec(Ve&&Ve.keys&&Ve.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""}();function yr(e){return!!ft&&ft in e}var vr=Function.prototype,wr=vr.toString;function ce(e){if(e!=null){try{return wr.call(e)}catch{}try{return e+""}catch{}}return""}var Ur=/[\\^$.*+?()[\]{}|]/g,Dr=/^\[object .+?Constructor\]$/,Wr=Function.prototype,Fr=Object.prototype,Mr=Wr.toString,Gr=Fr.hasOwnProperty,Yr=RegExp("^"+Mr.call(Gr).replace(Ur,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function kr(e){if(!sn(e)||yr(e))return!1;var t=on(e)?Yr:Dr;return t.test(ce(e))}function jr(e,t){return e==null?void 0:e[t]}function Ce(e,t){var n=jr(e,t);return kr(n)?n:void 0}var Qe=Ce(x,"WeakMap");function et(){}var Hr=9007199254740991,Vr=/^(?:0|[1-9]\d*)$/;function xr(e,t){var n=typeof e;return t=t??Hr,!!t&&(n=="number"||n!="symbol"&&Vr.test(e))&&e>-1&&e%1==0&&e-1&&e%1==0&&e<=Br}function Kr(e){return e!=null&&cn(e.length)&&!on(e)}var $r=Object.prototype;function qr(e){var t=e&&e.constructor,n=typeof t=="function"&&t.prototype||$r;return e===n}function zr(e,t){for(var n=-1,a=Array(e);++n-1}function ti(e,t){var n=this.__data__,a=We(n,e);return a<0?(++this.size,n.push([e,t])):n[a][1]=t,this}function B(e){var t=-1,n=e==null?0:e.length;for(this.clear();++tc))return!1;var l=i.get(e),C=i.get(t);if(l&&C)return l==t&&C==e;var p=-1,E=!0,N=n&ji?new De:void 0;for(i.set(e,t),i.set(t,e);++p{setTimeout(()=>{s(new An)},t)});return Promise.race([e,n])}function hn(){return typeof document>"u"}function Pc(e){if(!("data"in e))return;const t=e.data;typeof t=="string"&&t.trim().match(/\D/)&&e.preventDefault()}function Lc(e,t=()=>new Error(`Invalid value: ${e}`)){throw t()}async function fn(e){const t=typeof e=="function"?e:()=>e,n=performance.now(),a=await t(),i=performance.now()-n;return{result:a,durationMs:i}}var Ee;class ps{constructor(){gt(this,Ee,!1)}initialize(){it()||je(this,Ee)||(H.init({applicationId:"a10b421a-4c98-4e3a-9b20-89ebbf54befb",clientToken:"pub87c677d9c9bf34b387f60a689c1990a6",site:"datadoghq.com",service:"login-web",env:"prod",version:"f305bca6afc89159e811fd824396d6daaf880760",sessionSampleRate:100,sessionReplaySampleRate:1,trackUserInteractions:!0,trackResources:!0,trackLongTasks:!0,allowedTracingUrls:[location.origin],defaultPrivacyLevel:Qn.MASK_USER_INPUT,trackViewsManually:!0,trackFeatureFlagsForEvents:["vital","action","long_task","resource"],beforeSend:t=>!(t.type==="error"&&Ts(t.error))}),At(this,Ee,!0))}setContext({clientId:t,appNameEnum:n,sessionLoggingId:a,track:s,deviceId:i}){this.ensureInitializationBefore(()=>{H.setGlobalContext({clientId:t,appNameEnum:n,sessionLoggingId:a,track:s,deviceId:i,usesCdn:"f305bca6afc89159e811fd824396d6daaf880760".endsWith("-with_cdn")})})}addAction(t,n){this.ensureInitializationBefore(()=>{H.addAction(t,n)})}addError(t,n){this.ensureInitializationBefore(()=>{H.addError(t,n)})}addTiming(t,n){this.ensureInitializationBefore(()=>{H.addTiming(t,n)})}addPageView({name:t,routeId:n,isError:a}){this.ensureInitializationBefore(()=>{H.startView({name:t,context:{routeId:n,isError:a}})})}addFeatureFlagEvaluation(t,n){this.ensureInitializationBefore(()=>{H.addFeatureFlagEvaluation(t,n)})}startDurationVital(t,n){this.ensureInitializationBefore(()=>{H.startDurationVital(t,n)})}stopDurationVital(t,n){this.ensureInitializationBefore(()=>{H.stopDurationVital(t,n)})}async trackAsDurationVital(t,n,a){this.startDurationVital(t,a);try{return await n()}finally{this.stopDurationVital(t)}}ensureInitializationBefore(t){it()||(je(this,Ee)||this.initialize(),t())}}Ee=new WeakMap;const m=new ps,gs=["Fetch is aborted","Load failed","ResizeObserver loop completed with undelivered notifications.","Object Not Found Matching Id","Object.hasOwn is not a function","csp_violation: 'eval' blocked","Web translate error: received error callback from translatePage"],As=["font-src"],Ke=[],hs=100,fs=500,Ts=e=>{var s;if(Ke.length>=fs||(s=e.stack)!=null&&s.includes("-extension://")||gs.some(i=>{var o;return(o=e.message)==null?void 0:o.includes(i)})||As.some(i=>{var o;return(o=e.type)==null?void 0:o.includes(i)}))return!0;const t=Date.now();Ke.push(t);const n=t-1e3*60;return Ke.filter(i=>i=hs};let ve;const Os=new Uint8Array(16);function ms(){if(!ve&&(ve=typeof crypto<"u"&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto),!ve))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return ve(Os)}const R=[];for(let e=0;e<256;++e)R.push((e+256).toString(16).slice(1));function Is(e,t=0){return R[e[t+0]]+R[e[t+1]]+R[e[t+2]]+R[e[t+3]]+"-"+R[e[t+4]]+R[e[t+5]]+"-"+R[e[t+6]]+R[e[t+7]]+"-"+R[e[t+8]]+R[e[t+9]]+"-"+R[e[t+10]]+R[e[t+11]]+R[e[t+12]]+R[e[t+13]]+R[e[t+14]]+R[e[t+15]]}const Ns=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),Gt={randomUUID:Ns};function Ps(e,t,n){if(Gt.randomUUID&&!t&&!e)return Gt.randomUUID();e=e||{};const a=e.random||(e.rng||ms)();return a[6]=a[6]&15|64,a[8]=a[8]&63|128,Is(a)}function bc(e){return new URLSearchParams(window.location.search).get(e)}function Ls(e){const t=document.cookie.split(";");for(let n=0;nst.StatsigClient.instance(Tn);async function Rc(e=1e3){try{const t=J();await Promise.race([t.flush(),new Promise(n=>setTimeout(n,e))])}catch{}}async function yc({clientId:e,appNameEnum:t,originator:n,sessionLoggingId:a,statsigBootstrap:s}){if(new st.StatsigClient(Tn,{locale:navigator.language,appVersion:"f305bca6afc89159e811fd824396d6daaf880760",userAgent:navigator.userAgent,customIDs:{[Rs]:Oe,[ys]:Oe,stableID:Oe},custom:{client_id:e,app_name_enum:t,originator:n,[vs]:a,client_type:"web"}},{environment:{tier:ws()},plugins:[new He.StatsigAutoCapturePlugin({eventFilterFunc:i=>i.eventName===He.AutoCaptureEventName.PERFORMANCE||i.eventName===He.AutoCaptureEventName.WEB_VITALS})]}),J().on("gate_evaluation",({gate:i})=>{$e("gate",i.name,i.value)}),J().on("experiment_evaluation",({experiment:i})=>{$e("experiment",i.name,i.value)}),J().on("layer_evaluation",({layer:i})=>{i.__value&&$e("layer",i.name,i.__value)}),s){J().dataAdapter.setData(s),J().initializeSync();return}await J().initializeAsync()}function T(e){J().logEvent(e.eventName,e.value,e.metadata)}function $e(e,t,n){if(n&&pi(n))for(const[a,s]of Object.entries(n))m.addFeatureFlagEvaluation(`${e}__${t}__${a}`,s);else m.addFeatureFlagEvaluation(`${e}__${t}`,n)}const _t=g.createContext({getHasAnyErrorView:()=>!1,registerErrorView:et,unregisterErrorView:et});function vc(){const{getHasAnyErrorView:e}=g.useContext(_t);return e}function wc(){const e=g.useId(),{registerErrorView:t,unregisterErrorView:n}=g.useContext(_t),[a]=g.useState(()=>ot(t));a(e),g.useEffect(()=>()=>{n(e)},[e,n])}const Uc=({children:e})=>{const t=g.useRef(new Set),n=g.useCallback(i=>{t.current.add(i)},[]),a=g.useCallback(i=>{t.current.delete(i)},[]),s=g.useCallback(()=>t.current.size>0,[]);return j.jsx(_t.Provider,{value:{getHasAnyErrorView:s,registerErrorView:n,unregisterErrorView:a},children:e})},Us=r.enum(["api","apple","chat","sora","sora2","juno","tailoros","jam","teammate","sky"]),On=g.createContext(null),Dc=({appNameEnum:e,children:t})=>{const n=Us.safeParse(e);return j.jsx(On.Provider,{value:n.success?n.data:null,children:t})};function Wc(){return g.useContext(On)}let mn=()=>{throw new Error("getUserScopedGlobals not yet registered")};const Fc=e=>{mn=e},k=()=>mn();class Mc{constructor(){L(this,"latestClientAuthSessionCacheEntry",null)}read(){return this.latestClientAuthSessionCacheEntry}readByChecksum(t){var n;return((n=this.latestClientAuthSessionCacheEntry)==null?void 0:n.checksum)===t?this.latestClientAuthSessionCacheEntry:null}write(t){return this.latestClientAuthSessionCacheEntry=t,t}writeFromDumpResponse(t){return this.write({checksum:t.checksum,sessionId:t.session_id,session:t.client_auth_session})}clear(){this.latestClientAuthSessionCacheEntry=null}}class he extends Error{}he.prototype.name="InvalidTokenError";function Ds(e){return decodeURIComponent(atob(e).replace(/(.)/g,(t,n)=>{let a=n.charCodeAt(0).toString(16).toUpperCase();return a.length<2&&(a="0"+a),"%"+a}))}function Ws(e){let t=e.replace(/-/g,"+").replace(/_/g,"/");switch(t.length%4){case 0:break;case 2:t+="==";break;case 3:t+="=";break;default:throw new Error("base64 string is not of the correct length")}try{return Ds(t)}catch{return atob(t)}}function In(e,t){if(typeof e!="string")throw new he("Invalid token specified: must be a string");t||(t={});const n=t.header===!0?0:1,a=e.split(".")[n];if(typeof a!="string")throw new he(`Invalid token specified: missing part #${n+1}`);let s;try{s=Ws(a)}catch(i){throw new he(`Invalid token specified: invalid base64 for part #${n+1} (${i.message})`)}try{return JSON.parse(s)}catch(i){throw new he(`Invalid token specified: invalid json for part #${n+1} (${i.message})`)}}const Fs=Cr().map(e=>r.literal(e)),Ms=r.union(Fs),Y=r.enum(["totp","recovery_code","email","sms","push_auth","passkey"]),Nn=r.discriminatedUnion("factor_type",[r.object({id:r.string(),factor_type:r.literal(Y.enum.recovery_code)}),r.object({id:r.string(),factor_type:r.literal(Y.enum.totp)}),r.object({id:r.string(),factor_type:r.literal(Y.enum.email),metadata:r.object({email:r.string()})}),r.object({id:r.string(),factor_type:r.literal(Y.enum.sms),metadata:r.object({phone_number:r.string(),verified_channels:r.array(r.string())})}),r.object({id:r.string(),factor_type:r.literal(Y.enum.push_auth),metadata:r.object({mfa_request_id:r.string().optional().nullable()}).nullable()}),r.object({id:r.string(),factor_type:r.literal(Y.enum.passkey)})]);r.object({factor_id:r.string(),factors:r.array(Nn),errors:r.object({formErrors:r.optional(r.array(r.string())),fieldErrors:r.optional(r.object({code:r.optional(r.array(r.string()))}))}).optional()});const Pn=r.discriminatedUnion("factor_type",[r.object({id:r.string(),factor_type:r.literal(Y.enum.recovery_code),metadata:r.object({secret:r.string()})}),r.object({id:r.string(),factor_type:r.literal(Y.enum.totp),metadata:r.object({secret:r.string(),qr_code_secret_url:r.string()})})]);r.object({factor_id:r.string(),factors:r.array(Pn),errors:r.object({formErrors:r.optional(r.array(r.string())),fieldErrors:r.optional(r.object({code:r.optional(r.array(r.string()))}))}).optional()});const Gs=r.enum(["whatsapp"]),ee=r.enum(["email","phone_number"]),Gc=ee.enum.email,$=r.object({value:r.string(),kind:ee}),Yc=e=>e===ee.enum.email||e===ee.enum.phone_number;var F=(e=>(e.ACCESS_FLOW_USER_ACTION_TYPE_UNSPECIFIED="ACCESS_FLOW_USER_ACTION_TYPE_UNSPECIFIED",e.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE="ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE",e.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_GOOGLE="ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_GOOGLE",e.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_MICROSOFT="ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_MICROSOFT",e.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_APPLE="ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_APPLE",e.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_SSO="ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_SSO",e.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_PASSWORD="ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_PASSWORD",e.ACCESS_FLOW_USER_ACTION_TYPE_PREVIOUS_STEP="ACCESS_FLOW_USER_ACTION_TYPE_PREVIOUS_STEP",e.ACCESS_FLOW_USER_ACTION_TYPE_EDIT_USERNAME="ACCESS_FLOW_USER_ACTION_TYPE_EDIT_USERNAME",e.ACCESS_FLOW_USER_ACTION_TYPE_EXIT="ACCESS_FLOW_USER_ACTION_TYPE_EXIT",e.ACCESS_FLOW_USER_ACTION_TYPE_EXIT_STAY_LOGGED_OUT_LINK_CLICKED="ACCESS_FLOW_USER_ACTION_TYPE_EXIT_STAY_LOGGED_OUT_LINK_CLICKED",e.ACCESS_FLOW_USER_ACTION_TYPE_EXIT_CLOSE_BUTTON_CLICKED="ACCESS_FLOW_USER_ACTION_TYPE_EXIT_CLOSE_BUTTON_CLICKED",e.ACCESS_FLOW_USER_ACTION_TYPE_RESEND="ACCESS_FLOW_USER_ACTION_TYPE_RESEND",e.ACCESS_FLOW_USER_ACTION_TYPE_INPUT="ACCESS_FLOW_USER_ACTION_TYPE_INPUT",e.ACCESS_FLOW_USER_ACTION_TYPE_AUTOFILL="ACCESS_FLOW_USER_ACTION_TYPE_AUTOFILL",e.ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_EMAIL="ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_EMAIL",e.ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_PHONE="ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_PHONE",e.ACCESS_FLOW_USER_ACTION_TYPE_FORGOT_PASSWORD="ACCESS_FLOW_USER_ACTION_TYPE_FORGOT_PASSWORD",e.ACCESS_FLOW_USER_ACTION_TYPE_CONSUME_OTP_DEEPLINK="ACCESS_FLOW_USER_ACTION_TYPE_CONSUME_OTP_DEEPLINK",e.ACCESS_FLOW_USER_ACTION_TYPE_CONSUME_MFA_DEEPLINK="ACCESS_FLOW_USER_ACTION_TYPE_CONSUME_MFA_DEEPLINK",e.ACCESS_FLOW_USER_ACTION_TYPE_COPY_RECOVERY_CODE="ACCESS_FLOW_USER_ACTION_TYPE_COPY_RECOVERY_CODE",e.ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_TOTP_CODE="ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_TOTP_CODE",e.ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_SMS_CODE="ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_SMS_CODE",e.ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_EMAIL_CODE="ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_EMAIL_CODE",e.ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_RECOVERY_CODE="ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_RECOVERY_CODE",e.ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_PUSH_AUTH_CODE="ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_PUSH_AUTH_CODE",e.ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_UNKNOWN_FACTOR="ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_UNKNOWN_FACTOR",e.ACCESS_FLOW_USER_ACTION_TYPE_LOGIN_INSTEAD_LINK_CLICKED="ACCESS_FLOW_USER_ACTION_TYPE_LOGIN_INSTEAD_LINK_CLICKED",e.ACCESS_FLOW_USER_ACTION_TYPE_SIGNUP_INSTEAD_LINK_CLICKED="ACCESS_FLOW_USER_ACTION_TYPE_SIGNUP_INSTEAD_LINK_CLICKED",e.ACCESS_FLOW_USER_ACTION_TYPE_TERMS_OF_USE_CLICK="ACCESS_FLOW_USER_ACTION_TYPE_TERMS_OF_USE_CLICK",e.ACCESS_FLOW_USER_ACTION_TYPE_PRIVACY_POLICY_CLICK="ACCESS_FLOW_USER_ACTION_TYPE_PRIVACY_POLICY_CLICK",e.ACCESS_FLOW_USER_ACTION_TYPE_WORDMARK_CLICKED="ACCESS_FLOW_USER_ACTION_TYPE_WORDMARK_CLICKED",e.ACCESS_FLOW_USER_ACTION_TYPE_SEND_EMAIL_OTP="ACCESS_FLOW_USER_ACTION_TYPE_SEND_EMAIL_OTP",e.ACCESS_FLOW_USER_ACTION_TYPE_CANCEL_WEB_FLOW="ACCESS_FLOW_USER_ACTION_TYPE_CANCEL_WEB_FLOW",e.ACCESS_FLOW_USER_ACTION_TYPE_LOGIN_WITH_ONE_TIME_CODE="ACCESS_FLOW_USER_ACTION_TYPE_LOGIN_WITH_ONE_TIME_CODE",e.ACCESS_FLOW_USER_ACTION_TYPE_SIGNUP_WITH_ONE_TIME_CODE="ACCESS_FLOW_USER_ACTION_TYPE_SIGNUP_WITH_ONE_TIME_CODE",e.ACCESS_FLOW_USER_ACTION_TYPE_BACK_TO_LOGIN="ACCESS_FLOW_USER_ACTION_TYPE_BACK_TO_LOGIN",e.ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_PASSKEY="ACCESS_FLOW_USER_ACTION_TYPE_MFA_SELECT_PASSKEY",e.ACCESS_FLOW_USER_ACTION_TYPE_ACQUIRE_AUTOFILL_CREDENTIALS="ACCESS_FLOW_USER_ACTION_TYPE_ACQUIRE_AUTOFILL_CREDENTIALS",e.ACCESS_FLOW_USER_ACTION_TYPE_STORE_AUTOFILL_CREDENTIALS="ACCESS_FLOW_USER_ACTION_TYPE_STORE_AUTOFILL_CREDENTIALS",e.ACCESS_FLOW_USER_ACTION_TYPE_NATIVE_FOREGROUND="ACCESS_FLOW_USER_ACTION_TYPE_NATIVE_FOREGROUND",e.ACCESS_FLOW_USER_ACTION_TYPE_NATIVE_BACKGROUND="ACCESS_FLOW_USER_ACTION_TYPE_NATIVE_BACKGROUND",e.ACCESS_FLOW_USER_ACTION_TYPE_REMOVE_SAVED_ACCOUNT_CLICKED="ACCESS_FLOW_USER_ACTION_TYPE_REMOVE_SAVED_ACCOUNT_CLICKED",e.ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_GENERIC_LOGIN_CLICKED="ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_GENERIC_LOGIN_CLICKED",e.ACCESS_FLOW_USER_ACTION_TYPE_TRY_ANOTHER_WAY_FROM_PASSKEY="ACCESS_FLOW_USER_ACTION_TYPE_TRY_ANOTHER_WAY_FROM_PASSKEY",e.ACCESS_FLOW_USER_ACTION_TYPE_EXIT_ESCAPE_KEY_PRESSED="ACCESS_FLOW_USER_ACTION_TYPE_EXIT_ESCAPE_KEY_PRESSED",e.ACCESS_FLOW_USER_ACTION_TYPE_EXIT_CLOSE_LINK_CLICKED="ACCESS_FLOW_USER_ACTION_TYPE_EXIT_CLOSE_LINK_CLICKED",e.ACCESS_FLOW_USER_ACTION_TYPE_EXIT_BACKDROP_CLICKED="ACCESS_FLOW_USER_ACTION_TYPE_EXIT_BACKDROP_CLICKED",e.ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_AGE="ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_AGE",e.ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_DATE_OF_BIRTH="ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_DATE_OF_BIRTH",e.ACCESS_FLOW_USER_ACTION_TYPE_SHOW_AGE_FALLBACK_CONFIRMATION="ACCESS_FLOW_USER_ACTION_TYPE_SHOW_AGE_FALLBACK_CONFIRMATION",e.ACCESS_FLOW_USER_ACTION_TYPE_CONFIRM_AGE_FALLBACK="ACCESS_FLOW_USER_ACTION_TYPE_CONFIRM_AGE_FALLBACK",e.ACCESS_FLOW_USER_ACTION_TYPE_CANCEL_AGE_FALLBACK_CONFIRMATION="ACCESS_FLOW_USER_ACTION_TYPE_CANCEL_AGE_FALLBACK_CONFIRMATION",e.UNRECOGNIZED="UNRECOGNIZED",e))(F||{}),fe=(e=>(e.ACCESS_FLOW_FIELD_UNSPECIFIED="ACCESS_FLOW_FIELD_UNSPECIFIED",e.ACCESS_FLOW_FIELD_EMAIL="ACCESS_FLOW_FIELD_EMAIL",e.ACCESS_FLOW_FIELD_PHONE="ACCESS_FLOW_FIELD_PHONE",e.ACCESS_FLOW_FIELD_COUNTRY_CODE="ACCESS_FLOW_FIELD_COUNTRY_CODE",e.ACCESS_FLOW_FIELD_PASSWORD="ACCESS_FLOW_FIELD_PASSWORD",e.ACCESS_FLOW_FIELD_OTP="ACCESS_FLOW_FIELD_OTP",e.ACCESS_FLOW_FIELD_NAME="ACCESS_FLOW_FIELD_NAME",e.ACCESS_FLOW_FIELD_BIRTHDAY="ACCESS_FLOW_FIELD_BIRTHDAY",e.ACCESS_FLOW_FIELD_CONFIRM_PASSWORD="ACCESS_FLOW_FIELD_CONFIRM_PASSWORD",e.UNRECOGNIZED="UNRECOGNIZED",e))(fe||{}),A=(e=>(e.ACCESS_FLOW_PAGE_TYPE_UNSPECIFIED="ACCESS_FLOW_PAGE_TYPE_UNSPECIFIED",e.ACCESS_FLOW_PAGE_TYPE_ABOUT_YOU="ACCESS_FLOW_PAGE_TYPE_ABOUT_YOU",e.ACCESS_FLOW_PAGE_TYPE_ADD_EMAIL="ACCESS_FLOW_PAGE_TYPE_ADD_EMAIL",e.ACCESS_FLOW_PAGE_TYPE_APPLE_INTELLIGENCE_START="ACCESS_FLOW_PAGE_TYPE_APPLE_INTELLIGENCE_START",e.ACCESS_FLOW_PAGE_TYPE_CONTACT_VERIFICATION="ACCESS_FLOW_PAGE_TYPE_CONTACT_VERIFICATION",e.ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_LATER="ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_LATER",e.ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_LATER_QUEUED="ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_LATER_QUEUED",e.ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_PASSWORD="ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_PASSWORD",e.ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_START="ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_START",e.ACCESS_FLOW_PAGE_TYPE_EMAIL_OTP_SEND="ACCESS_FLOW_PAGE_TYPE_EMAIL_OTP_SEND",e.ACCESS_FLOW_PAGE_TYPE_EMAIL_OTP_VERIFICATION="ACCESS_FLOW_PAGE_TYPE_EMAIL_OTP_VERIFICATION",e.ACCESS_FLOW_PAGE_TYPE_ERROR="ACCESS_FLOW_PAGE_TYPE_ERROR",e.ACCESS_FLOW_PAGE_TYPE_EXTERNAL_URL="ACCESS_FLOW_PAGE_TYPE_EXTERNAL_URL",e.ACCESS_FLOW_PAGE_TYPE_IDENTITY_VERIFICATION="ACCESS_FLOW_PAGE_TYPE_IDENTITY_VERIFICATION",e.ACCESS_FLOW_PAGE_TYPE_CHATGPT_WEB_LOGIN_MODAL="ACCESS_FLOW_PAGE_TYPE_CHATGPT_WEB_LOGIN_MODAL",e.ACCESS_FLOW_PAGE_TYPE_CHATGPT_WEB_LOG_BACK_IN_MODAL="ACCESS_FLOW_PAGE_TYPE_CHATGPT_WEB_LOG_BACK_IN_MODAL",e.ACCESS_FLOW_PAGE_TYPE_LOGIN_OR_SIGNUP_START="ACCESS_FLOW_PAGE_TYPE_LOGIN_OR_SIGNUP_START",e.ACCESS_FLOW_PAGE_TYPE_LOGIN_PASSWORD="ACCESS_FLOW_PAGE_TYPE_LOGIN_PASSWORD",e.ACCESS_FLOW_PAGE_TYPE_LOGIN_START="ACCESS_FLOW_PAGE_TYPE_LOGIN_START",e.ACCESS_FLOW_PAGE_TYPE_MFA_CHALLENGE="ACCESS_FLOW_PAGE_TYPE_MFA_CHALLENGE",e.ACCESS_FLOW_PAGE_TYPE_MFA_CHALLENGE_SELECTION="ACCESS_FLOW_PAGE_TYPE_MFA_CHALLENGE_SELECTION",e.ACCESS_FLOW_PAGE_TYPE_MFA_ENROLL="ACCESS_FLOW_PAGE_TYPE_MFA_ENROLL",e.ACCESS_FLOW_PAGE_TYPE_PHONE_OTP_SEND="ACCESS_FLOW_PAGE_TYPE_PHONE_OTP_SEND",e.ACCESS_FLOW_PAGE_TYPE_PUSH_AUTH_VERIFICATION="ACCESS_FLOW_PAGE_TYPE_PUSH_AUTH_VERIFICATION",e.ACCESS_FLOW_PAGE_TYPE_RESET_PASSWORD_START="ACCESS_FLOW_PAGE_TYPE_RESET_PASSWORD_START",e.ACCESS_FLOW_PAGE_TYPE_RESET_PASSWORD_NEW_PASSWORD="ACCESS_FLOW_PAGE_TYPE_RESET_PASSWORD_NEW_PASSWORD",e.ACCESS_FLOW_PAGE_TYPE_RESET_PASSWORD_SUCCESS="ACCESS_FLOW_PAGE_TYPE_RESET_PASSWORD_SUCCESS",e.ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_CONSENT="ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_CONSENT",e.ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_ORG="ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_ORG",e.ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_CODEX_CONSENT="ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_CODEX_CONSENT",e.ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_CODEX_ORG="ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_CODEX_ORG",e.ACCESS_FLOW_PAGE_TYPE_SSO="ACCESS_FLOW_PAGE_TYPE_SSO",e.ACCESS_FLOW_PAGE_TYPE_TOKEN_EXCHANGE="ACCESS_FLOW_PAGE_TYPE_TOKEN_EXCHANGE",e.ACCESS_FLOW_PAGE_TYPE_WORKSPACE="ACCESS_FLOW_PAGE_TYPE_WORKSPACE",e.ACCESS_FLOW_PAGE_TYPE_END="ACCESS_FLOW_PAGE_TYPE_END",e.ACCESS_FLOW_PAGE_TYPE_MOBILE_LEGACY_FINISH_ONBOARDING="ACCESS_FLOW_PAGE_TYPE_MOBILE_LEGACY_FINISH_ONBOARDING",e.ACCESS_FLOW_PAGE_TYPE_CHATGPT_WEB_LOGIN_PAGE="ACCESS_FLOW_PAGE_TYPE_CHATGPT_WEB_LOGIN_PAGE",e.ACCESS_FLOW_PAGE_TYPE_LOGIN_PASSKEY="ACCESS_FLOW_PAGE_TYPE_LOGIN_PASSKEY",e.ACCESS_FLOW_PAGE_TYPE_EMAIL_ROLLBACK_CONFIRM="ACCESS_FLOW_PAGE_TYPE_EMAIL_ROLLBACK_CONFIRM",e.UNRECOGNIZED="UNRECOGNIZED",e))(A||{}),Ln=(e=>(e.ACCESS_FLOW_VALIDATION_TYPE_UNSPECIFIED="ACCESS_FLOW_VALIDATION_TYPE_UNSPECIFIED",e.ACCESS_FLOW_VALIDATION_TYPE_BACKEND_API="ACCESS_FLOW_VALIDATION_TYPE_BACKEND_API",e.ACCESS_FLOW_VALIDATION_TYPE_CLIENT="ACCESS_FLOW_VALIDATION_TYPE_CLIENT",e.UNRECOGNIZED="UNRECOGNIZED",e))(Ln||{}),Pe=(e=>(e.ACCESS_FLOW_API_STATUS_UNSPECIFIED="ACCESS_FLOW_API_STATUS_UNSPECIFIED",e.ACCESS_FLOW_API_STATUS_SUCCESS="ACCESS_FLOW_API_STATUS_SUCCESS",e.ACCESS_FLOW_API_STATUS_FAILURE="ACCESS_FLOW_API_STATUS_FAILURE",e.UNRECOGNIZED="UNRECOGNIZED",e))(Pe||{});const q={$type:"protobuf_analytics_events.v1.AccessFlowUserAction"},Ys={$type:"protobuf_analytics_events.v1.AccessFlowPageLoad"},ks={$type:"protobuf_analytics_events.v1.AccessFlowValidationError"},bn={$type:"protobuf_analytics_events.v1.AccessFlowApiInvocation"};/*! - * cookie - * Copyright(c) 2012-2014 Roman Shtylman - * Copyright(c) 2015 Douglas Christopher Wilson - * MIT Licensed - */var Rn=js;function js(e,t){if(typeof e!="string")throw new TypeError("argument str must be a string");var n={},a=e.length;if(a<2)return n;var s=t&&t.decode||Hs,i=0,o=0,c=0;do{if(o=e.indexOf("=",i),o===-1)break;if(c=e.indexOf(";",i),c===-1)c=a;else if(o>c){i=e.lastIndexOf(";",o-1)+1;continue}var u=Yt(e,i,o),l=kt(e,o,u),C=e.slice(u,l);if(!n.hasOwnProperty(C)){var p=Yt(e,o+1,c),E=kt(e,c,p);e.charCodeAt(p)===34&&e.charCodeAt(E-1)===34&&(p++,E--);var N=e.slice(p,E);n[C]=Vs(N,s)}i=c+1}while(in;){var a=e.charCodeAt(--t);if(a!==32&&a!==9)return t+1}return n}function Hs(e){return e.indexOf("%")!==-1?decodeURIComponent(e):e}function Vs(e,t){try{return t(e)}catch{return e}}const xs=Object.prototype.toString,Bs=e=>xs.call(e)==="[object Error]",Ks=new Set(["network error","Failed to fetch","NetworkError when attempting to fetch resource.","The Internet connection appears to be offline.","Network request failed","fetch failed","terminated"," A network error occurred.","Network connection lost"]);function $s(e){if(!(e&&Bs(e)&&e.name==="TypeError"&&typeof e.message=="string"))return!1;const{message:n,stack:a}=e;return n==="Load failed"?a===void 0||"__sentry_captured__"in e:n.startsWith("error sending request for url")?!0:Ks.has(n)}function qs(e){if(typeof e=="number"){if(e<0)throw new TypeError("Expected `retries` to be a non-negative number.");if(Number.isNaN(e))throw new TypeError("Expected `retries` to be a valid number or Infinity, got NaN.")}else if(e!==void 0)throw new TypeError("Expected `retries` to be a number or Infinity.")}function we(e,t,{min:n=0,allowInfinity:a=!1}={}){if(t!==void 0){if(typeof t!="number"||Number.isNaN(t))throw new TypeError(`Expected \`${e}\` to be a number${a?" or Infinity":""}.`);if(!a&&!Number.isFinite(t))throw new TypeError(`Expected \`${e}\` to be a finite number.`);if(t{const a=n.retries-(t-1);return Object.freeze({error:e,attemptNumber:t,retriesLeft:a})};function Xs(e,t){const n=t.randomize?Math.random()+1:1;let a=Math.round(n*Math.max(t.minTimeout,1)*t.factor**(e-1));return a=Math.min(a,t.maxTimeout),a}async function Zs(e,t,n,a,s){var p;let i=e;if(i instanceof Error||(i=new TypeError(`Non-error was thrown: "${i}". You should only throw errors.`)),i instanceof zs)throw i.originalError;if(i instanceof TypeError&&!$s(i))throw i;const o=Js(i,t,n);await n.onFailedAttempt(o);const c=Date.now();if(c-a>=s||t>=n.retries+1||!await n.shouldRetry(o))throw i;const u=Xs(t,n),l=s-(c-a);if(l<=0)throw i;const C=Math.min(u,l);C>0&&await new Promise((E,N)=>{var b,D;const f=()=>{var v;clearTimeout(P),(v=n.signal)==null||v.removeEventListener("abort",f),N(n.signal.reason)},P=setTimeout(()=>{var v;(v=n.signal)==null||v.removeEventListener("abort",f),E()},C);n.unref&&((b=P.unref)==null||b.call(P)),(D=n.signal)==null||D.addEventListener("abort",f,{once:!0})}),(p=n.signal)==null||p.throwIfAborted()}async function Qs(e,t={}){var o,c,u;if(t={...t},qs(t.retries),Object.hasOwn(t,"forever"))throw new Error("The `forever` option is no longer supported. For many use-cases, you can set `retries: Infinity` instead.");t.retries??(t.retries=10),t.factor??(t.factor=2),t.minTimeout??(t.minTimeout=1e3),t.maxTimeout??(t.maxTimeout=Number.POSITIVE_INFINITY),t.randomize??(t.randomize=!1),t.onFailedAttempt??(t.onFailedAttempt=()=>{}),t.shouldRetry??(t.shouldRetry=()=>!0),we("factor",t.factor,{min:0,allowInfinity:!1}),we("minTimeout",t.minTimeout,{min:0,allowInfinity:!1}),we("maxTimeout",t.maxTimeout,{min:0,allowInfinity:!0});const n=t.maxRetryTime??Number.POSITIVE_INFINITY;we("maxRetryTime",n,{min:0,allowInfinity:!0}),t.factor>0||(t.factor=1),(o=t.signal)==null||o.throwIfAborted();let a=0;const s=Date.now(),i=n;for(;a(e.CHOOSE_AN_ACCOUNT="CHOOSE_AN_ACCOUNT",e.LEGACY_LOGIN_WEB_PAGE_SPLAT="LEGACY_LOGIN_WEB_PAGE_SPLAT",e.LEGACY_LOGIN_WEB_PAGE_INDEX="LEGACY_LOGIN_WEB_PAGE_INDEX",e.ADVANCED_ACCOUNT_SECURITY_ENROLL="ADVANCED_ACCOUNT_SECURITY_ENROLL",e.ADVANCED_ACCOUNT_SECURITY_SECURE_METHODS="ADVANCED_ACCOUNT_SECURITY_SECURE_METHODS",e.ADVANCED_ACCOUNT_SECURITY_ENROLL_COMPLETE="ADVANCED_ACCOUNT_SECURITY_ENROLL_COMPLETE",e.ADVANCED_ACCOUNT_SECURITY_RECOVERY_KEYS="ADVANCED_ACCOUNT_SECURITY_RECOVERY_KEYS",e.ADVANCED_ACCOUNT_SECURITY_RECOVERY_KEYS_GENERATE="ADVANCED_ACCOUNT_SECURITY_RECOVERY_KEYS_GENERATE",e.ABOUT_YOU="ABOUT_YOU",e.ACCOUNT_CREATION_TEMPORARILY_UNAVAILABLE="ACCOUNT_CREATION_TEMPORARILY_UNAVAILABLE",e.ADD_EMAIL="ADD_EMAIL",e.ADD_PHONE="ADD_PHONE",e.CONTACT_VERIFICATION="CONTACT_VERIFICATION",e.CREATE_ACCOUNT="CREATE_ACCOUNT",e.CREATE_ACCOUNT_PASSWORD="CREATE_ACCOUNT_PASSWORD",e.CREATE_ACCOUNT_LATER="CREATE_ACCOUNT_LATER",e.CREATE_ACCOUNT_LATER_QUEUED="CREATE_ACCOUNT_LATER_QUEUED",e.DEVICE_AUTH_CALLBACK="DEVICE_AUTH_CALLBACK",e.EMAIL_VERIFICATION="EMAIL_VERIFICATION",e.EMAIL_ROLLBACK_SUCCESS="EMAIL_ROLLBACK_SUCCESS",e.EMAIL_ROLLBACK_REJECTED="EMAIL_ROLLBACK_REJECTED",e.EMAIL_ROLLBACK_CONFIRM="EMAIL_ROLLBACK_CONFIRM",e.ERROR="ERROR",e.ADD_PASSWORD_NEW_PASSWORD="ADD_PASSWORD_NEW_PASSWORD",e.LOG_IN="LOG_IN",e.LOG_IN_PASSWORD="LOG_IN_PASSWORD",e.LOG_IN_PASSKEY="LOG_IN_PASSKEY",e.JOIN_WAITLIST="JOIN_WAITLIST",e.JOIN_WAITLIST_ADDED="JOIN_WAITLIST_ADDED",e.AUTH_CHALLENGE_SELECTION="AUTH_CHALLENGE_SELECTION",e.AUTH_CHALLENGE_METHOD="AUTH_CHALLENGE_METHOD",e.MFA_CHALLENGE_SELECTION="MFA_CHALLENGE_SELECTION",e.MFA_CHALLENGE_METHOD="MFA_CHALLENGE_METHOD",e.MFA_ENROLL_METHOD="MFA_ENROLL_METHOD",e.PHONE_VERIFICATION="PHONE_VERIFICATION",e.PASSKEY_ENROLL="PASSKEY_ENROLL",e.PASSKEY_PRF_SYNC="PASSKEY_PRF_SYNC",e.PUSH_AUTH_VERIFICATION="PUSH_AUTH_VERIFICATION",e.RESET_PASSWORD_START="RESET_PASSWORD_START",e.RESET_PASSWORD_NEW_PASSWORD="RESET_PASSWORD_NEW_PASSWORD",e.RESET_PASSWORD_SUCCESS="RESET_PASSWORD_SUCCESS",e.SIGN_IN_WITH_CHATGPT_CODEX_CONSENT="SIGN_IN_WITH_CHATGPT_CODEX_CONSENT",e.SIGN_IN_WITH_CHATGPT_CODEX_ORGANIZATION="SIGN_IN_WITH_CHATGPT_CODEX_ORGANIZATION",e.SIGN_IN_WITH_CHATGPT_CONSENT="SIGN_IN_WITH_CHATGPT_CONSENT",e.SSO_SELECTION="SSO_SELECTION",e.START="START",e.VERIFY_YOUR_IDENTITY="VERIFY_YOUR_IDENTITY",e.WAITLIST="WAITLIST",e.WORKSPACE_SELECTION="WORKSPACE_SELECTION",e.UNIFIED_LOG_IN_OR_SIGN_UP_INPUT="UNIFIED_LOG_IN_OR_SIGN_UP_INPUT",e.UNKNOWN="UNKNOWN",e))(_||{});function no(e){return Object.values(_).includes(e)}function G(e){switch(e){case _.LEGACY_LOGIN_WEB_PAGE_SPLAT:case _.LEGACY_LOGIN_WEB_PAGE_INDEX:case _.DEVICE_AUTH_CALLBACK:case _.JOIN_WAITLIST:case _.JOIN_WAITLIST_ADDED:case _.WAITLIST:case _.UNKNOWN:case _.ACCOUNT_CREATION_TEMPORARILY_UNAVAILABLE:case _.EMAIL_ROLLBACK_SUCCESS:case _.EMAIL_ROLLBACK_REJECTED:case _.ADD_PASSWORD_NEW_PASSWORD:case _.ADD_PHONE:case _.PHONE_VERIFICATION:case _.ADVANCED_ACCOUNT_SECURITY_ENROLL:case _.ADVANCED_ACCOUNT_SECURITY_SECURE_METHODS:case _.ADVANCED_ACCOUNT_SECURITY_ENROLL_COMPLETE:case _.ADVANCED_ACCOUNT_SECURITY_RECOVERY_KEYS:case _.ADVANCED_ACCOUNT_SECURITY_RECOVERY_KEYS_GENERATE:case _.PASSKEY_PRF_SYNC:case _.AUTH_CHALLENGE_SELECTION:case _.AUTH_CHALLENGE_METHOD:case _.CHOOSE_AN_ACCOUNT:return A.ACCESS_FLOW_PAGE_TYPE_UNSPECIFIED;case _.ABOUT_YOU:return A.ACCESS_FLOW_PAGE_TYPE_ABOUT_YOU;case _.LOG_IN:return A.ACCESS_FLOW_PAGE_TYPE_LOGIN_START;case _.UNIFIED_LOG_IN_OR_SIGN_UP_INPUT:return A.ACCESS_FLOW_PAGE_TYPE_LOGIN_OR_SIGNUP_START;case _.START:return A.ACCESS_FLOW_PAGE_TYPE_APPLE_INTELLIGENCE_START;case _.LOG_IN_PASSWORD:return A.ACCESS_FLOW_PAGE_TYPE_LOGIN_PASSWORD;case _.LOG_IN_PASSKEY:return A.ACCESS_FLOW_PAGE_TYPE_LOGIN_PASSWORD;case _.CREATE_ACCOUNT:return A.ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_START;case _.CREATE_ACCOUNT_PASSWORD:return A.ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_PASSWORD;case _.CREATE_ACCOUNT_LATER:return A.ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_LATER;case _.CREATE_ACCOUNT_LATER_QUEUED:return A.ACCESS_FLOW_PAGE_TYPE_CREATE_ACCOUNT_LATER_QUEUED;case _.ADD_EMAIL:return A.ACCESS_FLOW_PAGE_TYPE_ADD_EMAIL;case _.CONTACT_VERIFICATION:return A.ACCESS_FLOW_PAGE_TYPE_CONTACT_VERIFICATION;case _.EMAIL_VERIFICATION:return A.ACCESS_FLOW_PAGE_TYPE_EMAIL_OTP_VERIFICATION;case _.EMAIL_ROLLBACK_CONFIRM:return A.ACCESS_FLOW_PAGE_TYPE_EMAIL_ROLLBACK_CONFIRM;case _.MFA_CHALLENGE_SELECTION:case _.MFA_CHALLENGE_METHOD:return A.ACCESS_FLOW_PAGE_TYPE_MFA_CHALLENGE;case _.MFA_ENROLL_METHOD:case _.PASSKEY_ENROLL:return A.ACCESS_FLOW_PAGE_TYPE_MFA_ENROLL;case _.PUSH_AUTH_VERIFICATION:return A.ACCESS_FLOW_PAGE_TYPE_PUSH_AUTH_VERIFICATION;case _.RESET_PASSWORD_START:return A.ACCESS_FLOW_PAGE_TYPE_RESET_PASSWORD_START;case _.RESET_PASSWORD_NEW_PASSWORD:return A.ACCESS_FLOW_PAGE_TYPE_RESET_PASSWORD_NEW_PASSWORD;case _.RESET_PASSWORD_SUCCESS:return A.ACCESS_FLOW_PAGE_TYPE_RESET_PASSWORD_SUCCESS;case _.SIGN_IN_WITH_CHATGPT_CONSENT:return A.ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_CONSENT;case _.SIGN_IN_WITH_CHATGPT_CODEX_CONSENT:return A.ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_CODEX_CONSENT;case _.SIGN_IN_WITH_CHATGPT_CODEX_ORGANIZATION:return A.ACCESS_FLOW_PAGE_TYPE_SIGN_IN_WITH_CHATGPT_CODEX_ORG;case _.SSO_SELECTION:return A.ACCESS_FLOW_PAGE_TYPE_SSO;case _.WORKSPACE_SELECTION:return A.ACCESS_FLOW_PAGE_TYPE_WORKSPACE;case _.VERIFY_YOUR_IDENTITY:return A.ACCESS_FLOW_PAGE_TYPE_IDENTITY_VERIFICATION;case _.ERROR:return A.ACCESS_FLOW_PAGE_TYPE_ERROR;default:return e}}async function yn(e,t){if(hn())return;const{maybeWriteClientAuthSessionCacheFromNavigationJson:n}=await tn(async()=>{const{maybeWriteClientAuthSessionCacheFromNavigationJson:a}=await Promise.resolve().then(()=>cc);return{maybeWriteClientAuthSessionCacheFromNavigationJson:a}},void 0);await n(e,t)}async function kc(e,t,n,{intercept:a,routeId:s}={}){var f;a=a??(()=>{});const i=new URL(e.url),o=new Headers(n==null?void 0:n.headers);o.set("Accept","application/json");const{result:c,durationMs:u}=await fn(fetch(t,{credentials:"include",...n,headers:o})),l=c.clone(),C=await c.json();k().eventLogger.logStructuredEvent(bn,{path:t.toString(),status:c.ok?Pe.ACCESS_FLOW_API_STATUS_SUCCESS:Pe.ACCESS_FLOW_API_STATUS_FAILURE,responseCode:c.status,latencyMs:u,page:s?G(s):void 0,errorMessage:c.ok||(f=C==null?void 0:C.error)==null?void 0:f.message}),c.ok&&await yn(C,t.toString());const p=await a({status:c.status,json:C});if(p)return p;if(!c.ok||typeof C!="object")throw l;const{continue_url:E=null}=C;if(!E)throw Response.json("Missing a continue_url",{status:400,statusText:"Missing a continue_url"});const N=new URL(E);return N.origin!==i.origin||N.pathname.startsWith("/api/")?tr(E):nr(E)}async function jc(e,t,n,{intercept:a,routeId:s}={}){var P;const i=new Headers(n==null?void 0:n.headers);i.set("Accept","application/json"),i.set("Content-Type","application/json");const o=`${k().authapiBaseUrl}${t}`,c={credentials:"include",method:"POST",signal:e.signal,...n,headers:i},{result:u,durationMs:l}=await fn(Qs(async()=>{const b=await fetch(o,c);if(b.status>=500&&b.status<=599)throw new Error(`Server error ${b.status}`);return b},{retries:1,factor:2,minTimeout:100,randomize:!0,signal:e.signal})),C=u.clone(),p=u.headers.get("Content-Type");if(!(p!=null&&p.includes("application/json")))throw console.info("Invalid content type",p,await u.text()),Response.json(`Invalid content type: ${p}`,{status:400,statusText:`Invalid content type: ${p}`});const E=await u.json();k().eventLogger.logStructuredEvent(bn,{path:t,status:u.ok?Pe.ACCESS_FLOW_API_STATUS_SUCCESS:Pe.ACCESS_FLOW_API_STATUS_FAILURE,responseCode:u.status,latencyMs:l,page:s?G(s):void 0,errorMessage:u.ok||(P=E==null?void 0:E.error)==null?void 0:P.message}),u.ok&&await yn(E,t);const N=a?await a({status:u.status,json:E}):void 0;if(N)return{page:N,responseHeaders:u.headers};if(!u.ok||typeof E!="object")throw C;const{page:f=null}=E;if(!f)throw Response.json("Missing a page",{status:400,statusText:"Missing a page"});return{page:f,responseHeaders:u.headers}}const Ge=e=>hn()?e.headers.get("cookie")??"":document.cookie;function vn(e,t){return Rn(e)[t]||null}const ro=r.object({connection_name:r.string(),title:r.string(),connection_provider:r.number().int().nullish()}),ao=r.object({id:r.string(),name:r.string(),username:$,avatar_url:r.string().nullish()}),io=r.enum(["ble","cable","hybrid","internal","nfc","usb"]),so=r.string(),jt=r.enum(["sms","whatsapp"]),oo=r.object({type:r.literal("public-key"),id:r.string(),transports:r.array(io).optional()}).passthrough(),_o=r.object({challenge:r.string(),timeout:r.number().optional(),rpId:r.string().optional(),allowCredentials:r.array(oo).optional(),userVerification:r.enum(["required","preferred","discouraged"]).optional(),extensions:r.record(r.unknown()).optional()}).passthrough(),co=r.object({mfa_request_id:r.string(),challenge_mode:r.string().optional().nullable(),match_number:r.string().optional().nullable()}),uo=r.object({passkey_request_options:_o,mfa_request_id:r.string(),use_browser_autofill:r.boolean().optional(),fallback_allowed:r.boolean().optional()}).passthrough(),lo=r.enum(["aas"]),Eo=r.enum(["passkey","recovery-code"]),Ht=r.object({}).passthrough(),So=r.object({challenge_type:lo,user_id:r.string(),available_methods:r.object({passkey:Ht.optional(),"recovery-code":Ht.optional()}).passthrough(),completed_methods:r.array(Eo)}),Ye=r.object({session_id:r.string().optional(),auth_session_logging_id:r.string().optional(),screen_hint:r.string().nullish(),codex_error_state:r.string().optional(),username:$.optional(),username_verified:r.boolean().optional(),email:r.string().optional(),email_verified:r.boolean().optional(),set_phone_number:r.string().optional(),set_phone_number_verified:r.boolean().optional(),set_phone_verification_channel:jt.optional(),phone_number:r.string().optional(),phone_number_verified:r.boolean().optional(),phone_verification_channel:jt.optional(),current_email:r.string().optional(),previous_email:r.string().optional(),name:r.string().optional(),birthdate:r.string().optional(),chatgpt_plan_type:r.string().optional(),unified_sessions:r.array(ao).optional(),sso:r.object({connections:r.array(ro),must_use_enterprise_sso:r.boolean().default(!1)}).optional(),third_party_login_association:Gs.optional(),workspaces:r.array(r.object({id:r.string(),name:r.string().nullish(),profile_picture_url:r.string().nullish(),profile_picture_alt_text:r.string().nullish(),kind:r.enum(["personal","organization"]),beta_settings:r.object({codex_device_code_auth:r.boolean().optional()}).optional()})).optional(),openai_client_id:r.string().optional(),originator:r.string().optional(),originator_display_name:r.string().optional(),logo_uri:r.string().optional(),policy_uri:r.string().optional(),tos_uri:r.string().optional(),app_name_enum:r.string().optional(),promo:r.string().optional(),signup_source:r.string().optional(),device_id:r.string().optional(),persona_inquiry:r.object({id:r.string()}).optional(),auth_challenge_data:So.optional(),mfa_factors:r.array(Nn).optional(),mfa_enrollment_factors:r.array(Pn).optional(),aas_enabled:r.boolean().optional(),login_start_url:r.string().optional(),is_device_authorization:r.boolean().optional(),country_code_hint:Ms.optional().catch(void 0),original_screen_hint:r.string().optional(),passkey_challenge_option:uo.optional(),push_auth_challenge_state:co.optional(),email_verification_mode:so.optional(),passwordless_disabled:r.boolean().optional(),passwordless_otp_from_password_redirect:r.boolean().optional(),reauth:r.boolean().optional(),post_login_add_password:r.boolean().optional()}),Co="oai-client-auth-session",po="login_web_enable_double_reads_for_get_client_auth_session";async function wn(e){const t=vn(e,Co);if(!t)return{is_missing_session:!0};const n=In(t,{header:!0}),a=Ye.safeParse(n);return a.success?{...a.data,is_missing_session:!1}:{is_missing_session:!0}}async function Hc(e){const t=await wn(Ge(e)),{statsigClient:n,maybeShadowCompareClientAuthSession:a}=k();return!t.is_missing_session&&(n!=null&&n.checkGate(po))&&a(e).catch(()=>{}),t}const go=g.createContext(null);function Ao(){const e=g.useContext(go);if(!e)throw new Error("useClientAuthSession must be used within a ClientAuthSessionProvider");return e}const ho=r.object({client_auth_session:Ye,checksum:r.string(),session_id:r.string().nullable()});class le extends Error{constructor(n,a){super(a??`Failed to fetch client auth session dump (${n})`);L(this,"status");L(this,"retryable");this.name="ClientAuthSessionDumpFetchError",this.status=n,this.retryable=n>=500}}async function fo({request:e}){var o;const t=new Headers(await k().getAuthapiHeaderMixins(e));t.set("Accept","application/json");const n={method:"GET",credentials:"include",headers:t,signal:e.signal};let a;try{a=await fetch(`${k().authapiBaseUrl}/client_auth_session_dump`,n)}catch{throw new le(503)}if(!a.ok){let c;try{const u=await a.clone().json();c=(o=u==null?void 0:u.error)==null?void 0:o.message}catch{}throw new le(a.status,c)}let s;try{s=await a.json()}catch{throw new le(500,"Invalid client auth session dump response body.")}const i=ho.safeParse(s);if(!i.success)throw new le(500,"Invalid client auth session dump response shape.");return i.data}const To="auth-session-minimized-client-checksum";function ct(e){const t=Rn(e)[To]||null;if(!t)return null;try{const n=JSON.parse(t);return typeof n.affinity=="string"?n.affinity:null}catch{return null}}const qe="client_auth_session_cache_cookie_mismatch",Oo="client_auth_session_cache_cookie_equal",Vt="login_web_client_auth_session_dump_fetch_needed";let V=null;function mo({cacheSession:e,cookieSession:t}){const n=Object.prototype.hasOwnProperty,a=[],s=[],i=[],o=Array.from(new Set([...Object.keys(t),...Object.keys(e)])).sort();for(const c of o){const u=n.call(t,c);if(!n.call(e,c)){a.push(c);continue}if(!u){s.push(c);continue}Me(t[c],e[c])||i.push(c)}return{diffKeys:i,keysPresentInCookieButNotCache:a,keysPresentInCacheButNotCookie:s,diffCount:i.length}}async function Io({request:e}){const t=await wn(Ge(e));if(!t.is_missing_session)try{const n=await No({request:e}),a=Ye.parse(t);if(Me(n.session,a)){m.addAction(Oo,{});return}const s=mo({cacheSession:n.session,cookieSession:a});m.addAction(qe,{mismatchKind:"session_payload_differs",...s})}catch(n){if(n instanceof le&&n.status===404){m.addAction(qe,{mismatchKind:"missing_session"});return}n instanceof le&&n.status>=500&&m.addAction(qe,{mismatchKind:"turtle_error"})}}const Vc=e=>{const t=Ge(e),n=ct(t);if((V==null?void 0:V.checksumSignal)===n)return V.promise;const a=Io({request:e}).finally(()=>{(V==null?void 0:V.promise)===a&&(V=null)});return V={checksumSignal:n,promise:a},a};async function No({request:e}){const{clientAuthSessionCache:t}=k(),n=Ge(e),a=t.read();if(a){const i=ct(n);if(a.checksum===i)return a;m.addAction(Vt,{reason:"stale_cache"})}else m.addAction(Vt,{reason:"missing_cache"});const s=await fo({request:e});return t.writeFromDumpResponse(s)}const Po=r.object({session_id:r.string().default(""),openai_client_id:r.string().default(""),app_name_enum:r.string().default(""),auth_session_logging_id:r.string().default(""),originator:r.string().default("")}),Un=r.object({track:r.enum(["stable","canary"]).default("stable"),requestStartMillis:r.number().default(-1),isDefaultBootstrapValue:r.boolean().default(!1),statsigBootstrap:r.string().nullable().default(null),shouldConsumeStatsigBootstrap:r.boolean().default(!1),immutableClientSessionMetadata:Po.default({}),experimentEvaluations:r.array(r.object({layerName:r.string(),experimentName:r.string(),groupName:r.string()})).default([])}),Dn=Un.parse({isDefaultBootstrapValue:!0}),Lo="bootstrap-inert-script";function xc(){try{const e=document.getElementById(Lo),t=e==null?void 0:e.textContent;if(!t)throw new Error("Bootstrap data not found");return{bootstrap:Un.parse(JSON.parse(t)),bytesCount:t.length}}catch(e){return m.addError(e,{message:"Error parsing bootstrap data"}),{bootstrap:Dn,bytesCount:0}}}const y=[];for(let e=0;e<256;++e)y.push((e+256).toString(16).slice(1));function bo(e,t=0){return(y[e[t+0]]+y[e[t+1]]+y[e[t+2]]+y[e[t+3]]+"-"+y[e[t+4]]+y[e[t+5]]+"-"+y[e[t+6]]+y[e[t+7]]+"-"+y[e[t+8]]+y[e[t+9]]+"-"+y[e[t+10]]+y[e[t+11]]+y[e[t+12]]+y[e[t+13]]+y[e[t+14]]+y[e[t+15]]).toLowerCase()}let ze;const Ro=new Uint8Array(16);function yo(){if(!ze){if(typeof crypto>"u"||!crypto.getRandomValues)throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");ze=crypto.getRandomValues.bind(crypto)}return ze(Ro)}const vo=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),xt={randomUUID:vo};function wo(e,t,n){var s;if(xt.randomUUID&&!t&&!e)return xt.randomUUID();e=e||{};const a=e.random??((s=e.rng)==null?void 0:s.call(e))??yo();if(a.length<16)throw new Error("Random bytes length must be >= 16");return a[6]=a[6]&15|64,a[8]=a[8]&63|128,bo(a)}const Uo="javascript-client",Do=3e4,Bt="__protobuf_structured_event__",Wo=new Set(["localhost","127.0.0.1","testserver"]),Fo={getPunchOut:()=>{},count:(e,t)=>{},addError:(e,t)=>{},addAction:(e,t)=>{}};function Mo(){let e,t;return{promise:new Promise((a,s)=>{e=a,t=s}),resolve:e,reject:t}}class Go{constructor({appName:t,appVersion:n,deviceId:a,browserLocale:s,options:i,settings:o,instrumentation:c=Fo}){this.initializeResolvedGate=Mo(),this.pendingStructuredTrackPromises=new Set,this.appName=t,this.appVersion=n,this.instrumentation=c,this.statscTags={app_name:this.appName,app_version:this.appVersion},this.analytics=new Promise(u=>{tn(()=>import("./segment-D0siyHcc.js"),__vite__mapDeps([0,1,2,3])).then(l=>{if(!l){this.instrumentation.count("AnalyticsLogger.segmentImport.failed",this.statscTags);return}const C=new l.AnalyticsBrowser;u([C])})}),this.deviceId=a,this.browserLocale=s,this.options=i,this.settings=o}async initialize({user:t,statsigClient:n}){t&&(this.user=t),n&&(this.statsigClient=n),this.initializePromise||(this.instrumentation.count("AnalyticsLogger.initialize.start",this.statscTags),this.initializePromise=(async()=>{if(this.structuredEventTransport=this.shouldUseStatsigLogEventTransport()?"statsig":"segment",this.structuredEventTransport==="statsig"){this.instrumentation.count("AnalyticsLogger.initialize.success",this.statscTags);return}const[a]=await this.analytics;a.load(this.settings,this.options).catch(s=>{this.instrumentation.count("AnalyticsLogger.initialize.failed",this.statscTags)}),a.ready(()=>{this.instrumentation.count("AnalyticsLogger.initialize.success",this.statscTags)})})().then(()=>{this.initializeResolvedGate.resolve()}).catch(a=>{throw this.initializeResolvedGate.reject(a),a})),await this.initializePromise}getServedHostname(){var n;const t=(n=globalThis.location)==null?void 0:n.hostname;if(!(typeof t!="string"||t.trim().length===0))return t.toLowerCase()}getServedOrigin(){var n;const t=(n=globalThis.location)==null?void 0:n.origin;if(!(typeof t!="string"||t.trim().length===0))return t}getTopLevelHost(t){const n=t.trim().toLowerCase(),a=n.split(".").filter(Boolean);return a.length<2?n:a.slice(-2).join(".")}isSameTopLevelHost(t,n){return typeof n!="string"||n.trim().length===0?!1:t===n.toLowerCase()||this.getTopLevelHost(t)===this.getTopLevelHost(n)}isClientEventsServiceLogEventUrl(t){let n;try{n=new URL(t,this.getServedOrigin()??"https://chatgpt.com")}catch{return!1}const a=n.hostname.toLowerCase(),s=n.pathname.replace(/\/+$/,"").toLowerCase(),i=s.endsWith("/v1/rgstr")||s.endsWith("/v1/log_event"),o=Wo.has(a),c=this.isSameTopLevelHost(a,this.getServedHostname());return!i||!o&&!c?!1:s.includes("/ces/")||o}shouldUseStatsigLogEventTransport(){var n,a;if(typeof((n=this.statsigClient)==null?void 0:n.logEvent)!="function")return!1;const t=(a=this.statsigClient.networkConfig)==null?void 0:a.logEventUrl;return typeof t=="string"&&t.trim().length>0&&this.isClientEventsServiceLogEventUrl(t)}buildSegmentEnvelope(t,n,a){var E,N,f,P,b,D,v,ge,re,ae;const s=t.$type,i=(E=this.statsigClient)==null?void 0:E.getContext(),o=i==null?void 0:i.user,c=(i==null?void 0:i.stableID)??void 0;let u;if(i){const ue=(o==null?void 0:o.locale)??this.browserLocale,be=ue?ue.split("-")[0]:void 0;u={stableId:c,sdkType:Uo,sdkVersion:st.SDK_VERSION,sessionId:(f=(N=i==null?void 0:i.session)==null?void 0:N.data)==null?void 0:f.sessionID,appIdentifier:this.appName,appVersion:this.appVersion,locale:ue,language:be}}if(o){let ue;o.customIDs&&(ue=Object.fromEntries(Object.entries(o.customIDs).filter(Re=>typeof Re[1]=="string")));let be;if(o.custom){const Re=Object.entries(o.custom).flatMap(zn=>{const[Jn,dt]=zn;if(dt===void 0)return[];try{const St=JSON.stringify(dt);return St===void 0?[]:[[Jn,St]]}catch{return[]}});Re.length>0&&(be=Object.fromEntries(Re))}const qn={userId:o.userID,customIds:ue,email:o.email,ip:o.ip,userAgent:o.userAgent,country:o.country,locale:o.locale,appVersion:o.appVersion,custom:be};u&&(u.user=qn)}const l=(b=(P=this.user)==null?void 0:P.traits)==null?void 0:b.is_openai_internal,C={userId:((D=this.user)==null?void 0:D.userId)??"",deviceId:this.deviceId,authStatus:this.user?"logged_in":"logged_out",planType:(ge=(v=this.user)==null?void 0:v.traits)==null?void 0:ge.plan_type,workspaceId:(ae=(re=this.user)==null?void 0:re.traits)==null?void 0:ae.workspace_id,isOpenaiInternal:l},p={};return{eventId:a.eventId,eventCreatedAt:a.eventCreatedAt,eventType:"web",userParams:C,deviceParams:p,statsigMetadataV2:u,eventParams:{"@type":`openai.buf.dev/openai/protobuf-analytics-events/${s}`,...n},punchOutInfoToken:this.instrumentation.getPunchOut(),clientMetadata:{name:this.appName,version:this.appVersion}}}buildStatsigEnvelope(t,n,a){const s=t.$type,i={};return{eventId:a.eventId,eventCreatedAt:a.eventCreatedAt,eventType:"web",deviceParams:i,eventParams:{"@type":`openai.buf.dev/openai/protobuf-analytics-events/${s}`,...n},punchOutInfoToken:this.instrumentation.getPunchOut()}}normalizeEventName(t){return(t.split(".").pop()??t).replace(/([a-z0-9])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2").toLowerCase()}settleWithin(t,n){let a;const s=new Promise(o=>{a=setTimeout(o,n)}),i=t.catch(()=>{}).then(()=>{});return Promise.race([i,s]).finally(()=>{a!=null&&clearTimeout(a)})}async trackStructuredEvent(t,n){const a={eventId:wo(),eventCreatedAt:new Date().toISOString()},s=this.initializeResolvedGate.promise.then(()=>this.sendStructuredEvent(t,n,a));this.trackStructuredTrackPromise(s),await s}trackStructuredTrackPromise(t){const n=this.settleWithin(t,Do);this.pendingStructuredTrackPromises.add(n),n.finally(()=>{this.pendingStructuredTrackPromises.delete(n)})}async sendStructuredEvent(t,n,a){const s=this.normalizeEventName(t.$type);if(this.structuredEventTransport==="statsig"){const o=this.buildStatsigEnvelope(t,n,a);try{this.statsigClient.logEvent({eventName:Bt,metadata:o})}catch(c){this.instrumentation.count("AnalyticsLogger.statsigLogEvent.failed",this.statscTags),c instanceof Error&&this.instrumentation.addError(c,{eventName:s})}}else{const o=this.buildSegmentEnvelope(t,n,a),c=this.analytics.then(([u])=>u.track(Bt,o)).catch(()=>{});this.trackStructuredTrackPromise(c),await this.analytics}const i={platform:"web",event_name:s,...this.statscTags};this.instrumentation.count("analytics_event_tracked",i)}async drainPendingStructuredTracks(t){for(;;){const n=t-Date.now();if(n<=0)return;const a=Array.from(this.pendingStructuredTrackPromises);if(a.length===0)return;await Promise.race([Promise.allSettled(a).then(()=>{}),new Promise(s=>{setTimeout(s,n)})])}}async flush(t=1e3){var i;const n=Number.isFinite(t)&&t>=0?t:1e3,a=Date.now()+n;await this.drainPendingStructuredTracks(a).catch(()=>{});const s=a-Date.now();s>0&&this.structuredEventTransport==="statsig"&&typeof((i=this.statsigClient)==null?void 0:i.flush)=="function"&&await Promise.race([this.statsigClient.flush().catch(()=>{}).then(()=>{}),new Promise(o=>{setTimeout(o,s)})])}async trackCounter(t,n){return this.trackStructuredEvent(to,{counterName:t,metricValue:n})}}class Yo{constructor({appName:t,deviceId:n,options:a,settings:s}){L(this,"analytics");L(this,"appName");L(this,"deviceId");L(this,"options");L(this,"settings");this.analytics=new er,this.appName=t,this.deviceId=n,this.options=a,this.settings=s,this.analytics.addSourceMiddleware(ko(this))}initialize(){this.analytics.load(this.settings,this.options).catch(t=>{console.error("Failed to load analytics",t)})}page({name:t,routeId:n,isError:a}){this.analytics.page("Identity",t,{route_id:n,is_error:a,search:"redacted",url:"redacted",referrer:"redacted",origin:"login-web"})}track(t,n){this.analytics.track(t,{...n,openai_app:this.appName,origin:"login-web"})}getExtraContext(){const{openai_client_id:t="",app_name_enum:n="",auth_session_logging_id:a=""}=k().immutableClientSessionMetadata;return{app_name:this.appName,app_version:"f305bca6afc89159e811fd824396d6daaf880760",device_id:this.deviceId,auth_session_logging_id:a,openai_client_id:t,app_name_enum:n}}}function ko(e){return({payload:t,next:n})=>{t.obj.context={...t.obj.context,...e.getExtraContext()},n(t)}}const Wn="chatgpt.com/ces",Fn="https",jo=Ss(),Ho={strategy:"batching",config:{size:10,timeout:6e3}},Kt=jo?Ho:void 0,Mn={disableClientPersistence:!0,integrations:{"Segment.io":{apiHost:`${Wn}/v1`,protocol:Fn,...Kt?{deliveryStrategy:Kt}:void 0}}},Gn={writeKey:"oai",cdnURL:`${Fn}://${Wn}`},Bc={[_.LEGACY_LOGIN_WEB_PAGE_SPLAT]:()=>"Legacy Login Web Page",[_.LEGACY_LOGIN_WEB_PAGE_INDEX]:()=>"Legacy Login Web Page",[_.ADVANCED_ACCOUNT_SECURITY_ENROLL]:()=>"Advanced Account Security Enrollment",[_.ADVANCED_ACCOUNT_SECURITY_SECURE_METHODS]:()=>"Advanced Account Security Secure Methods",[_.ADVANCED_ACCOUNT_SECURITY_ENROLL_COMPLETE]:()=>"Advanced Account Security Enrollment Complete",[_.ADVANCED_ACCOUNT_SECURITY_RECOVERY_KEYS]:()=>"Advanced Account Security Recovery Keys",[_.ADVANCED_ACCOUNT_SECURITY_RECOVERY_KEYS_GENERATE]:()=>"Advanced Account Security Recovery Keys Generate",[_.ABOUT_YOU]:()=>"About You",[_.ACCOUNT_CREATION_TEMPORARILY_UNAVAILABLE]:()=>"Account Creation Temporarily Unavailable",[_.ADD_PASSWORD_NEW_PASSWORD]:()=>"Add Password New Password",[_.ADD_EMAIL]:()=>"Add Email",[_.ADD_PHONE]:()=>"Add Phone",[_.CHOOSE_AN_ACCOUNT]:()=>"Choose An Account",[_.CONTACT_VERIFICATION]:()=>"Contact Verification",[_.CREATE_ACCOUNT]:()=>"Create Account",[_.CREATE_ACCOUNT_PASSWORD]:()=>"Create Account Password",[_.CREATE_ACCOUNT_LATER]:()=>"Create Account Later",[_.CREATE_ACCOUNT_LATER_QUEUED]:()=>"Create Account Later Queued",[_.DEVICE_AUTH_CALLBACK]:()=>"Device Authorization Callback",[_.EMAIL_VERIFICATION]:()=>"Email Verification",[_.EMAIL_ROLLBACK_SUCCESS]:()=>"Email Rollback Success",[_.EMAIL_ROLLBACK_REJECTED]:()=>"Email Rollback Rejected",[_.EMAIL_ROLLBACK_CONFIRM]:()=>"Email Rollback Confirm",[_.ERROR]:()=>"Error",[_.LOG_IN]:()=>"Log In",[_.LOG_IN_PASSWORD]:()=>"Log In Password",[_.LOG_IN_PASSKEY]:()=>"Log In Passkey",[_.UNIFIED_LOG_IN_OR_SIGN_UP_INPUT]:()=>"Unified Login Or Signup Input Page",[_.JOIN_WAITLIST]:()=>"Join Waitlist",[_.JOIN_WAITLIST_ADDED]:()=>"Join Waitlist Added",[_.AUTH_CHALLENGE_SELECTION]:()=>"Auth Challenge Selection",[_.AUTH_CHALLENGE_METHOD]:()=>"Auth Challenge Method",[_.MFA_CHALLENGE_SELECTION]:()=>"MFA Challenge Selection",[_.MFA_CHALLENGE_METHOD]:()=>"MFA Challenge Method",[_.MFA_ENROLL_METHOD]:()=>"MFA Enroll Method",[_.PHONE_VERIFICATION]:()=>"Phone Verification",[_.PASSKEY_ENROLL]:()=>"Passkey Enroll",[_.PASSKEY_PRF_SYNC]:()=>"Passkey PRF Sync",[_.PUSH_AUTH_VERIFICATION]:()=>"Push Auth Verification",[_.RESET_PASSWORD_START]:()=>"Reset Password Start",[_.RESET_PASSWORD_NEW_PASSWORD]:()=>"Reset Password New Password",[_.RESET_PASSWORD_SUCCESS]:()=>"Reset Password Success",[_.SIGN_IN_WITH_CHATGPT_CONSENT]:()=>"Sign In With ChatGPT Consent",[_.SSO_SELECTION]:()=>"SSO Selection",[_.START]:()=>"Log In or Sign Up",[_.VERIFY_YOUR_IDENTITY]:()=>"Verify Your Identity",[_.WAITLIST]:()=>"Waitlist",[_.WORKSPACE_SELECTION]:()=>"Workspace Selection",[_.SIGN_IN_WITH_CHATGPT_CODEX_CONSENT]:()=>"Sign In With ChatGPT Codex Consent",[_.SIGN_IN_WITH_CHATGPT_CODEX_ORGANIZATION]:()=>"Sign In With ChatGPT Codex Organization",[_.UNKNOWN]:()=>"Unknown"},Yn="login_web",Vo="f305bca6afc89159e811fd824396d6daaf880760",xo=navigator.language,Ue=new Yo({appName:Yn,settings:Gn,options:Mn,deviceId:Oe}),Je=new Go({appName:Yn,appVersion:Vo,deviceId:Oe,browserLocale:xo,options:Mn,settings:Gn}),S={initialize({statsigClient:e}={}){Ue.initialize(),Je.initialize({user:void 0,statsigClient:e})},logEvent(e,t){Ue.track(e,t)},logPageView(e){Ue.page(e)},logStructuredEvent(e,t){Je.trackStructuredEvent(e,t)},trackCounter(e,t){return Je.trackCounter(e,t)},getClient(){return Ue}},$t=6;function ut(e){return r.string({required_error:e.formatMessage({id:"emailVerification.codeRequired",defaultMessage:"The verification code is required",description:"Error message shown when the verification code field is empty on the email verification page."})}).length($t,e.formatMessage({id:"emailVerification.incorrectCodeLength",defaultMessage:"The verification code should be exactly {length} characters long",description:"Error message shown when the verification code length is incorrect on the email verification page."},{length:$t})).regex(/^\d*$/,e.formatMessage({id:"emailVerification.invalidCodeFormat",defaultMessage:"Code must contain only numbers",description:"Error message shown when a user types in non-number characters for their one-time-passcode that has only numbers."}))}r.enum(["resend","validate"]);const d=r.enum(["advanced_account_security_enroll","about_you","add_password_new_password","add_email","add_phone","apple_intelligence_start","auth_challenge","contact_verification","choose_an_account","create_account_later","create_account_later_queued","create_account_password","login_passkey","create_account_start","email_otp_send","email_otp_verification","error","external_url","identity_verification","login_or_signup_start","login_password","login_start","mfa_challenge","mfa_enroll","phone_otp_send","phone_otp_verification","push_auth_verification","reset_password_start","reset_password_new_password","reset_password_success","sign_in_with_chatgpt_consent","sign_in_with_chatgpt_codex_consent","sign_in_with_chatgpt_codex_org","sso","token_exchange","workspace","__test__"]),Se=r.enum(["validate","resend"]);function Bo(e){const t=r.object({intent:r.literal(Se.enum.validate),username_kind:ee,code:ut(e)}),n=r.object({intent:r.literal(Se.enum.resend),username_kind:ee});return r.discriminatedUnion("intent",[t,n])}function Kc(e){return r.object({origin_page_type:d.extract([d.enum.contact_verification]),data:Bo(e)})}class Ko extends Error{constructor({userVisibleMessage:n,errorCode:a,metadata:s}){super(a);L(this,"errorCode");L(this,"userVisibleErrorId");L(this,"userVisibleMessage");L(this,"metadata");this.name="DetailedRouteError",this.userVisibleMessage=n,this.errorCode=a,this.userVisibleErrorId=$o(),this.metadata=s}}function $o(){return Math.floor(Math.random()*1e8).toString().padStart(8,"0")}var h=(e=>(e.pageLoad="Page Load",e.useSso="Use Sso",e.useEmail="Use Email",e.usePhone="Use Phone",e.invalidEmail="Invalid Email",e.invalidPhoneNumber="Invalid Phone Number",e.loginLinkClicked="Log in Link Clicked",e.signupLinkClicked="Sign up Link Clicked",e.onboardUserInfoComplete="Onboarding: User Info: Complete",e.ageFallbackTriggered="Onboarding: Age Fallback Triggered",e.ageFallbackSubmit="Onboarding: Age Fallback Submit",e.ageFallbackUseDob="Onboarding: Age Fallback Use DOB",e.ageFallbackConfirmShown="Onboarding: Age Fallback Confirm Shown",e.ageFallbackConfirmCanceled="Onboarding: Age Fallback Confirm Canceled",e.verifyPassword="Verify Password",e.verifyPasswordResult="Verify Password Result",e.registerUser="Register User",e.validateOtp="Validate OTP",e.resendOtp="Resend OTP",e.emailAlreadyVerified="Email Already Verified",e.wasDeniedAccountCreation="Was Denied Account Creation",e.missingSessionDetected="Missing Session Detected",e.missingSessionErrorPageAction="Missing Session Error Page Action",e.wordmarkClicked="Wordmark Clicked",e.usernameKindSwitched="Username Kind Switched",e.passkeyEnrollmentFailed="Temp Passkey Enrollment Failed",e.issueMfaChallengeFailed="Issue MFA Challenge Failed",e.reissueMfaChallengeFailed="Reissue MFA Challenge Failed",e))(h||{});const qo=new Proxy({},{get:()=>et}),kn=g.createContext(qo);function $c(){return g.useContext(kn)}function zo(e){switch(e){case"google-oauth2":return F.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_GOOGLE;case"windowslive":return F.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_MICROSOFT;case"apple":return F.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE_WITH_APPLE;default:return F.ACCESS_FLOW_USER_ACTION_TYPE_UNSPECIFIED}}function Jo(e){switch(e){case ee.enum.email:return F.ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_EMAIL;case ee.enum.phone_number:return F.ACCESS_FLOW_USER_ACTION_TYPE_SWITCH_TO_PHONE;default:return e}}function qt(e,t=1e3){return e.length<=t?e:`${e.slice(0,t)}…`}function Xo(e){return Object.fromEntries(Object.entries(e).filter(([,t])=>t!==void 0))}function Zo(e){return Object.fromEntries(Object.entries(e).map(([t,n])=>[t,String(n)]))}function M(){return k().immutableClientSessionMetadata.auth_session_logging_id||"missing"}const jn={logSwitchToSignUp({showedInvalidEmailError:e,fromRoute:t}){const n={typedInvalidEmail:e?"true":"false",loginWebUI:"new",route:t};S.logEvent(h.signupLinkClicked,n),S.logStructuredEvent(q,{authSessionLoggingId:M(),actionType:F.ACCESS_FLOW_USER_ACTION_TYPE_SIGNUP_INSTEAD_LINK_CLICKED,page:G(t)}),T({eventName:"login_web_signup_link_clicked",value:t,metadata:n})},logSwitchToLogIn({showedInvalidEmailError:e,fromRoute:t}){const n={typedInvalidEmail:e?"true":"false",loginWebUI:"new",route:t};S.logEvent(h.loginLinkClicked,n),S.logStructuredEvent(q,{authSessionLoggingId:M(),actionType:F.ACCESS_FLOW_USER_ACTION_TYPE_LOGIN_INSTEAD_LINK_CLICKED,page:G(t)}),T({eventName:"login_web_login_link_clicked",value:t,metadata:n})},logSocialSso({connection:e,fromRoute:t}){const n={flow:"authapi",loginWebUI:"new",connection:e,route:t};S.logEvent(h.useSso,n),S.logStructuredEvent(q,{authSessionLoggingId:M(),actionType:zo(e),page:G(t)}),T({eventName:"login_web_use_sso",value:e,metadata:n})},logContinueWithUsername({isInputPhoneNumber:e,fromRoute:t}){const n={flow:"authapi",loginWebUI:"new",route:t};S.logEvent(e?h.usePhone:h.useEmail,n),S.logStructuredEvent(q,{authSessionLoggingId:M(),actionType:F.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE,page:G(t),field:e?fe.ACCESS_FLOW_FIELD_PHONE:fe.ACCESS_FLOW_FIELD_EMAIL}),T({eventName:e?"login_web_use_phone_number":"login_web_use_email",value:t,metadata:n})},logInvalidInput({isInputPhoneNumber:e,fromRoute:t}){const n={flow:"authapi",loginWebUI:"new",route:t};S.logEvent(e?h.invalidPhoneNumber:h.invalidEmail,n),S.logStructuredEvent(ks,{authSessionLoggingId:M(),page:G(t),field:e?fe.ACCESS_FLOW_FIELD_PHONE:fe.ACCESS_FLOW_FIELD_EMAIL,validationType:Ln.ACCESS_FLOW_VALIDATION_TYPE_CLIENT}),T({eventName:e?"login_web_invalid_phone_number":"login_web_invalid_email",value:t,metadata:n})},logOnboardUserInfoComplete(){const e={flow:"authapi",loginWebUI:"new"};S.logEvent(h.onboardUserInfoComplete,e),T({eventName:"login_web_onboarding_user_info_complete",metadata:e})},logAgeFallbackTriggered(){const e={flow:"authapi",loginWebUI:"new",route:_.ABOUT_YOU};S.logEvent(h.ageFallbackTriggered,e),T({eventName:"login_web_age_fallback_triggered",metadata:e})},logAgeFallbackSubmit(){const e={flow:"authapi",loginWebUI:"new",route:_.ABOUT_YOU};S.logEvent(h.ageFallbackSubmit,e),T({eventName:"login_web_age_fallback_submit",metadata:e})},logAgeFallbackUseDob(){const e={flow:"authapi",loginWebUI:"new",route:_.ABOUT_YOU};S.logEvent(h.ageFallbackUseDob,e),T({eventName:"login_web_age_fallback_use_dob",metadata:e})},logAgeFallbackConfirmShown(){const e={flow:"authapi",loginWebUI:"new",route:_.ABOUT_YOU};S.logEvent(h.ageFallbackConfirmShown,e),T({eventName:"login_web_age_fallback_confirm_shown",metadata:e})},logAgeFallbackConfirmCanceled(){const e={flow:"authapi",loginWebUI:"new",route:_.ABOUT_YOU};S.logEvent(h.ageFallbackConfirmCanceled,e),T({eventName:"login_web_age_fallback_confirm_canceled",metadata:e})},logPageView({name:e,routeId:t,isError:n,previousRouteId:a,transitionLatencyMs:s}){S.logPageView({name:e,routeId:t,isError:n});const i={authSessionLoggingId:M(),page:G(t)};a&&(i.previousPage=G(a)),s!==void 0&&(i.transitionLatencyMs=s),S.logStructuredEvent(Ys,i),T({eventName:`Login Web: Page View: ${e}`,value:t,metadata:{routeId:t,isError:`${n}`}}),m.addPageView({name:e,routeId:t,isError:n})},logIssueMfaChallengeFailed({factorType:e}){const t={factorType:e};S.logEvent(h.issueMfaChallengeFailed,t),T({eventName:"login_web_issue_mfa_challenge_failed",value:e,metadata:t})},logPasskeyFailure({error:e,httpResponseBody:t,mfaTokenUserId:n,...a}){const s=a.errorName??(e&&typeof e=="object"&&"name"in e?String(e.name):void 0),i=a.errorMessage??(e&&typeof e=="object"&&"message"in e?String(e.message):void 0);S.logStructuredEvent(eo,{authSessionLoggingId:M(),stage:a.stage,isAutoAttempt:a.isAutoAttempt,originAppName:a.originAppName,creationOptionsSource:a.creationOptionsSource,normalizedErrorCode:a.normalizedErrorCode,normalizedErrorMessage:a.normalizedErrorMessage,rawErrorName:a.rawErrorName,rawErrorMessage:a.rawErrorMessage,rawErrorCode:a.rawErrorCode,errorName:s,errorMessage:i,errorStack:a.errorStack,httpStatus:a.httpStatus,httpStatusText:a.httpStatusText,httpResponseBody:t&&qt(t,1e3),httpRequestId:a.httpRequestId,redirectPath:a.redirectPath,hasMfaToken:a.hasMfaToken,passkeyUsage:a.passkeyUsage,mfaTokenUserId:n});const o=Xo({...a,originAppName:a.originAppName,errorName:s,errorMessage:i,mfaTokenUserId:n,httpResponseBody:t&&qt(t,1e3),httpRequestId:a.httpRequestId}),c=Object.keys(o).length>0?Zo(o):void 0;T({eventName:a.statsigEventName,value:a.stage,metadata:c}),m.addAction(a.statsigEventName,o)},logReissueMfaChallengeFailed({factorType:e}){const t={factorType:e};S.logEvent(h.reissueMfaChallengeFailed,t),T({eventName:"login_web_reissue_mfa_challenge_failed",value:e,metadata:t})},logFallbackErrorPage({error:e}){m.addError(e,e instanceof Ko?{errorCode:e.errorCode,userVisibleErrorId:e.userVisibleErrorId,metadata:e.metadata}:void 0)},logMissingSessionDetected({routeId:e,durationMs:t}){m.addAction("login_web_missing_session_detected",{routeId:e,...t!==void 0&&{durationMs:t}}),S.logEvent(h.missingSessionDetected,{routeId:e,...t!==void 0&&{durationMs:t}});const n={};e&&(n.routeId=e),t!==void 0&&(n.durationMs=String(t)),T({eventName:"Login Web: Missing Session Detected",value:e,metadata:Object.keys(n).length?n:void 0})},logMissingSessionErrorPageAction({action:e}){S.logEvent(h.missingSessionErrorPageAction,{action:e}),T({eventName:"Login Web: Missing Session Error Page Action",value:e})},logWasDeniedAccountCreation({email:e,phoneNumber:t,pageShown:n}){S.logEvent(h.wasDeniedAccountCreation,{email:e,phoneNumber:t,pageShown:n})},logVerifyPassword({usernameKind:e}){S.logEvent(h.verifyPassword,{usernameKind:e}),T({eventName:"login_web_verify_password",value:e})},logVerifyPasswordResult({usernameKind:e,result:t}){S.logEvent(h.verifyPasswordResult,{usernameKind:e,result:t}),T({eventName:"login_web_verify_password_result",value:t,metadata:{usernameKind:e,result:t}})},logRegisterUser({usernameKind:e}){S.logEvent(h.registerUser,{usernameKind:e}),T({eventName:"login_web_register_user",value:e})},logContactVerification(e){e.intent===Se.enum.validate?(S.logEvent(h.validateOtp,e),T({eventName:"login_web_validate_otp",metadata:e})):e.intent===Se.enum.resend&&(S.logEvent(h.resendOtp,e),T({eventName:"login_web_resend_otp",metadata:e}))},logEmailAlreadyVerified({routeId:e,authSessionLoggingId:t,screenHint:n}){const a={routeId:e,...t?{authSessionLoggingId:t}:{},...n?{screenHint:n}:{}};S.logEvent(h.emailAlreadyVerified,a),T({eventName:"login_web_email_already_verified",value:e,metadata:a})},logWordmarkClicked({isNavigatable:e,fromRoute:t}){S.logEvent(h.wordmarkClicked,{isNavigatable:e}),T({eventName:"login_web_wordmark_clicked",value:e?"true":"false"}),S.logStructuredEvent(q,{authSessionLoggingId:M(),actionType:F.ACCESS_FLOW_USER_ACTION_TYPE_WORDMARK_CLICKED,page:G(t)})},logUsernameKindSwitched({usernameKind:e,fromRoute:t}){S.logEvent(h.usernameKindSwitched,{usernameKind:e});const n=Jo(e);S.logStructuredEvent(q,{authSessionLoggingId:M(),actionType:n,page:G(t)}),T({eventName:"login_web_username_kind_switched",value:e})},logUserInputAction({actionType:e,page:t,field:n}){S.logStructuredEvent(q,{authSessionLoggingId:M(),actionType:e??F.ACCESS_FLOW_USER_ACTION_TYPE_INPUT,page:t,field:n})},logStructuredEvent(e,t){S.logStructuredEvent(e,{authSessionLoggingId:M(),...t})},logContinueAction(e,t){S.logStructuredEvent(q,{authSessionLoggingId:M(),actionType:t??F.ACCESS_FLOW_USER_ACTION_TYPE_CONTINUE,page:G(e)})}},qc=jn,zc=({children:e})=>j.jsx(kn.Provider,{value:jn,children:e});function zt({useSentinelDomain:e}){return new Promise((t,n)=>{const a=document.createElement("script");a.type="text/javascript",a.src=e?"https://sentinel.openai.com/backend-api/sentinel/sdk.js":"https://chatgpt.com/backend-api/sentinel/sdk.js",a.async=!0,a.defer=!0,a.onload=t,a.onerror=n,document.getElementsByTagName("head")[0].appendChild(a)})}const Qo=async()=>{try{await zt({useSentinelDomain:!0})}catch(e){m.addError(e,{message:"Failed to load Sentinel SDK script, retrying with chatgpt.com domain"});try{await zt({useSentinelDomain:!1})}catch(t){throw m.addError(t,{message:"Failed to load Sentinel SDK script from chatgpt.com domain after trying sentinel domain"}),t}}},Hn=(()=>{let e;return()=>(e||(e=Qo()),e)})();function Jc(e,t){const n={};return n["OpenAI-Sentinel-Token"]=e,t&&(n["OpenAI-Sentinel-SO-Token"]=t),n}function Xe(e,t,n){if(e!==void 0)return(t??n)-e}function e_(e,t,n){const a=performance.now(),s=Xe(n.helperStartedAtMs,n.sdkReadyCompletedAtMs,a),i=Xe(n.sentinelTokenStartedAtMs,n.sentinelTokenCompletedAtMs,a),o=Xe(n.sessionObserverTokenStartedAtMs,n.sessionObserverTokenCompletedAtMs,a);m.addAction("sentinel_sdk_token_fetch_timeout",{flow:e,timeout_ms:t,timeout_cause:n.sdkReadyCompletedAtMs===void 0?"sdk_ready":n.sentinelTokenCompletedAtMs===void 0?"sentinel_token_mint":"session_observer_token_mint",sdk_ready_completed:n.sdkReadyCompletedAtMs!==void 0,sentinel_token_started:n.sentinelTokenStartedAtMs!==void 0,sentinel_token_completed:n.sentinelTokenCompletedAtMs!==void 0,session_observer_token_started:n.sessionObserverTokenStartedAtMs!==void 0,session_observer_token_completed:n.sessionObserverTokenCompletedAtMs!==void 0,...s!==void 0?{sdk_ready_duration_ms:s}:{},...i!==void 0?{sentinel_token_duration_ms:i}:{},...o!==void 0?{session_observer_token_duration_ms:o}:{}})}async function lt(e){return await Hn(),e&&(e.sdkReadyCompletedAtMs=performance.now()),window.SentinelSDK}async function t_(e,t=lt(),n){const a=await t;if(a===void 0||typeof a.token!="function")return JSON.stringify({e:"q2n8w7x5z1"});n&&(n.sentinelTokenStartedAtMs=performance.now());try{return await a.token(e)}catch(s){return m.addError(s,{message:"Sentinel SDK token fetch failed",flow:e}),JSON.stringify({e:"k9d4s6v3b2"})}finally{n&&(n.sentinelTokenCompletedAtMs=performance.now())}}function Xc(e){return Hn().then(async()=>{try{const t=window.SentinelSDK;return t===void 0||typeof t.init!="function"?null:await t.init(e)}catch(t){return m.addError(t,{message:"Sentinel prefetch requirements failed for flow",flow:e}),null}}).catch(()=>null)}function n_(e,t,n,a,s){return s===void 0?e:new Promise((i,o)=>{const c=setTimeout(()=>{e_(t,s,n);const u=a();if(u!==null){i(u);return}o(new An)},s);e.then(u=>{clearTimeout(c),i(u)},u=>{clearTimeout(c),o(u)})})}function Zc(e,{timeoutMs:t}={}){const n={helperStartedAtMs:performance.now()},a=lt(n);let s,i;const o=t_(e,a,n).then(u=>(s=u,u)),c=r_(e,a,n).then(u=>(i=u,u));return n_(Promise.all([o,c]).then(([u,l])=>({sentinelToken:u,sessionObserverToken:l})),e,n,()=>s!==void 0?{sentinelToken:s,sessionObserverToken:i??null}:i?{sentinelToken:JSON.stringify({e:"k9d4s6v3b2"}),sessionObserverToken:i}:null,t)}async function r_(e,t=lt(),n){const a=await t;if(a===void 0||typeof a.sessionObserverToken!="function")return null;n&&(n.sessionObserverTokenStartedAtMs=performance.now());try{return await a.sessionObserverToken(e)}catch(s){return m.addError(s,{message:"Sentinel SDK session observer token fetch failed"}),null}finally{n&&(n.sessionObserverTokenCompletedAtMs=performance.now())}}const a_=g.createContext(null);function Qc(){const e=g.useContext(a_);return e||Dn}function i_(){return rr().at(-1)}function eu(){const{id:e}=i_()??{id:_.UNKNOWN};return no(e)?e:_.UNKNOWN}const s_=()=>()=>{};function tu(e){return g.useSyncExternalStore(s_,e,()=>{})}function o_(e){const t=g.useRef(e);return or(()=>{t.current=e},[e]),t}function nu(e){const t=o_(e);g.useEffect(()=>t.current(),[t])}const w=cr({missingEmailOrPhoneNumber:{id:"authApiFailure.missingEmailOrPhoneNumber",defaultMessage:"You must provide at least an email or phone number to continue.",description:"Error message when the user did not provide an email or a phone number when signing in."},cannotUsePlusEmail:{id:"authApiFailure.cannotUsePlusEmail",defaultMessage:"If you want to use a + in the openai email, please use username and password.",description:"Error message when the user's email contains a '+' (plus) character."},ssoRequired:{id:"authApiFailure.ssoRequired",defaultMessage:"Please use your organization's SSO to access your account.",description:"Error message when the user's organization has SSO (single sign-on) configured but the user used a non-SSO login method."},emailVerificationRequired:{id:"authApiFailure.emailVerificationRequired",defaultMessage:"You must verify your email with OpenAI before continuing.",description:"Error message when the user tried to sign in with their email but that email address is not yet verified."},phoneNumberVerificationRequired:{id:"authApiFailure.phoneNumberVerificationRequired",defaultMessage:"You must verify your phone number with OpenAI before continuing.",description:"Error message when the user tried to sign in with their phone number but that phone number is not yet verified."},chatGptAccountMissing:{id:"authApiFailure.chatGptAccountMissing",defaultMessage:"No eligible ChatGPT account found.",description:"Error message when a personal ChatGPT account is required for the application the user tries to access but no such account was found."},chatGptAccountMissingSso:{id:"authApiFailure.chatGptAccountMissingSso",defaultMessage:"No eligible ChatGPT account found. Authenticate with SSO to access your available account.",description:"Error message when a workspace requires SSO but the user logged in without SSO."},tokenMissingEmailClaim:{id:"authApiFailure.TokenMissingEmailClaim",defaultMessage:"OpenAI requires an email address to access our services, but the identity provider you signed in with did not provide one.",description:"OpenAI requires an email address to access our services, but the identity provider you signed in with did not provide one."},loginRequestExpired:{id:"authApiFailure.LoginRequestExpired",defaultMessage:"Your login session has expired, please try again.",description:"Your login session has expired, please try again."},pushAuthLoginRequestDenied:{id:"authApiFailure.PushAuthLoginRequestDenied",defaultMessage:"Your login request was denied. If this was a mistake, please try signing in again.",description:"Your login request was denied, please try signing in again."},thirdPartyPolicyCheckFailed:{id:"authApiFailure.ThirdPartyPolicyCheckFailed",defaultMessage:"We couldn't verify whether this account is eligible for this app. Please try again.",description:"Error shown when a third-party app policy check fails and login-web should avoid displaying the untranslated backend user message."},thirdPartyPhoneAccountNotSupported:{id:"authApiFailure.ThirdPartyPhoneAccountNotSupported",defaultMessage:"Signing in to this application with a phone-number account is not supported.",description:"Error shown when a user tries to sign in to a third-party app with a phone-number account."},thirdPartyTenantPolicyDenied:{id:"authApiFailure.ThirdPartyTenantPolicyDenied",defaultMessage:"You can't sign in to this application with ChatGPT because one of your tenants does not allow it.",description:"Error shown when a tenant-level third-party app policy blocks the user from signing in with ChatGPT."},thirdPartyWorkspacePlanBlocked:{id:"authApiFailure.ThirdPartyWorkspacePlanBlocked",defaultMessage:"Your workspaces or plan types are not eligible to sign in to this application with ChatGPT.",description:"Error shown when the user's workspace plans are not eligible for third-party app sign-in with ChatGPT."},errorWithCode:{id:"authApiFailure.errorWithCode",defaultMessage:"An error occurred during authentication ({code}). Please try again.",description:"Generic authentication error message with an error code."},genericError:{id:"authApiFailure.genericError",defaultMessage:"An error occurred during authentication. Please try again.",description:"Generic authentication error message."}}),__={missing_email_or_phone_number:w.missingEmailOrPhoneNumber,cannot_use_plus_email:w.cannotUsePlusEmail,sso_required:w.ssoRequired,email_verification_required:w.emailVerificationRequired,phone_number_verification_required:w.phoneNumberVerificationRequired,chatgpt_account_missing:w.chatGptAccountMissing,chatgpt_account_sso_required:w.chatGptAccountMissingSso,token_missing_email_claim:w.tokenMissingEmailClaim,login_request_expired:w.loginRequestExpired,push_auth_login_request_denied:w.pushAuthLoginRequestDenied,"3p_login_account_type_policy_check_failed":w.thirdPartyPolicyCheckFailed,"3p_login_phone_account_not_supported":w.thirdPartyPhoneAccountNotSupported,"3p_login_tenant_policy_denied":w.thirdPartyTenantPolicyDenied,"3p_login_workspace_plan_blocked":w.thirdPartyWorkspacePlanBlocked};function ru(e){const t=e?__[e]:void 0;return t?{descriptor:t}:e?{descriptor:w.errorWithCode,values:{code:e}}:{descriptor:w.genericError}}const c_=g.createContext({});function au(){return g.useContext(c_)}const u_=g.createContext(void 0);function iu(){return g.useContext(u_)}function su(){return se.useLayer("auth_login_signup_holdout_layer").get("is_font_improvements_enabled",!1)}r.object({type:d.extract([d.enum.token_exchange]),continue_url:r.string(),payload:r.object({state:r.string(),code:r.string()}).passthrough()});const ou=r.enum(["revert_to_previous_email","keep_current_email"]);function l_(){return new Uint8Array(32).buffer}function X(e,t){m.addAction(`${t}.${e}`)}function E_(e,t){if(X("login_web_passkey_prf_handle_called",t),(e==null?void 0:e.enabled)!==!0){X("login_web_passkey_prf_handle_return_prf_not_enabled",t);return}const n=e.results,a=n==null?void 0:n.first;if(a==null){m.addError(new Error("Passkey PRF extension was enabled but results.first was missing"),{message:"Passkey PRF extension was enabled but results.first was missing",hasPrfResults:!!n,entryPoint:t}),X("login_web_passkey_prf_handle_return_missing_first",t);return}X("login_web_passkey_prf_handle_return_success",t)}function Et(e,t){X("login_web_passkey_prf_filter_called",t);const n=e.clientExtensionResults,a=n==null?void 0:n.prf;if(!n)return X("login_web_passkey_prf_filter_return_no_client_extension_results",t),e;if(!a)return X("login_web_passkey_prf_filter_return_no_prf_extension",t),e;E_(a,t);const s={...n,prf:{...a,results:{...a.results??{},first:l_()}}};return X("login_web_passkey_prf_filter_return_sanitized",t),{...e,clientExtensionResults:s}}async function _u(e,...t){const n=await lr(...t);return Et(n,e)}const Vn="https://auth.openai.com/api/mfa";class _e extends Error{constructor(n,a,s,i,o,c,u){super(n);L(this,"message");L(this,"httpStatusCode",0);L(this,"httpStatusText","");L(this,"responseBody","");L(this,"url","");L(this,"httpRequestId","");L(this,"code","http-error");this.message=n,this.name="PasskeyEnrollmentAPIError",this.httpStatusCode=a,this.httpStatusText=s,this.responseBody=i,this.url=o,this.httpRequestId=c,this.code=u}}function d_(e){if(!e||typeof e!="object"||!("eval"in e))return!1;const t=e.eval;if(!t||typeof t!="object")return!1;const n=t.first;return typeof n=="string"||n instanceof ArrayBuffer}function xn(e){var n;const t=(n=e==null?void 0:e.extensions)==null?void 0:n.prf;return t&&d_(t)&&typeof t.eval.first=="string"&&(t.eval.first=Er(t.eval.first)),e}async function pe({response:e,url:t,operation:n}){const a=await e.text().catch(()=>{}),s=e.status===401?"session-expired":"http-error",i=e.headers.get("x-request-id")??void 0;throw new _e(`${n} failed (${e.status})`,e.status,e.statusText,a,t,i,s)}function cu(e){return e instanceof _e?{httpStatus:e.httpStatusCode??void 0,httpStatusText:e.httpStatusText??void 0,httpResponseBody:e.responseBody??void 0,httpRequestId:e.httpRequestId??void 0}:{}}async function uu(e){const t=`${Vn}/public/passkey/enrollment/start`,n=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:e})});n.ok||await pe({response:n,url:t,operation:"start"});const{passkey_creation_options:a}=await n.json();if(!a){const s=n.headers.get("x-request-id")??void 0;throw new _e("Server response missing creation options",n.status,n.statusText,void 0,t,s)}return xn(a)}async function lu({token:e,originAppName:t,passkeyCreationResponse:n}){const a=Et(n,"passkey_enrollment_creation"),s=`${Vn}/public/passkey/enrollment/finish`,i=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:e,origin_app_name:t,passkey_creation_response:a})});i.ok||await pe({response:i,url:s,operation:"finish"});const o=await i.json();if(!o.credential_id){const c=i.headers.get("x-request-id")??void 0;throw new _e("Server response missing credential_id",i.status,i.statusText,void 0,s,c)}return o}function Eu(e){return e==="API"?"/settings/profile?tab=security":e==="androidchat"||e==="ioschat"?"/settings/Security":"/#settings/Security"}function du(e,t){return t==="API"?e.searchParams.append("passkey_created",""):t==="androidchat"||t==="ioschat"?e.searchParams.set("message","passkey_created"):e.searchParams.set("_tm","passkey_created"),e}const ke="https://auth.openai.com/api/accounts";async function Su({signal:e}={}){const t=`${ke}/advanced-account-security/passkeys`,n=await fetch(t,{method:"GET",credentials:"include",signal:e,headers:{Accept:"application/json"}});return n.ok||await pe({response:n,url:t,operation:"get passkeys"}),(await n.json()).passkeys??[]}async function Cu({signal:e}={}){const t=`${ke}/advanced-account-security/passkey/enrollment/start`,n=await fetch(t,{method:"POST",credentials:"include",signal:e,headers:{Accept:"application/json"}}),a=n.headers.get("x-request-id")??void 0;n.ok||await pe({response:n,url:t,operation:"start"});const s=await n.json(),i=s.passkey_creation_options;if(!i)throw new _e("Server response missing creation options",n.status,n.statusText,void 0,t,a);if(!s.mfa_request_id)throw new _e("Server response missing mfa_request_id",n.status,n.statusText,void 0,t,a);return{passkeyCreationOptions:xn(i),mfaRequestId:s.mfa_request_id}}async function pu({mfaRequestId:e,passkeyCreationResponse:t,signal:n}){const a=`${ke}/advanced-account-security/passkey/enrollment/finish`,s=Et(t,"passkey_enrollment_creation"),i=await fetch(a,{method:"POST",credentials:"include",signal:n,headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({mfa_request_id:e,passkey_creation_response:s})}),o=i.headers.get("x-request-id")??void 0;i.ok||await pe({response:i,url:a,operation:"finish"});const c=await i.json();if(!c.credential_id)throw new _e("Server response missing credential_id",i.status,i.statusText,void 0,a,o);return{credentialId:c.credential_id}}async function gu({factorId:e,signal:t}){const n=`${ke}/advanced-account-security/passkey/delete`,a=await fetch(n,{method:"POST",credentials:"include",signal:t,headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({factor_id:e})});return a.ok||await pe({response:a,url:n,operation:"delete passkey"}),{message:(await a.json()).message??"Factor deleted"}}async function Au({getCreationOptions:e,finishEnrollment:t,isAutoAttempt:n,onFailure:a}){let s="fetched",i,o;try{const u=await e();i=u.creationOptions,s=u.creationOptionsSource,o=u.context}catch(u){return a({error:u,stage:"passkey_enrollment_start",creationOptionsSource:s,isAutoAttempt:n}),!1}let c;try{c=await dr(i)}catch(u){return a({error:u,stage:"passkey_enrollment_start",creationOptionsSource:s,isAutoAttempt:n}),!1}try{return await t({registrationResponse:c,context:o}),!0}catch(u){return a({error:u,stage:"passkey_enrollment_finish",creationOptionsSource:s,isAutoAttempt:n}),!1}}function ne(e,t,n){const a=se.useDynamicConfig(e),s=t.safeParse(a.value);return s.success?s.data:n}const S_="https://help.openai.com/en/articles/7039943-data-usage-for-consumer-services-faq",C_="https://help.openai.com/en/articles/10517909-korea-consent-form-information#h_46357d857c",p_="https://help.openai.com/en/articles/10517909-korea-consent-form-information#h_22484fb895",g_="https://help.openai.com/en/articles/10517909-korea-consent-form-information#h_46357d857c",A_="https://openai.com/policies/terms-of-use",h_="https://openai.com/policies/privacy-policy",f_="https://openai.com/ko-KR/policies/kr-privacy-policy-addendum",T_=r.object({help_center_url:r.string().optional(),overseas_transfer_consent_url:r.string().optional(),personal_info_consent_url:r.string().optional(),third_party_consent_url:r.string().optional()});function hu(){const e=ne("login_web_external_url_override",T_,{});return{helpCenterUrl:e.help_center_url??S_,overseasTransferConsentUrl:e.overseas_transfer_consent_url??C_,personalInfoConsentUrl:e.personal_info_consent_url??p_,thirdPartyConsentUrl:e.third_party_consent_url??g_,termsOfUseUrl:A_,privacyPolicyUrl:h_,koreaAddendumUrl:f_}}function O_(e){const t=e.formatMessage({id:"aboutYou.invalidName",defaultMessage:"Hmm, that doesn't look right. Try again?",description:"Error message shown when the name field in the 'About you' form is not a valid name."});return r.string({required_error:e.formatMessage({id:"aboutYou.nameRequired",defaultMessage:"Please enter name to continue",description:"Error message shown when the name field is empty on the about you page"})}).trim().max(96,t).regex(/^[\p{L}\p{M}\p{P}\s]+$/u,t)}function m_(e,t){const s=(t==null?void 0:t.enforceMinimumAge)??!1,i=new Date,o=new Date(i.getTime()-409968e7),c=e.formatMessage({id:"aboutYou.invalidBirthday",defaultMessage:"Hmm, that doesn't look right. Try again?",description:"Error message shown when the birthday field in the 'About you' form is not a valid date."}),u=e.formatMessage({id:"aboutYou.restrictedBirthdayError",defaultMessage:"We can't create an account with that info. Try again.",description:"Error message displayed when the provided birthday is too young (under 5 years)."});return r.string({required_error:e.formatMessage({id:"aboutYou.birthdayRequired",defaultMessage:"Please enter birthday to continue",description:"Error message shown when the birthday field is empty on the about you page"})}).date().superRefine((l,C)=>{const p=new Date(`${l}T00:00:00Z`);if(pi){C.addIssue({code:r.ZodIssueCode.too_big,type:"date",inclusive:!1,maximum:i.getTime(),message:c});return}if(!s)return;const E=new Date(i);E.setUTCFullYear(E.getUTCFullYear()-5),p>E&&C.addIssue({code:r.ZodIssueCode.too_big,type:"date",inclusive:!0,maximum:E.getTime(),message:u})})}function fu(e){const t=e.formatMessage({id:"tellUsAboutYou.requiredCheckboxErrorMessage",defaultMessage:"Please accept to continue",description:"Error message for required checkboxes"});return r.boolean({required_error:t}).refine(n=>n,{message:t})}function Tu(e,t){return r.object({origin_page_type:d.extract([d.enum.about_you]),data:r.object({name:O_(e),birthday:m_(e,t)})})}r.object({origin_page_type:d}).passthrough();class Ou extends Error{constructor(t){super(t)}}class mu extends Error{constructor(t){super(t)}}function I(e,t){return r.object({status:r.literal(e),data:r.object({error:r.object({code:r.literal(t)})})})}function Iu(e){return new Response(JSON.stringify({error:{code:e.shape.data.shape.error.shape.code.value}}),{status:e.shape.status.value,headers:{"Content-Type":"application/json"}})}const Nu=I(400,"bad_request"),Pu=I(400,"phone_number_in_use"),Lu=I(400,"phone_carrier_unknown"),bu=r.union([I(400,"invalid_phone_number"),I(400,"unhandled_provider_error")]),Ru=I(400,"voip_phone_disallowed"),yu=I(400,"landline_disallowed"),vu=I(400,"password_contains_user_info"),wu=I(400,"password_too_weak"),Uu=r.object({status:r.literal(400),data:r.object({error:r.object({code:r.literal("string_above_max_length"),param:r.literal("password")})})}),I_=I(401,"wrong_email_otp_code"),N_=I(401,"wrong_phone_otp_code"),Du=I(400,"invalid_input"),Wu=I(400,"fraud_guard"),Fu=I(400,"invalid_request");r.union([I_,N_]);const Mu=I(400,"max_check_attempts"),Gu=I(400,"rate_limit_exceeded"),Yu=I(400,"password_already_used"),ku=I(401,"invalid_username_or_password"),ju=I(400,"invalid_username"),Hu=I(401,"password_reset_required"),Vu=I(403,"incorrect_code"),xu=r.object({status:r.literal(429)}),Bu=r.object({status:r.literal(400)});I(409,"invalid_state");I(409,"preauth_cookie_invalid");const Bn=r.string().min(12),Ku=r.object({origin_page_type:r.literal(d.enum.add_password_new_password),data:r.object({password:Bn})}),$u=r.object({origin_page_type:d.extract([d.enum.choose_an_account]),intent:r.enum(["select","remove"]),session_id:r.string()}),P_=r.enum(["passwordless_signup_send_otp"]),qu=r.object({origin_page_type:d.extract([d.enum.create_account_password]),data:r.union([r.object({username:$,password:Bn}),r.object({intent:r.literal(P_.enum.passwordless_signup_send_otp)})])}),U=r.enum(["google","microsoft","apple","email","phone_number"]),L_=r.enum(["existing_user","new_user"]),Le=r.object({name:U,enabled:r.boolean(),tempDisabled:r.boolean(),kind:r.enum(["social","username"]),loginStrategy:L_}),zu={[U.enum.google]:"google-oauth2",[U.enum.microsoft]:"windowslive",[U.enum.apple]:"apple"},b_=r.object({kind:r.literal("username"),username:$}),R_=r.object({kind:r.literal("connection"),connection:U.exclude([U.enum.email,U.enum.phone_number])}),Ju=r.object({origin_page_type:d.extract([d.enum.create_account_start]),data:r.discriminatedUnion("kind",[b_,R_])}),Xu=r.object({origin_page_type:d.extract([d.enum.email_otp_send])});function y_(e){const t=r.object({intent:r.literal(Se.enum.validate),code:ut(e)}),n=r.object({intent:r.literal(Se.enum.resend)});return r.discriminatedUnion("intent",[t,n])}function Zu(e){return r.object({origin_page_type:d.extract([d.enum.email_otp_verification]),data:y_(e)})}const v_=r.object({kind:r.literal("username"),username:$}),w_=r.object({kind:r.literal("connection"),connection:U.exclude([U.enum.email,U.enum.phone_number])}),Qu=r.object({origin_page_type:d.extract([d.enum.login_or_signup_start]),data:r.discriminatedUnion("kind",[v_,w_])}),Kn=r.enum(["verify","try_another_way"]),U_=r.object({issued_at:r.number().int().nonnegative(),challenge:r.string().min(1),signature:r.string().min(1)}),D_=r.object({intent:r.literal(Kn.enum.verify),mfa_request_id:r.string(),passkey_challenge_response:r.unknown(),usernameless_passkey_challenge_data:U_.optional(),using_conditional_ui:r.boolean().optional()}),W_=r.object({intent:r.literal(Kn.enum.try_another_way),username:$.optional()}),F_=r.union([D_,W_]),el=r.object({origin_page_type:d.extract([d.enum.login_passkey]),data:F_}),Jt=r.enum(["validate","passwordless_login_send_otp","passkey"]),tl=r.object({origin_page_type:d.extract([d.enum.login_password]),data:r.discriminatedUnion("intent",[r.object({intent:r.literal(Jt.enum.validate),username:$,password:r.string()}),r.object({intent:r.literal(Jt.enum.passwordless_login_send_otp)})])}),M_=r.object({kind:r.literal("username"),username:$}),G_=r.object({kind:r.literal("connection"),connection:U.exclude([U.enum.email,U.enum.phone_number])}),nl=r.object({origin_page_type:d.extract([d.enum.login_start]),data:r.discriminatedUnion("kind",[M_,G_])}),Xt=r.enum(["validate","reissue"]),Y_=r.discriminatedUnion("intent",[r.object({intent:r.literal(Xt.enum.validate),factor_id:r.string(),factor_type:Y,code:r.string().nullable(),passkey_challenge_response:r.string().optional().nullable(),mfa_request_id:r.string().optional().nullable()}),r.object({intent:r.literal(Xt.enum.reissue),factor_id:r.string(),factor_type:Y,force_fresh_challenge:r.boolean(),mfa_request_id:r.string().optional().nullable()})]),rl=r.object({origin_page_type:d.extract([d.enum.mfa_challenge]),data:Y_}),k_=r.object({factor_id:r.string(),factor_type:Y,code:r.string()}),al=r.object({origin_page_type:d.extract([d.enum.mfa_enroll]),data:k_}),il=r.object({origin_page_type:d.extract([d.enum.phone_otp_send])}),Zt=r.enum(["validate","resend"]);function j_(e){const t=r.object({intent:r.literal(Zt.enum.validate),code:ut(e)}),n=r.object({intent:r.literal(Zt.enum.resend)});return r.discriminatedUnion("intent",[t,n])}function sl(e){return r.object({origin_page_type:d.extract([d.enum.phone_otp_verification]),data:j_(e)})}const Ze=r.enum(["finalize","resend_push_auth_challenge","send_email_otp"]),H_=r.discriminatedUnion("intent",[r.object({intent:r.literal(Ze.enum.finalize),mfa_session_id:r.string()}),r.object({intent:r.literal(Ze.enum.resend_push_auth_challenge),mfa_session_id:r.string()}),r.object({intent:r.literal(Ze.enum.send_email_otp)})]),ol=r.object({origin_page_type:d.extract([d.enum.push_auth_verification]),data:H_}),_l=r.object({origin_page_type:r.literal(d.enum.reset_password_new_password),data:r.object({password:r.string(),username:$.optional()})}),Qt=r.enum(["send_otp","passwordless_login_send_otp","back_to_login"]),cl=r.object({origin_page_type:r.literal(d.enum.reset_password_start),intent:Qt.default(Qt.enum.send_otp)}),V_=(e,t)=>t.formatMessage(nn({id:"form_validation.unexpected_message",description:`A message for an unexpected issue with a field in a form. We try to provide specific messages like "the value you entered isn't a valid date" but this is a backup in case there's an error we haven't specifically accounted for.`,defaultMessage:"Unexpected issue with this field: {errorMessage}"}),{errorMessage:e.message??JSON.stringify(e)});function ul(e){return t=>{const n=e(t);return a=>a.flatMap(s=>{const i=n.map(o=>o(s)).filter(o=>o!==null);return i.length>0?i:[V_(s,t)]})}}function ll(e,t,n){return a=>!Me(a.path,e)||!t(a)?null:typeof n=="function"?n(a):n}function El(e){const[t,n]=g.useState(!!e.status);return g.useEffect(()=>{e.status&&n(!0)},[e.status]),a=>{if(a.value!==void 0||e.status||t)return a.errors}}function dl(e){const t=g.useRef(!1);return(...n)=>{t.current||(e(...n),t.current=!0)}}function Sl(e,t){const[n,a]=ar(),s=e.safeParse(Object.fromEntries(n)),i=a;return[s.success?s.data:t,i]}function en(e){try{const t=new URL(e);return t.protocol==="openai:"||["chatgpt.com","openai.com","chatgpt-staging.com","openai.org"].includes(t.hostname)}catch{return!1}}const Cl=r.object({email:r.string().default(""),phone_number:r.string().default(""),login_url:r.string().default("").transform(e=>en(e)?e:""),no_auth_url:r.string().default("").transform(e=>en(e)?e:""),app_name_enum:r.string().default("")}),x_=e=>({message:nn({id:"form_validation.unexpected_message",description:`A message for an unexpected issue with a field in a form. We try to provide specific messages like "the value you entered isn't a valid date" but this is a backup in case there's an error we haven't specifically accounted for.`,defaultMessage:"Unexpected issue with this field: {errorMessage}"}),values:{errorMessage:e.message??JSON.stringify(e)}});function pl(...e){return t=>t.flatMap(n=>{const a=e.map(s=>s(n)).filter(s=>s!==null);return a.length>0?a:[x_(n)]})}function gl(e,t,n){return a=>!Me(a.path,e)||!t(a)?null:typeof n=="function"?n(a):{message:n}}function Al(e){return e.code===r.ZodIssueCode.invalid_type&&(e.received===r.ZodParsedType.null||e.received===r.ZodParsedType.undefined)}function B_(){const e=ur();return t=>e.formatMessage(t.message,t.values)}function hl(e){const t=B_(),[n,a]=g.useState(!!e.status);return g.useEffect(()=>{e.status&&a(!0)},[e.status]),s=>{var i;if(s.value!==void 0||e.status||n)return(i=s.errors)==null?void 0:i.map(t)}}function fl(e){try{return JSON.parse(e)}catch{return!1}}function Tl(e){return r.string({required_error:e.formatMessage({id:"logIn.phoneRequired",defaultMessage:"Phone number is required.",description:"Error message shown when the phone number field is empty on the create account page"})}).refine(t=>pr(t),e.formatMessage({id:"logIn.invalidPhoneFormat",defaultMessage:"Phone number is not valid.",description:"Error message shown when the phone number format is invalid on the create account page"}))}const K_=["google","apple","microsoft","email","phone_number"],$_=r.object({options:U.array()});function q_({loginStrategy:e}){return ne(`login_web_${e}_login_option_google`,Le,{name:"google",enabled:!0,tempDisabled:!1,kind:"social",loginStrategy:e})}function z_({loginStrategy:e}){return ne(`login_web_${e}_login_option_microsoft`,Le,{name:"microsoft",enabled:!0,tempDisabled:!1,kind:"social",loginStrategy:e})}function J_({loginStrategy:e}){return ne(`login_web_${e}_login_option_apple`,Le,{name:"apple",enabled:!0,tempDisabled:!1,kind:"social",loginStrategy:e})}function X_({loginStrategy:e}){return ne(`login_web_${e}_login_option_email`,Le,{name:"email",enabled:!0,tempDisabled:!1,kind:"username",loginStrategy:e})}function Z_({loginStrategy:e}){const t=ne(`login_web_${e}_login_option_phone_number`,Le,{name:"phone_number",enabled:e==="existing_user",tempDisabled:!1,kind:"username",loginStrategy:e}),n=se.useLayer("chatgpt_phone_signup_layer"),a=se.useGateValue("chatgpt-signup-allow-phone");if(e!=="new_user")return t;const s=n.get("signup_allow_phone_from_login_web",!1),i=n.get("in_phone_signup_holdout_from_login_web",!1),o=a||s,c=t.enabled;return{...t,enabled:!i&&c&&o}}function Ol({loginStrategy:e}){const t=q_({loginStrategy:e}),n=z_({loginStrategy:e}),a=J_({loginStrategy:e}),s=X_({loginStrategy:e}),i=Z_({loginStrategy:e}),{options:o}=ne("web_login_options_list",$_,{options:K_}),u=[t,n,a,s,i].filter(l=>l&&l.enabled&&o.includes(l.name)).sort((l,C)=>o.indexOf(l.name)-o.indexOf(C.name));return{googleOption:t,microsoftOption:n,appleOption:a,emailOption:s,phoneNumberOption:i,optionsForDisplay:u}}const ml="team-1-month-free";function Q_(){const e=se.useGateValue("simplified_auth_screen_var3_feature_launch_gate"),t=se.useLayer("auth_login_signup_holdout_layer").get("is_in_sso_up_holdout",!1);return!e||t?ec:"google_and_others_and_username"}function Il(){const t=Q_()==="google_and_others_and_username",n=se.useLayer("auth_login_signup_holdout_layer").get("is_use_phone_a_link",!1);return{isSocialAuthPrioritized:t,isUsePhoneALink:n}}const ec="username_and_socials",Nl=()=>{const{signup_source:e,promo:t}=Ao();return e!=="business"?{enabled:!1,promo:""}:{enabled:!0,promo:t}},tc=r.object({redirect_url:r.string().default("")});function Pl(){const{redirect_url:e}=ne("signup-waitlist-urls",tc,{redirect_url:""});return e||null}function Ll(e,t){return e.formatMessage({id:"thirdPartyLoginSubtitle",defaultMessage:`{app, select, - whatsapp {to link your account to WhatsApp} - other {to link your account} - }`,description:"Subtitle on a page where the user can log into an account or create a new one that shows an external app that the user will connect their account to. The page this is shown on could be a login page with a title like 'Welcome back' or an account creation page with a title like 'Create an account'."},{app:t})}const bl=()=>{const e=[W.dotFaint,W.dotMedium,W.dotSolid],t=[W.dotSolid,W.dotMedium,W.dotFaint];return j.jsxs("div",{className:W.decoration,"aria-hidden":"true",children:[j.jsx("div",{className:W.dotGroup,children:e.map((n,a)=>j.jsx("span",{className:`${W.dot} ${n}`},`left-${a}`))}),j.jsx("div",{className:W.iconCircle,children:j.jsx(_r,{className:W.passkeyIcon})}),j.jsx("div",{className:W.dotGroup,children:t.map((n,a)=>j.jsx("span",{className:`${W.dot} ${n}`},`right-${a}`))})]})};function Rl(e){return r.string({required_error:e.formatMessage({id:"login.emailRequired",defaultMessage:"Email is required.",description:"Error message shown when the email field is empty on the create account page"})}).email(e.formatMessage({id:"logIn.invalidEmailFormat",defaultMessage:"Email is not valid.",description:"Error message shown when the email format is invalid on the create account page"}))}function yl(e,t){return typeof(e==null?void 0:e.mfa_request_id)!="string"||e.mfa_request_id!==t?{challengeMode:null,matchNumber:null}:{challengeMode:typeof e.challenge_mode=="string"?e.challenge_mode:null,matchNumber:typeof e.match_number=="string"?e.match_number:null}}class vl{static async fetchTrustedDevicesOnLogin(){try{const n=await fetch("https://auth.openai.com/api/accounts/push-auth/devices/on-login",{method:"GET",headers:{Accept:"application/json"},credentials:"include"});return n.ok?(await n.json()).devices??[]:[]}catch{throw new Error("Failed to fetch trusted devices for push auth verification")}}}async function wl(e,t){const n=btoa(JSON.stringify({kind:"AuthApiFailure",errorCode:t}));let a;try{a=k().immutableClientSessionMetadata.session_id}catch{}const s=new URLSearchParams({payload:n,...a?{session_id:a}:{}});e(`/error?${s.toString()}`,{replace:!0})}function Ul(e,t){const n=new URLSearchParams({error:t});e(`/mfa-challenge?${n.toString()}`,{replace:!0})}const nc=r.object({id:r.string(),public_id:r.string().nullish(),title:r.string().nullish()}),rc=r.object({id:r.string(),name:r.string().nullish(),title:r.string().nullish(),profile_picture_url:r.string().nullish(),profile_picture_alt_text:r.string().nullish(),projects:r.array(nc).optional(),personal:r.boolean().default(!1),completed_platform_onboarding:r.boolean().default(!1).nullish()});function Dl(e){sessionStorage.setItem("organizations",JSON.stringify(e))}function Wl(){const e=r.array(rc).safeParse(JSON.parse(sessionStorage.getItem("organizations")??"[]"));return e.success?(e.data.forEach(t=>{t.projects&&t.projects.sort((n,a)=>{const s=n.title==="Sky",i=a.title==="Sky";return s&&!i?-1:!s&&i?1:0})}),e.data):[]}const $n="missing_error_page_verifier_session",ac=r.object({code:r.literal($n),reason:r.enum(["missing_query_verifier_id","missing_cookie_verifier_id","verifier_id_mismatch"])});function Fl(e){throw ir({code:$n,reason:e},{status:400,statusText:"Invalid Session"})}function Ml(e){return sr(e)?ac.safeParse(e.data).success:!1}const ic=r.object({id:r.string()}),sc="error_page_verifier";function Gl(e){const t=vn(e,sc);if(!t)return{is_missing_verifier:!0};try{const n=In(t,{header:!0}),a=ic.safeParse(n);if(a.success)return{verifier_id:a.data.id,is_missing_verifier:!1}}catch{return{is_missing_verifier:!0}}return{is_missing_verifier:!0}}function oc(e,t){if(!e){m.addAction("login_web_missing_client_auth_session_payload_in_navigation",{path:t});return}if(typeof e!="object"||Array.isArray(e)){m.addAction("login_web_invalid_client_auth_session_payload_in_navigation",{path:t});return}return e["oai-client-auth-session"]}async function _c(e,t){const{clientAuthSessionCache:n}=k(),a=oc(e,t);if(a===void 0)return;const s=Ye.safeParse(a);if(!s.success)return;const i=ct(document.cookie);if(!i){m.addAction("login_web_missing_client_auth_session_minimized_checksum",{navigationPath:t});return}m.addAction("login_web_writing_client_auth_session_to_cache",{path:t}),n.write({checksum:i,sessionId:typeof s.data.session_id=="string"?s.data.session_id:null,session:s.data})}const cc=Object.freeze(Object.defineProperty({__proto__:null,maybeWriteClientAuthSessionCacheFromNavigationJson:_c},Symbol.toStringTag,{value:"Module"}));export{Cl as $,Us as A,Lo as B,go as C,Gc as D,S as E,Hn as F,k as G,zc as H,Dc as I,Uc as J,Fc as K,qc as L,Oe as M,Vc as N,Mc as O,_e as P,ou as Q,_ as R,Hc as S,i_ as T,ee as U,wc as V,Su as W,Cu as X,pu as Y,gu as Z,kc as _,eu as a,no as a$,hl as a0,pl as a1,gl as a2,Al as a3,El as a4,Tl as a5,d as a6,Ao as a7,Bo as a8,Pc as a9,_u as aA,Kn as aB,bl as aC,Rl as aD,zu as aE,Eu as aF,vl as aG,yl as aH,wl as aI,Qt as aJ,Wu as aK,Yu as aL,vu as aM,wu as aN,Me as aO,rc as aP,Dl as aQ,Wl as aR,Tc as aS,Nl as aT,ml as aU,hu as aV,ot as aW,iu as aX,au as aY,su as aZ,vc as a_,$t as aa,Se as ab,Mu as ac,Nc as ad,t_ as ae,Jc as af,Xc as ag,Bn as ah,P_ as ai,F as aj,Rc as ak,Ko as al,q as am,y_ as an,j_ as ao,Zt as ap,ru as aq,nu as ar,U as as,Pl as at,Il as au,Ll as av,ul as aw,Yc as ax,ll as ay,Jt as az,Wc as b,Bc as b0,Zc as b1,ks as b2,A as b3,O_ as b4,m_ as b5,fu as b6,fl as b7,ro as b8,$ as b9,ju as bA,Xu as bB,Zu as bC,Qu as bD,xu as bE,el as bF,tl as bG,ku as bH,Hu as bI,nl as bJ,rl as bK,mu as bL,Vu as bM,al as bN,il as bO,sl as bP,ol as bQ,_l as bR,cl as bS,bc as bT,Ul as ba,Xt as bb,Gl as bc,Ge as bd,Fl as be,Ml as bf,Tu as bg,Ou as bh,jc as bi,Ku as bj,$u as bk,Kc as bl,Iu as bm,Du as bn,Gu as bo,qu as bp,Fu as bq,Nu as br,Pu as bs,Ru as bt,yu as bu,Lu as bv,bu as bw,Uu as bx,Bu as by,Ju as bz,tu as c,$c as d,mc as e,Ic as f,Ol as g,Lc as h,Oc as i,dl as j,fe as k,ne as l,G as m,et as n,Sl as o,cu as p,lu as q,Au as r,uu as s,du as t,Qc as u,In as v,m as w,xc as x,yc as y,J as z}; -//# sourceMappingURL=app-core-BVP192sF.js.map diff --git a/tmp_redirectToPage.js b/tmp_redirectToPage.js deleted file mode 100644 index d1190497..00000000 --- a/tmp_redirectToPage.js +++ /dev/null @@ -1,2 +0,0 @@ -import{e as z,r as Y,b as K,m as c}from"./react-router-Dv7ITjcY.js";import{bg as Q,bh as f,bi as l,R as h,a6 as s,bj as X,aL as L,aM as I,aN as T,bk as Z,bl as x,ab as V,ac as v,bm as j,aK as P,bn as D,bo as F,bp as ee,ai as se,bq as b,br as re,bs as ae,bt as te,bu as ne,bv as oe,bw as ie,bx as de,by as J,bz as ue,bA as O,U as E,aE as A,bB as ce,bC as pe,bD as le,bE as M,bF as me,aB as fe,bG as he,az as ge,bH as we,bI as _e,bJ as ye,bK as Pe,S as N,bL as R,bb as Ee,bM as W,bN as ve,bO as be,bP as Oe,ap as Me,bQ as Se,bR as Re,bS as Ie,aJ as C,h as G}from"./app-core-BVP192sF.js";import{z as _}from"./zod-BeXw7Xwl.js";const Te=_.object({status:_.literal(400),json:_.object({error:_.object({code:_.literal("registration_disallowed")})})}),Ae=_.object({status:_.literal(400),json:_.object({error:_.object({code:_.literal("registration_disallowed_too_young")})})}),Ne=(r,t,e)=>{const a=Q(e.intl).safeParse(t);if(!a.success)throw new f(a.error.message);return l(r,"/create_account",{...e.init,method:"POST",body:JSON.stringify({name:a.data.data.name,birthdate:a.data.data.birthday})},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.ABOUT_YOU,intercept:i=>{if(Te.safeParse(i).success)return{type:s.enum.about_you,payload:{errors:{formErrors:[e.intl.formatMessage({id:"aboutYou.cantCreateYourAccount",defaultMessage:"We can't create your account due to our Terms of Use",description:"Error message displayed when the user is not allowed to create an account for a generic reason."})]}}};if(Ae.safeParse(i).success)return{type:s.enum.about_you,payload:{errors:{formErrors:[e.intl.formatMessage({id:"aboutYou.restrictedBirthdayError",defaultMessage:"We can't create an account with that info. Try again.",description:"Error message displayed when the provided birthday is too young (under 5 years)."})]}}}}})},Ue=(r,t,e)=>{const a=X.safeParse(t);if(!a.success)throw new f(a.error.message);const{password:i}=a.data.data;return l(r,"/password/add",{...e.init,method:"POST",body:JSON.stringify({password:i})},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.ADD_PASSWORD_NEW_PASSWORD,intercept:n=>{const d={status:n.status,data:n.json};if(L.safeParse(d).success)return{type:s.enum.add_password_new_password,payload:{errors:{fieldErrors:{password:[e.intl.formatMessage({id:"resetPasswordNewPassword.passwordMustNotBeReused",defaultMessage:"Password must not be reused",description:"Error message shown when a new password is reused from the previous password"})]}}}};if(I.safeParse(d).success)return{type:s.enum.add_password_new_password,payload:{errors:{fieldErrors:{password:[e.intl.formatMessage({id:"resetPasswordNewPassword.passwordContainsUserInfo",defaultMessage:"Password must not contain personal information like your email, phone number, or name",description:"Error message displayed when the user tries to reset their password with a password that contains user information"})]}}}};if(T.safeParse(d).success)return{type:s.enum.add_password_new_password,payload:{errors:{fieldErrors:{password:[e.intl.formatMessage({id:"resetPasswordNewPassword.passwordTooWeak",defaultMessage:"Password is too weak",description:"Error message displayed when the user tries to reset their password with a password that is too weak"})]}}}}}})},ke=(r,t,e)=>{const a=Z.safeParse(t);if(!a.success)throw new f(a.error.message);const{path:i,method:n}=a.data.intent==="select"?{path:"/session/select",method:"POST"}:{path:"/session/remove",method:"DELETE"};return l(r,i,{...e.init,method:n,body:JSON.stringify({session_id:a.data.session_id})},{routeId:h.CHOOSE_AN_ACCOUNT})},Be=async(r,t,{intl:e,init:a,authapiBaseUrlOverride:i})=>{var p;const n=x(e).safeParse(t);if(!n.success)throw new f(n.error.message);const d=n.data.data.username_kind==="email"?"/email-otp":"/phone-otp";if(n.data.data.intent===V.enum.validate)return l(r,`${d}/validate`,{...a,method:"POST",body:JSON.stringify({code:n.data.data.code})},{authapiBaseUrlOverride:i,routeId:h.CONTACT_VERIFICATION,intercept:({status:m,json:w})=>{if(v.safeParse({status:m,data:w}).success)throw j(v);if(P.safeParse({status:m,data:w}).success)return{type:s.enum.contact_verification,payload:{errors:{formErrors:[e.formatMessage({id:"contactVerification.fraudGuard",description:"Error message displayed when a phone number fails fraud guard checks.",defaultMessage:"Unable to send a text message to this phone number"})]}}};if(m===401||D.safeParse({status:m,data:w}).success)return{type:s.enum.contact_verification,payload:{errors:{fieldErrors:{code:[e.formatMessage({id:"contactVerification.incorrectCode",description:"Error message displayed when the user enters an incorrect OTP code.",defaultMessage:"Incorrect code"})]}}}}}});const o=await fetch(`${i??"https://auth.openai.com/api/accounts"}${d}/resend`,{...a,method:"POST",credentials:"include",signal:r.signal});if(o.ok)return{page:{type:s.enum.contact_verification},responseHeaders:o.headers};const u=(p=o.headers.get("Content-Type"))!=null&&p.includes("application/json")?await o.clone().json():null;if(F.safeParse({status:o.status,data:u}).success)return{page:{type:s.enum.contact_verification,payload:{errors:{formErrors:[e.formatMessage({id:"contactVerification.tooManyResentOtps",defaultMessage:"Tried to resend too many times. Please try again later.",description:"Error message for too many attempts"})]}}},responseHeaders:o.headers};if(n.data.data.username_kind==="phone_number"&&P.safeParse({status:o.status,data:u}).success)return{page:{type:s.enum.contact_verification,payload:{errors:{formErrors:[e.formatMessage({id:"contactVerification.fraudGuard",description:"Error message displayed when a phone number fails fraud guard checks.",defaultMessage:"Unable to send a text message to this phone number"})]}}},responseHeaders:o.headers};throw o},Ce=(r,t,{intl:e,init:a,authapiBaseUrlOverride:i})=>{const n=ee.safeParse(t);if(!n.success)throw new f(n.error.message);if("intent"in n.data.data&&n.data.data.intent===se.enum.passwordless_signup_send_otp)return l(r,"/passwordless/send-otp",{...a},{authapiBaseUrlOverride:i,routeId:h.CREATE_ACCOUNT_PASSWORD,intercept:o=>{const u={status:o.status,data:o.json};if(b.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{formErrors:[e.formatMessage({id:"createAccountPassword.passwordlessSignupSendOtp.InvalidRequest",defaultMessage:"We couldn’t send you a one-time code. Please try again or create account with a password.",description:"Shown when the create account password page cannot send a one-time code due to an invalid request"})]}}}}});const d=n.data.data;return l(r,"/user/register",{...a,body:JSON.stringify({password:d.password,username:d.username.value})},{authapiBaseUrlOverride:i,routeId:h.CREATE_ACCOUNT_PASSWORD,intercept:o=>{const u={status:o.status,data:o.json},p=d.username.kind==="email"?e.formatMessage({id:"createAccountPassword.emailAlreadyInUse",defaultMessage:"An account for this email address already exists",description:"Error message displayed when the user tries to create an account with a username that is already in use."}):e.formatMessage({id:"createAccountPassword.phoneAlreadyInUse",defaultMessage:"An account for this phone number already exists",description:"Error message displayed when the user tries to create an account with a username that is already in use."});if(re.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{formErrors:[p]}}};if(ae.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{formErrors:[p]}}};if(te.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{formErrors:[e.formatMessage({id:"createAccountPassword.voipPhoneNumberDisallowed",defaultMessage:"It looks like this is a virtual phone number (also known as VoIP). Please provide a valid, non-virtual phone number to continue.",description:"Error message displayed when the user tries to create an account with a VoIP (virtual phone number) and it's not allowed, asking them to use a valid phone number."})]}}};if(ne.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{formErrors:[e.formatMessage({id:"createAccountPassword.landlineDisallowed",defaultMessage:"The phone number you entered does not appear to be a mobile number. Please provide a valid mobile phone number that can receive SMS messages to continue.",description:"Error message displayed when the user tries to create an account with a non-mobile phone number, asking them to use a valid mobile phone number that can receive SMS message."})]}}};if(oe.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{formErrors:[e.formatMessage({id:"createAccountPassword.unsupportedPhoneNumber",defaultMessage:"The phone number you entered is not supported, please try a different one.",description:"Error message displayed when the user tries to create an account with an unsupported phone number."})]}}};if(ie.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{formErrors:[e.formatMessage({id:"createAccountPassword.invalidPhoneNumber",defaultMessage:"The phone number you entered is invalid. Please check and try again.",description:"Error message displayed when the user tries to create an account with an invalid phone number."})]}}};if(I.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{fieldErrors:{password:[e.formatMessage({id:"createAccountPassword.passwordContainsUserInfo",defaultMessage:"Password must not contain personal information like your email, phone number, or name",description:"Error message displayed when the user tries to create an account with a password that contains user information"})]}}}};if(T.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{fieldErrors:{password:[e.formatMessage({id:"createAccountPassword.passwordTooWeak",defaultMessage:"Password is too weak",description:"Error message displayed when the user tries to create an account with a password that is too weak"})]}}}};if(de.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{fieldErrors:{password:[e.formatMessage({id:"createAccountPassword.passwordTooLong",defaultMessage:"Password must be no more than 512 characters",description:"Error message displayed when the user tries to create an account with a password that exceeds the maximum allowed length"})]}}}};if(J.safeParse(u).success)return{type:s.enum.create_account_password,payload:{errors:{formErrors:[e.formatMessage({id:"createAccountPassword.genericBadRequest",defaultMessage:"Failed to create account. Please try again",description:"Generic error message when account creation fails with a generic bad request error without a specific error code"})]}}}}})},He=(r,t,e)=>{const a=ue.safeParse(t);if(!a.success)throw new f(a.error.message);const i=qe(a.data);return l(r,"/authorize/continue",{...e.init,body:JSON.stringify(i)},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.CREATE_ACCOUNT,intercept:n=>{const d={status:n.status,data:n.json};if(O.safeParse(d).success&&a.data.data.kind==="username"){const o=a.data.data.username.kind===E.enum.phone_number;return{type:s.enum.create_account_start,payload:{errors:{fieldErrors:{username:[e.intl.formatMessage(o?{id:"createAccountStart.invalidPhoneNumber",defaultMessage:"Phone number is not valid.",description:"Error message shown when the server reports an invalid phone number during create account start."}:{id:"createAccountStart.invalidEmail",defaultMessage:"Email is not valid.",description:"Error message shown when the server reports an invalid email during create account start."})]}}}}}}})};function qe({data:r}){return r.kind==="username"?{username:r.username,screen_hint:"signup"}:{connection:A[r.connection]}}const Le=(r,t,{init:e,authapiBaseUrlOverride:a})=>{const i=ce.safeParse(t);if(!i.success)throw new f(i.error.message);return l(r,"/email-otp/send",{...e,method:"GET"},{authapiBaseUrlOverride:a})},Ve=async(r,t,{intl:e,init:a,authapiBaseUrlOverride:i})=>{const n=pe(e).safeParse(t);if(!n.success)throw new f(n.error.message);const d="/email-otp";if(n.data.data.intent===V.enum.validate)return l(r,`${d}/validate`,{...a,method:"POST",body:JSON.stringify({code:n.data.data.code})},{authapiBaseUrlOverride:i,routeId:h.EMAIL_VERIFICATION,intercept:({status:u})=>{if(u===429)return{type:s.enum.email_otp_verification,payload:{errors:{formErrors:[e.formatMessage({id:"emailVerification.tooManyValidateOtps",defaultMessage:"Too many attempts. Please try again later.",description:"Error message when the user tries to validate the OTP code too many times"})]}}};if(u===401)return{type:s.enum.email_otp_verification,payload:{errors:{fieldErrors:{code:[e.formatMessage({id:"emailVerification.incorrectCode",description:"Error message displayed when the user enters an incorrect OTP code.",defaultMessage:"Incorrect code"})]}}}}}});const o=await fetch(`${i??"https://auth.openai.com/api/accounts"}${d}/resend`,{...a,method:"POST",credentials:"include",signal:r.signal});if(o.ok)return{page:{type:s.enum.email_otp_verification},responseHeaders:o.headers};if(o.status===429)return{page:{type:s.enum.email_otp_verification,payload:{errors:{formErrors:[e.formatMessage({id:"emailVerification.tooManyResentOtps",defaultMessage:"Too many attempts. Please try again later.",description:"Error message when the user tries to resend the OTP code too many times"})]}}},responseHeaders:o.headers};throw o},je=(r,t,e)=>{const a=le.safeParse(t);if(!a.success)throw new f(a.error.message);const i=De(a.data);return l(r,"/authorize/continue",{...e.init,body:JSON.stringify(i)},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.UNIFIED_LOG_IN_OR_SIGN_UP_INPUT,intercept:n=>{const d={status:n.status,data:n.json};if(O.safeParse(d).success&&a.data.data.kind==="username"){const o=a.data.data.username.kind===E.enum.phone_number;return{type:s.enum.login_or_signup_start,payload:{errors:{fieldErrors:{username:[e.intl.formatMessage(o?{id:"loginOrSignupStart.invalidPhoneNumber",defaultMessage:"Phone number is not valid.",description:"Error message shown when the server reports an invalid phone number during login-or-signup start."}:{id:"loginOrSignupStart.invalidEmail",defaultMessage:"Email is not valid.",description:"Error message shown when the server reports an invalid email during login-or-signup start."})]}}}}}if(M.safeParse(d).success)return{type:s.enum.login_or_signup_start,payload:{errors:{formErrors:[e.intl.formatMessage({id:"loginOrSignupStart.tooManyRequests",defaultMessage:"Rate limit exceeded, please try again later",description:"Error message shown when the user is rate limited because they have made too many requests during login-or-signup start."})]}}}}})};function De({data:r}){return r.kind==="username"?{username:r.username,screen_hint:"login_or_signup"}:{connection:A[r.connection]}}const Fe=(r,t,e)=>{const a=me.safeParse(t);if(!a.success)throw new f(a.error.message);if(a.data.data.intent===fe.enum.try_another_way){const{username:u}=a.data.data;return l(r,"/authorize/continue",{...e.init,body:JSON.stringify({...u?{username:u,screen_hint:"login_or_signup"}:{},from_login_screen:"passkey"})},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.LOG_IN_PASSKEY,intercept:p=>{const m={status:p.status,data:p.json};if(O.safeParse(m).success&&u){const w=u.kind===E.enum.phone_number;return{type:s.enum.login_or_signup_start,payload:{errors:{fieldErrors:{username:[e.intl.formatMessage(w?{id:"loginOrSignupStart.invalidPhoneNumber",defaultMessage:"Phone number is not valid.",description:"Error message shown when the server reports an invalid phone number during login-or-signup start."}:{id:"loginOrSignupStart.invalidEmail",defaultMessage:"Email is not valid.",description:"Error message shown when the server reports an invalid email during login-or-signup start."})]}}}}}if(M.safeParse(m).success)return{type:s.enum.login_or_signup_start,payload:{errors:{formErrors:[e.intl.formatMessage({id:"loginOrSignupStart.tooManyRequests",defaultMessage:"Rate limit exceeded, please try again later",description:"Error message shown when the user is rate limited because they have made too many requests during login-or-signup start."})]}}}}})}const{mfa_request_id:i,passkey_challenge_response:n,usernameless_passkey_challenge_data:d,using_conditional_ui:o}=a.data.data;return l(r,"/passkey/verify",{...e.init,method:"POST",body:JSON.stringify({mfa_request_id:i,passkey_challenge_response:n,...d!==void 0?{usernameless_passkey_challenge_data:d}:{},...o!==void 0?{using_conditional_ui:o}:{}})},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.LOG_IN_PASSKEY})},Je=(r,t,e)=>{const a=he.safeParse(t);if(!a.success)throw new f(a.error.message);if(a.data.data.intent===ge.enum.passwordless_login_send_otp)return l(r,"/passwordless/send-otp",{...e.init},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.LOG_IN_PASSWORD,intercept:d=>{const o={status:d.status,data:d.json};if(b.safeParse(o).success)return{type:s.enum.login_password,payload:{errors:{formErrors:[e.intl.formatMessage({id:"loginPassword.passwordlessLoginSendOtp.InvalidRequest",defaultMessage:"We couldn’t send you a one-time code. Please try again or continue with your password.",description:"Shown when the login password page cannot send a one-time code due to an invalid request"})]}}}}});const{username:i,password:n}=a.data.data;return l(r,"/password/verify",{...e.init,method:"POST",body:JSON.stringify({password:n})},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.LOG_IN_PASSWORD,intercept:d=>{const o={status:d.status,data:d.json};if(we.safeParse(o).success){const u=(i==null?void 0:i.kind)===E.enum.phone_number;return{type:s.enum.login_password,payload:{errors:{fieldErrors:{password:[e.intl.formatMessage(u?{id:"logInPassword.incorrectPhonePassword",defaultMessage:"Incorrect phone number or password",description:"Error message shown when a user enters an incorrect password for phone-password login."}:{id:"logInPassword.incorrectEmailPassword",defaultMessage:"Incorrect email address or password",description:"Error message shown when a user enters an incorrect password for email-password login."})]}}}}}if(_e.safeParse(o).success)return{type:s.enum.login_password,payload:{errors:{fieldErrors:{password:[e.intl.formatMessage({id:"logInPassword.passwordResetRequired",defaultMessage:"For improved account protection, please change your password and then log in again",description:"Error message shown when the user must reset their password before logging in."})]}}}}}})},We=(r,t,e)=>{const a=ye.safeParse(t);if(!a.success)throw new f(a.error.message);const i=Ge(a.data);return l(r,"/authorize/continue",{...e.init,method:"POST",body:JSON.stringify(i)},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.LOG_IN,intercept:n=>{const d={status:n.status,data:n.json};if(O.safeParse(d).success&&a.data.data.kind==="username"){const o=a.data.data.username.kind===E.enum.phone_number;return{type:s.enum.login_start,payload:{errors:{fieldErrors:{username:[e.intl.formatMessage(o?{id:"loginStart.invalidPhoneNumber",defaultMessage:"Phone number is not valid.",description:"Error message shown when the server reports an invalid phone number during login start."}:{id:"loginStart.invalidEmail",defaultMessage:"Email is not valid.",description:"Error message shown when the server reports an invalid email during login start."})]}}}}}}})};function Ge({data:r}){return r.kind==="username"?{username:r.username}:{connection:A[r.connection]}}const $e=async(r,t,e)=>{var U;const a=Pe.safeParse(t);if(!a.success)throw new f(a.error.message);const{intent:i,factor_id:n,factor_type:d}=a.data.data,{mfa_factors:o,passkey_challenge_option:u}=await N(r);if(!o)throw new R("MFA factors not found");const p=o.find(y=>y.id===n);if(!p)throw new R("MFA factor not found");if(i===Ee.enum.validate){const y={id:n,type:d,code:a.data.data.code};if(p.factor_type!=="passkey"&&y.code==null)throw new f("Missing verification code");const k=a.data.data.passkey_challenge_response;if(k)try{y.passkey_challenge_response=JSON.parse(k)}catch(S){throw console.error("Invalid passkey challenge response payload",S),new f("Invalid passkey challenge response payload")}else if(p.factor_type==="passkey")throw new f("Missing passkey challenge response");return y.mfa_request_id=a.data.data.mfa_request_id??H(p,u),l(r,"/mfa/verify",{...e.init,method:"POST",body:JSON.stringify(y)},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.MFA_CHALLENGE_METHOD,intercept:({status:S,json:$})=>{const B={status:S,data:$};if(W.safeParse(B).success)return{type:s.enum.mfa_challenge,payload:{factor_id:n,factors:o,errors:{fieldErrors:{code:[e.intl.formatMessage({id:"mfaChallenge.incorrectCode",defaultMessage:"Incorrect code. Please try again.",description:"Error message displayed when the user enters an incorrect code."})]}}}};if(M.safeParse(B).success)return{type:s.enum.mfa_challenge,payload:{factor_id:n,factors:o,errors:{fieldErrors:{code:[e.intl.formatMessage({id:"mfaChallenge.tooManyRequests",defaultMessage:"Too many requests. Please try again later.",description:"Error message displayed when the user has made too many requests."})]}}}}}})}const m=new Headers((U=e.init)==null?void 0:U.headers);m.set("Content-Type","application/json"),m.set("Accept","application/json");const w=await fetch(`${e.authapiBaseUrlOverride??"https://auth.openai.com/api/accounts"}/mfa/issue_challenge`,{...e.init,credentials:"include",method:"POST",signal:r.signal,headers:m,body:JSON.stringify({id:n,type:d,force_fresh_challenge:a.data.data.force_fresh_challenge,mfa_request_id:a.data.data.mfa_request_id??H(p,u)})});if(w.ok)return{page:{type:s.enum.mfa_challenge,payload:{factor_id:n,factors:o}},responseHeaders:w.headers};if(w.status===429)return{page:{type:s.enum.mfa_challenge,payload:{factor_id:n,factors:o,errors:{fieldErrors:{code:[e.intl.formatMessage({id:"mfaChallenge.tooManyRequests",defaultMessage:"Too many requests. Please try again later.",description:"Error message displayed when the user has made too many requests."})]}}}},responseHeaders:w.headers};throw w};function H(r,t){var e;if(r.factor_type==="push_auth")return((e=r.metadata)==null?void 0:e.mfa_request_id)??void 0;if(r.factor_type==="passkey")return t==null?void 0:t.mfa_request_id}async function q(r){const{mfa_enrollment_factors:t}=await N(r);if(!t)throw new R("MFA enrollment factors not found");return t}const ze=async(r,t,e)=>{const a=ve.safeParse(t);if(!a.success)throw new f(a.error.message);const{factor_id:i,factor_type:n,code:d}=a.data.data;return l(r,"/mfa/activate",{...e.init,method:"POST",body:JSON.stringify({id:i,type:n,code:d})},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.MFA_ENROLL_METHOD,intercept:async({status:o,json:u})=>{const p={status:o,data:u};if(W.safeParse(p).success){const m=await q(r);return{type:s.enum.mfa_enroll,payload:{factor_id:i,factors:m,errors:{fieldErrors:{code:[e.intl.formatMessage({id:"mfaEnroll.incorrectCode",defaultMessage:"Incorrect code. Please try again.",description:"Error message displayed when the user enters an incorrect code during enrollment."})]}}}}}if(M.safeParse(p).success){const m=await q(r);return{type:s.enum.mfa_enroll,payload:{factor_id:i,factors:m,errors:{formErrors:[e.intl.formatMessage({id:"mfaEnroll.tooManyRequests",defaultMessage:"Too many requests. Please try again later.",description:"Error message displayed when the user has made too many requests during enrollment."})]}}}}}})},Ye=(r,t,{init:e,authapiBaseUrlOverride:a})=>{const i=be.safeParse(t);if(!i.success)throw new f(i.error.message);return l(r,"/phone-otp/send",{...e,method:"GET"},{authapiBaseUrlOverride:a})},Ke=async(r,t,{intl:e,init:a,authapiBaseUrlOverride:i})=>{var p;const n=Oe(e).safeParse(t);if(!n.success)throw new f(n.error.message);const d="/phone-otp";if(n.data.data.intent===Me.enum.validate)return l(r,`${d}/validate`,{...a,method:"POST",body:JSON.stringify({code:n.data.data.code})},{authapiBaseUrlOverride:i,routeId:h.PHONE_VERIFICATION,intercept:({status:m,json:w})=>{if(v.safeParse({status:m,data:w}).success)throw j(v);if(P.safeParse({status:m,data:w}).success)return{type:s.enum.phone_otp_verification,payload:{errors:{formErrors:[e.formatMessage({id:"phoneVerification.fraudGuard",description:"Error message displayed when a phone number fails fraud guard checks.",defaultMessage:"Unable to send a text message to this phone number"})]}}};if(m===401||D.safeParse({status:m,data:w}).success)return{type:s.enum.phone_otp_verification,payload:{errors:{fieldErrors:{code:[e.formatMessage({id:"phoneVerification.incorrectCode",description:"Error message displayed when the user enters an incorrect OTP code.",defaultMessage:"Incorrect code"})]}}}}}});const o=await fetch(`${i??"https://auth.openai.com/api/accounts"}${d}/resend`,{...a,method:"POST",credentials:"include",signal:r.signal});if(o.ok)return{page:{type:s.enum.phone_otp_verification},responseHeaders:o.headers};const u=(p=o.headers.get("Content-Type"))!=null&&p.includes("application/json")?await o.clone().json():null;if(F.safeParse({status:o.status,data:u}).success)return{page:{type:s.enum.phone_otp_verification,payload:{errors:{formErrors:[e.formatMessage({id:"phoneVerification.tooManyResentOtps",defaultMessage:"Tried to resend too many times. Please try again later.",description:"Error message for too many attempts"})]}}},responseHeaders:o.headers};if(P.safeParse({status:o.status,data:u}).success)return{page:{type:s.enum.phone_otp_verification,payload:{errors:{formErrors:[e.formatMessage({id:"phoneVerification.fraudGuard",description:"Error message displayed when a phone number fails fraud guard checks.",defaultMessage:"Unable to send a text message to this phone number"})]}}},responseHeaders:o.headers};throw o},Qe=async(r,t,e)=>{var u;const a=Se.safeParse(t);if(!a.success)throw new f(a.error.message);const i=a.data.data.intent;if(i==="finalize"){const{mfa_session_id:p}=a.data.data;return l(r,"/push-auth/validate",{...e.init,method:"POST",body:JSON.stringify({mfa_session_id:p}),signal:r.signal,credentials:"include"},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.PUSH_AUTH_VERIFICATION})}if(i==="send_email_otp")return l(r,"/email-otp/send",{...e.init,method:"POST",signal:r.signal,credentials:"include"},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.PUSH_AUTH_VERIFICATION});const{mfa_session_id:n}=a.data.data,d=new Headers((u=e.init)==null?void 0:u.headers);d.set("Accept","application/json"),d.set("Content-Type","application/json");const o=await fetch(`${e.authapiBaseUrlOverride??"https://auth.openai.com/api/accounts"}/push-auth/resend`,{...e.init,method:"POST",credentials:"include",signal:r.signal,headers:d,body:JSON.stringify({mfa_session_id:n})});if(o.ok){const{session_id:p}=await N(r),m=new URLSearchParams;return p&&m.set("session_id",p),{page:{type:s.enum.push_auth_verification,payload:{mfa_request_id:n,query_params:m.toString()}},responseHeaders:o.headers}}throw o},Xe=(r,t,e)=>{const a=Re.safeParse(t);if(!a.success)throw new f(a.error.message);const{password:i}=a.data.data;return l(r,"/password/reset",{...e.init,method:"POST",body:JSON.stringify({password:i})},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.RESET_PASSWORD_NEW_PASSWORD,intercept:n=>{const d={status:n.status,data:n.json};if(L.safeParse(d).success)return{type:s.enum.reset_password_new_password,payload:{errors:{fieldErrors:{password:[e.intl.formatMessage({id:"resetPasswordNewPassword.passwordMustNotBeReused",defaultMessage:"Password must not be reused",description:"Error message shown when a new password is reused from the previous password"})]}}}};if(I.safeParse(d).success)return{type:s.enum.reset_password_new_password,payload:{errors:{fieldErrors:{password:[e.intl.formatMessage({id:"resetPasswordNewPassword.passwordContainsUserInfo",defaultMessage:"Password must not contain personal information like your email, phone number, or name",description:"Error message displayed when the user tries to reset their password with a password that contains user information"})]}}}};if(T.safeParse(d).success)return{type:s.enum.reset_password_new_password,payload:{errors:{fieldErrors:{password:[e.intl.formatMessage({id:"resetPasswordNewPassword.passwordTooWeak",defaultMessage:"Password is too weak",description:"Error message displayed when the user tries to reset their password with a password that is too weak"})]}}}};if(J.safeParse(d).success)return{type:s.enum.reset_password_new_password,payload:{errors:{formErrors:[e.intl.formatMessage({id:"resetPasswordNewPassword.genericBadRequest",defaultMessage:"Failed to reset password. Please try again",description:"Generic error message when resetting password fails with a generic bad request error without a specific error code"})]}}}}})},Ze=(r,t,e)=>{const a=Ie.safeParse(t);if(!a.success)throw new f(a.error.message);return(a.data.intent??C.enum.send_otp)===C.enum.passwordless_login_send_otp?l(r,"/passwordless/send-otp",{...e.init,method:"POST"},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.RESET_PASSWORD_START,intercept:n=>{const d={status:n.status,data:n.json};if(b.safeParse(d).success)return{type:s.enum.reset_password_start,payload:{errors:{formErrors:[e.intl.formatMessage({id:"resetPasswordStart.passwordlessLoginSendOtp.InvalidRequest",defaultMessage:"Invalid request",description:"Shown when send one-time code for passwordless login fails with invalid request"})]}}}}}):l(r,"/password/send-otp",{...e.init,method:"POST"},{authapiBaseUrlOverride:e.authapiBaseUrlOverride,routeId:h.RESET_PASSWORD_START,intercept:n=>{const d={status:n.status,data:n.json};if(P.safeParse(d).success)return{type:s.enum.reset_password_start,payload:{errors:{formErrors:[e.intl.formatMessage({id:"resetPasswordStart.fraudGuard",description:"Error message displayed when a phone number fails fraud guard checks.",defaultMessage:"Unable to send a text message to this phone number"})]}}};if(b.safeParse(d).success)return{type:s.enum.reset_password_start,payload:{errors:{formErrors:[e.intl.formatMessage({id:"resetPasswordStart.invalidRequest",defaultMessage:"Invalid request",description:"Shown when reset password start fails with invalid request"})]}}}}})};function as(r,t,e){switch(t.origin_page_type){case s.enum.advanced_account_security_enroll:return g();case s.enum.about_you:return Ne(r,t,e);case s.enum.add_password_new_password:return Ue(r,t,e);case s.enum.add_email:return g();case s.enum.add_phone:return g();case s.enum.apple_intelligence_start:return g();case s.enum.auth_challenge:return g();case s.enum.contact_verification:return Be(r,t,e);case s.enum.phone_otp_verification:return Ke(r,t,e);case s.enum.choose_an_account:return ke(r,t,e);case s.enum.create_account_later:return g();case s.enum.create_account_later_queued:return g();case s.enum.create_account_password:return Ce(r,t,e);case s.enum.create_account_start:return He(r,t,e);case s.enum.email_otp_send:return Le(r,t,e);case s.enum.email_otp_verification:return Ve(r,t,e);case s.enum.error:return g();case s.enum.external_url:return g();case s.enum.identity_verification:return g();case s.enum.login_password:return Je(r,t,e);case s.enum.login_passkey:return Fe(r,t,e);case s.enum.login_start:return We(r,t,e);case s.enum.login_or_signup_start:return je(r,t,e);case s.enum.mfa_challenge:return $e(r,t,e);case s.enum.mfa_enroll:return ze(r,t,e);case s.enum.phone_otp_send:return Ye(r,t,e);case s.enum.push_auth_verification:return Qe(r,t,e);case s.enum.reset_password_new_password:return Xe(r,t,e);case s.enum.reset_password_start:return Ze(r,t,e);case s.enum.reset_password_success:return g();case s.enum.sign_in_with_chatgpt_consent:return g();case s.enum.sign_in_with_chatgpt_codex_consent:return g();case s.enum.sign_in_with_chatgpt_codex_org:return g();case s.enum.sso:return g();case s.enum.token_exchange:return g();case s.enum.workspace:return g();case s.enum.__test__:return g();default:G(t,()=>z({error:"Unknown page type"},{status:400}))}}const g=async()=>({page:{type:s.enum.error},responseHeaders:new Headers});function xe(r){switch(r.type){case s.enum.advanced_account_security_enroll:return"/advanced-account-security";case s.enum.about_you:return c("/about-you");case s.enum.add_email:return c("/add-email");case s.enum.add_phone:return c("/add-phone");case s.enum.add_password_new_password:return c("/add-password/new-password");case s.enum.apple_intelligence_start:return c("/start");case s.enum.auth_challenge:return r.payload.method_id?c("/auth_challenge/:id",{id:r.payload.method_id}):c("/auth_challenge");case s.enum.contact_verification:return c("/contact-verification");case s.enum.phone_otp_verification:return c("/phone-verification");case s.enum.choose_an_account:return c("/choose-an-account");case s.enum.create_account_later:return c("/create-account-later");case s.enum.create_account_later_queued:return c("/create-account-later/queued");case s.enum.create_account_password:return c("/create-account/password");case s.enum.create_account_start:return c("/create-account");case s.enum.email_otp_send:return"https://auth.openai.com/api/accounts/email-otp/send";case s.enum.email_otp_verification:return c("/email-verification");case s.enum.error:return c("/error");case s.enum.external_url:return r.payload.url;case s.enum.identity_verification:return c("/verify-your-identity");case s.enum.login_passkey:return"/log-in/passkey";case s.enum.login_password:return c("/log-in/password");case s.enum.login_start:return c("/log-in");case s.enum.login_or_signup_start:return c("/log-in-or-create-account");case s.enum.mfa_challenge:return r.payload.factor_id?c("/mfa-challenge/:id",{id:r.payload.factor_id}):c("/mfa-challenge");case s.enum.mfa_enroll:return c("/mfa-enroll/:id",{id:r.payload.factor_id});case s.enum.phone_otp_send:return"https://auth.openai.com/api/accounts/phone-otp/send";case s.enum.push_auth_verification:return c("/push-auth-verification/:mfa_request_id",{mfa_request_id:r.payload.mfa_request_id})+`?${r.payload.query_params}`;case s.enum.reset_password_start:return c("/reset-password");case s.enum.reset_password_new_password:return c("/reset-password/new-password");case s.enum.reset_password_success:return c("/reset-password/success");case s.enum.sign_in_with_chatgpt_consent:return c("/sign-in-with-chatgpt/consent");case s.enum.sign_in_with_chatgpt_codex_consent:return c("/sign-in-with-chatgpt/codex/consent");case s.enum.sign_in_with_chatgpt_codex_org:return c("/sign-in-with-chatgpt/codex/organization");case s.enum.sso:return c("/sso");case s.enum.workspace:return c("/workspace");case s.enum.token_exchange:throw new Error(`no web mapping for pageType: ${r.type}`);case s.enum.__test__:return r.payload.path;default:G(r)}}function ts(r,t){const e=xe(r),a=new URL(t.url),i=new URL(e,a.origin);return i.origin!==a.origin||i.pathname.startsWith("/api/")?Y(e):K(e)}export{as as n,ts as r}; -//# sourceMappingURL=redirectToPage-QVIYfPch.js.map