register.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. """ChatGPT registration engine for zhuce6."""
  2. from __future__ import annotations
  3. import base64
  4. from dataclasses import asdict, dataclass, field
  5. from datetime import datetime
  6. import json
  7. import logging
  8. import re
  9. import time
  10. from pathlib import Path
  11. from typing import Any, Callable, Protocol
  12. from core.paths import STATE_DIR
  13. from .constants import OPENAI_PAGE_TYPES
  14. from .http_client import OpenAIHTTPClient
  15. from .oauth import OAuthManager, OAuthStart, submit_callback_url
  16. from . import register_http as register_http_module
  17. from .register_http import (
  18. _build_sentinel_header,
  19. _auth_url,
  20. _check_ip_location,
  21. _check_sentinel,
  22. _refresh_registration_session,
  23. _create_email,
  24. _create_user_account,
  25. _email_domain,
  26. _get_device_id,
  27. _init_session,
  28. _is_transient_transport_error,
  29. _load_add_phone_oauth_max_attempts,
  30. _load_add_phone_oauth_otp_timeout_seconds,
  31. _load_post_create_login_delay_seconds,
  32. _load_wait_otp_timeout_seconds,
  33. _metadata,
  34. _oauth_json_headers,
  35. _generate_password,
  36. _register_password,
  37. _send_verification_code,
  38. _session_request,
  39. _start_oauth,
  40. _start_oauth_via_chatgpt,
  41. _submit_signup_form,
  42. )
  43. from . import register_oauth as register_oauth_module
  44. from .register_oauth import (
  45. _decode_oauth_session_cookie,
  46. _extract_callback_url,
  47. _extract_callback_url_from_error,
  48. _extract_session_token,
  49. _fetch_client_auth_session_dump,
  50. _follow_redirects,
  51. _follow_redirects_with_session,
  52. _get_workspace_id,
  53. _handle_oauth_callback,
  54. _login_for_token as _login_for_token_impl,
  55. _load_oauth_session_payload,
  56. _parse_token_response,
  57. _parse_workspace_from_cookie,
  58. _refresh_tokens_from_session_cookie as _refresh_tokens_from_session_cookie_impl,
  59. _select_workspace,
  60. _try_create_account_callback_session_token,
  61. _try_direct_session_token,
  62. _parse_session_jwt,
  63. )
  64. from .register_otp import (
  65. _capture_mailbox_ids,
  66. _get_verification_code,
  67. _mailbox_context,
  68. _validate_verification_code,
  69. _wait_for_mailbox_code,
  70. )
  71. from .token_refresh import TokenRefreshManager
  72. logger = logging.getLogger(__name__)
  73. DEFAULT_ADD_PHONE_OAUTH_OTP_TIMEOUT_SECONDS = 90
  74. class EmailServiceProtocol(Protocol):
  75. def create_email(self, config: dict[str, Any] | None = None) -> dict[str, Any]:
  76. ...
  77. def get_verification_code(
  78. self,
  79. email: str | None = None,
  80. email_id: str | None = None,
  81. timeout: int = 120,
  82. pattern: str | None = None,
  83. otp_sent_at: float | None = None,
  84. ) -> str:
  85. ...
  86. class MailboxDedupeProtocol(Protocol):
  87. def reserve(self, email: str) -> bool:
  88. ...
  89. def release(self, email: str) -> None:
  90. ...
  91. def mark(self, email: str, *, reason: str) -> None:
  92. ...
  93. @dataclass
  94. class RegistrationResult:
  95. success: bool
  96. stage: str = "init"
  97. email: str = ""
  98. password: str = ""
  99. account_id: str = ""
  100. workspace_id: str = ""
  101. access_token: str = ""
  102. refresh_token: str = ""
  103. id_token: str = ""
  104. session_token: str = ""
  105. error_message: str = ""
  106. logs: list[str] = field(default_factory=list)
  107. metadata: dict[str, Any] = field(default_factory=dict)
  108. manual_steps: list[str] = field(default_factory=list)
  109. source: str = "register"
  110. def to_dict(self) -> dict[str, Any]:
  111. return asdict(self)
  112. @dataclass
  113. class SignupFormResult:
  114. success: bool
  115. page_type: str = ""
  116. is_existing_account: bool = False
  117. response_data: dict[str, Any] = field(default_factory=dict)
  118. error_message: str = ""
  119. register_http_module.SignupFormResult = SignupFormResult
  120. class RegistrationEngine:
  121. """Repaired ChatGPT registration flow with truthful runtime stages."""
  122. def __init__(
  123. self,
  124. email_service: EmailServiceProtocol,
  125. proxy_url: str | None = None,
  126. callback_logger: Callable[[str], None] | None = None,
  127. task_uuid: str | None = None,
  128. mailbox_dedupe_store: MailboxDedupeProtocol | None = None,
  129. create_email_max_attempts: int = 5,
  130. ) -> None:
  131. self.email_service = email_service
  132. self.proxy_url = proxy_url
  133. self.callback_logger = callback_logger or (lambda message: logger.info(message))
  134. self.task_uuid = task_uuid
  135. self.mailbox_dedupe_store = mailbox_dedupe_store
  136. self.create_email_max_attempts = max(1, int(create_email_max_attempts))
  137. self.http_client = OpenAIHTTPClient(proxy_url=proxy_url)
  138. self.oauth_manager = OAuthManager(proxy_url=proxy_url)
  139. self.email: str | None = None
  140. self.password: str | None = None
  141. self.email_info: dict[str, Any] | None = None
  142. self.oauth_start: OAuthStart | None = None
  143. self.session: Any | None = None
  144. self.session_token: str | None = None
  145. self.logs: list[str] = []
  146. self._otp_sent_at: float | None = None
  147. self._signup_otp_before_ids: set[str] = set()
  148. self._is_existing_account = False
  149. self._create_account_continue_url: str | None = None
  150. self._last_create_account_http_status: int | None = None
  151. self._last_create_account_error_code: str = ""
  152. self._last_create_account_error_message: str = ""
  153. self._last_create_account_error_body: str = ""
  154. self._last_signup_http_status: int | None = None
  155. self._last_signup_error_code: str = ""
  156. self._last_signup_error_message: str = ""
  157. self._last_signup_error_body: str = ""
  158. self._signup_auth_reset_count = 0
  159. self._last_mailbox_error_kind: str = ""
  160. self._last_mailbox_error_stage: str = ""
  161. self._last_mailbox_error_message: str = ""
  162. self._add_phone_oauth_max_attempts = self._load_add_phone_oauth_max_attempts()
  163. self._otp_wait_timeout_seconds = self._load_wait_otp_timeout_seconds()
  164. self._add_phone_oauth_otp_timeout_seconds = self._load_add_phone_oauth_otp_timeout_seconds()
  165. self._post_create_login_delay_seconds = self._load_post_create_login_delay_seconds()
  166. self._last_otp_wait_failure_reason: str = ""
  167. self._last_otp_wait_diagnostics: dict[str, Any] = {}
  168. self._reserved_email: str = ""
  169. self._add_phone_trace_context: dict[str, Any] = {}
  170. self._add_phone_oauth_attempt_counter = 0
  171. def _log(self, message: str) -> None:
  172. timestamp = datetime.now().strftime("%H:%M:%S")
  173. log_message = f"[{timestamp}] {message}"
  174. self.logs.append(log_message)
  175. self.callback_logger(log_message)
  176. def _result(
  177. self,
  178. *,
  179. success: bool,
  180. stage: str,
  181. error_message: str = "",
  182. source: str = "register",
  183. metadata: dict[str, Any] | None = None,
  184. manual_steps: list[str] | None = None,
  185. ) -> RegistrationResult:
  186. merged_metadata = self._metadata(metadata)
  187. return RegistrationResult(
  188. success=success,
  189. stage=stage,
  190. email=self.email or "",
  191. password=self.password or "",
  192. account_id="",
  193. workspace_id="",
  194. error_message=error_message,
  195. logs=list(self.logs),
  196. metadata=merged_metadata,
  197. manual_steps=manual_steps or [],
  198. source=source,
  199. )
  200. def _add_phone_trace_dir(self) -> Path:
  201. trace_dir = STATE_DIR / "add_phone_traces"
  202. trace_dir.mkdir(parents=True, exist_ok=True)
  203. return trace_dir
  204. def _set_add_phone_trace(self, **updates: Any) -> None:
  205. clean_updates = {key: value for key, value in updates.items() if value is not None}
  206. self._add_phone_trace_context.update(clean_updates)
  207. def _append_add_phone_attempt(self, payload: dict[str, Any]) -> None:
  208. attempts = self._add_phone_trace_context.setdefault("fresh_login_attempts", [])
  209. if isinstance(attempts, list):
  210. attempts.append(dict(payload))
  211. def _capture_add_phone_html(self, *, label: str, url: str, html: str) -> str:
  212. email_key = re.sub(r"[^A-Za-z0-9@._+-]+", "_", str(self.email or "unknown").strip()) or "unknown"
  213. html_path = self._add_phone_trace_dir() / f"{email_key}.{label}.html"
  214. snippet = str(html or "")
  215. html_path.write_text(snippet[:512000], encoding="utf-8")
  216. discovered_urls: set[str] = set()
  217. for match in re.finditer(r'<script[^>]+src=["\\\']([^"\\\']+)["\\\']', snippet, flags=re.I):
  218. src = match.group(1).strip()
  219. if src:
  220. discovered_urls.add(src)
  221. for match in re.finditer(
  222. r'<link[^>]+rel=["\\\']modulepreload["\\\'][^>]+href=["\\\']([^"\\\']+)["\\\']',
  223. snippet,
  224. flags=re.I,
  225. ):
  226. href = match.group(1).strip()
  227. if href:
  228. discovered_urls.add(href)
  229. script_urls = sorted(discovered_urls)
  230. html_artifacts = self._add_phone_trace_context.setdefault("html_artifacts", [])
  231. if isinstance(html_artifacts, list):
  232. html_artifacts.append(
  233. {
  234. "label": label,
  235. "url": url,
  236. "path": str(html_path),
  237. "script_urls": script_urls,
  238. }
  239. )
  240. return str(html_path)
  241. def _write_add_phone_trace_artifact(self, *, reason: str) -> str:
  242. email_key = re.sub(r"[^A-Za-z0-9@._+-]+", "_", str(self.email or "unknown").strip()) or "unknown"
  243. trace_path = self._add_phone_trace_dir() / f"{email_key}.json"
  244. payload = {
  245. "email": self.email or "",
  246. "reason": reason,
  247. "proxy_url": self.proxy_url or "",
  248. "written_at": datetime.now().astimezone().isoformat(timespec="seconds"),
  249. "logs": list(self.logs[-200:]),
  250. }
  251. payload.update(self._add_phone_trace_context)
  252. trace_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
  253. self._add_phone_trace_context["trace_path"] = str(trace_path)
  254. self._log(f"add-phone trace saved: {trace_path}")
  255. return str(trace_path)
  256. def _sync_oauth_helper_globals(self) -> None:
  257. register_oauth_module.OpenAIHTTPClient = OpenAIHTTPClient
  258. register_oauth_module.OPENAI_PAGE_TYPES = OPENAI_PAGE_TYPES
  259. register_oauth_module.TokenRefreshManager = TokenRefreshManager
  260. register_oauth_module.submit_callback_url = submit_callback_url
  261. register_oauth_module.base64 = base64
  262. register_oauth_module.re = __import__("re")
  263. def _should_short_circuit_add_phone_retry(self) -> bool:
  264. workspace_count = self._add_phone_trace_context.get("auth_session_workspace_count")
  265. try:
  266. workspace_count_int = int(workspace_count or 0)
  267. except Exception:
  268. workspace_count_int = 0
  269. if workspace_count_int > 0:
  270. return False
  271. direct_keys_raw = self._add_phone_trace_context.get("direct_session_keys") or []
  272. if isinstance(direct_keys_raw, str):
  273. direct_keys = {direct_keys_raw.strip()} if direct_keys_raw.strip() else set()
  274. else:
  275. direct_keys = {str(item).strip() for item in direct_keys_raw if str(item).strip()}
  276. if direct_keys and direct_keys != {"WARNING_BANNER"}:
  277. return False
  278. attempts = self._add_phone_trace_context.get("fresh_login_attempts") or []
  279. if not isinstance(attempts, list) or not attempts:
  280. return False
  281. last_attempt = attempts[-1] or {}
  282. final_page_type = str(last_attempt.get("final_page_type") or "").strip()
  283. final_continue_url = self._auth_url(str(last_attempt.get("final_continue_url") or "").strip())
  284. callback_found = bool(last_attempt.get("callback_found"))
  285. session_token_found = bool(last_attempt.get("session_token_found"))
  286. if callback_found or session_token_found:
  287. return False
  288. if final_page_type == "add_phone":
  289. return True
  290. return "add-phone" in final_continue_url
  291. def _refresh_tokens_from_session_cookie(
  292. self,
  293. session: Any | None = None,
  294. *,
  295. label: str,
  296. ) -> dict[str, Any] | None:
  297. self._sync_oauth_helper_globals()
  298. return _refresh_tokens_from_session_cookie_impl(self, session, label=label)
  299. def _login_for_token(self) -> dict[str, Any] | None:
  300. self._sync_oauth_helper_globals()
  301. return _login_for_token_impl(self)
  302. def _reset_signup_auth_context(self, *, reason: str) -> tuple[str | None, str | None]:
  303. self._signup_auth_reset_count += 1
  304. self.http_client = OpenAIHTTPClient(proxy_url=self.proxy_url)
  305. self.oauth_manager = OAuthManager(proxy_url=self.proxy_url)
  306. self.session = None
  307. self.oauth_start = None
  308. self.session_token = None
  309. self._log(
  310. "signup invalid_auth_step detected; rebuilding auth context "
  311. f"(reason={reason}, reset_count={self._signup_auth_reset_count})"
  312. )
  313. if not self._init_session():
  314. return None, None
  315. if not self._start_oauth():
  316. return None, None
  317. device_id = self._get_device_id()
  318. if not device_id:
  319. return None, None
  320. sentinel_token = self._check_sentinel(device_id)
  321. return device_id, sentinel_token
  322. def run_preflight(self) -> RegistrationResult:
  323. if not self.password:
  324. self.password = self._generate_password()
  325. ip_ok, location = self._check_ip_location()
  326. if not ip_ok:
  327. return self._result(
  328. success=False,
  329. stage="ip_check",
  330. error_message=f"unsupported or unknown ip location: {location}",
  331. source="register_preflight",
  332. )
  333. if not self._create_email():
  334. return self._result(
  335. success=False,
  336. stage="mailbox",
  337. error_message="mailbox bootstrap failed",
  338. source="register_preflight",
  339. )
  340. if not self._init_session():
  341. return self._result(
  342. success=False,
  343. stage="session",
  344. error_message="session bootstrap failed",
  345. source="register_preflight",
  346. )
  347. if not self._start_oauth():
  348. return self._result(
  349. success=False,
  350. stage="oauth_bootstrap",
  351. error_message="oauth bootstrap failed",
  352. source="register_preflight",
  353. )
  354. device_id = self._get_device_id()
  355. sentinel_token = self._check_sentinel(device_id) if device_id else None
  356. self._log("registration preflight ready")
  357. return RegistrationResult(
  358. success=False,
  359. stage="oauth_preflight",
  360. email=self.email or "",
  361. password=self.password or "",
  362. error_message="full registration flow requires live upstream interaction; preflight is ready",
  363. logs=list(self.logs),
  364. metadata=self._metadata(
  365. {
  366. "location": location,
  367. "device_id": device_id or "",
  368. "sentinel_token_present": bool(sentinel_token),
  369. "oauth_url": self.oauth_start.auth_url if self.oauth_start else "",
  370. "oauth_state": self.oauth_start.state if self.oauth_start else "",
  371. "oauth_code_verifier": self.oauth_start.code_verifier if self.oauth_start else "",
  372. "oauth_redirect_uri": self.oauth_start.redirect_uri if self.oauth_start else "",
  373. "task_uuid": self.task_uuid or "",
  374. }
  375. ),
  376. manual_steps=[
  377. "Open oauth_url in a browser if you want to continue manually.",
  378. "Use callback exchange if you capture a real callback URL.",
  379. ],
  380. source="register_preflight",
  381. )
  382. def run(self) -> RegistrationResult:
  383. result = RegistrationResult(success=False, stage="init", logs=list(self.logs))
  384. try:
  385. self._log("=" * 60)
  386. self._log("starting chatgpt registration flow")
  387. self._log("=" * 60)
  388. ip_ok, location = self._check_ip_location()
  389. if not ip_ok:
  390. return self._result(
  391. success=False,
  392. stage="ip_check",
  393. error_message=f"unsupported or unknown ip location: {location}",
  394. metadata={"location": location},
  395. )
  396. if not self._create_email():
  397. return self._result(success=False, stage="mailbox", error_message="create email failed")
  398. if not self._init_session():
  399. return self._result(success=False, stage="session", error_message="session bootstrap failed")
  400. if not self._start_oauth_via_chatgpt():
  401. self._log("chatgpt.com oauth failed, falling back to direct oauth")
  402. if not self._start_oauth():
  403. return self._result(success=False, stage="oauth_bootstrap", error_message="oauth bootstrap failed")
  404. device_id = self._get_device_id()
  405. if not device_id:
  406. return self._result(success=False, stage="device_id", error_message="device id acquisition failed")
  407. self._last_device_id = device_id
  408. sentinel_token = self._check_sentinel(device_id)
  409. self._last_sentinel_token = sentinel_token
  410. signup_result = self._submit_signup_form(device_id, sentinel_token)
  411. if (
  412. not signup_result.success
  413. and (
  414. str(self._last_signup_error_code or "").strip().lower() == "invalid_auth_step"
  415. or "invalid_auth_step" in str(signup_result.error_message or "").strip().lower()
  416. )
  417. ):
  418. reset_device_id, reset_sentinel_token = self._reset_signup_auth_context(reason="invalid_auth_step")
  419. if reset_device_id:
  420. device_id = reset_device_id
  421. sentinel_token = reset_sentinel_token
  422. signup_result = self._submit_signup_form(device_id, sentinel_token)
  423. if not signup_result.success:
  424. return self._result(
  425. success=False,
  426. stage="signup",
  427. error_message=signup_result.error_message or "signup form failed",
  428. metadata={
  429. "page_type": signup_result.page_type,
  430. "signup_auth_reset_count": self._signup_auth_reset_count,
  431. },
  432. )
  433. if self._is_existing_account:
  434. self._otp_sent_at = time.time()
  435. self._log("existing account flow: skipping password registration and otp send")
  436. else:
  437. if not self._register_password():
  438. return self._result(success=False, stage="password", error_message="password registration failed")
  439. time.sleep(1)
  440. if not self._send_verification_code():
  441. return self._result(success=False, stage="send_otp", error_message="otp send failed")
  442. code = self._get_verification_code()
  443. if not code:
  444. return self._result(success=False, stage="wait_otp", error_message="otp retrieval failed")
  445. if not self._validate_verification_code(code):
  446. return self._result(success=False, stage="validate_otp", error_message="otp validation failed")
  447. if not self._is_existing_account and not self._create_user_account():
  448. if (
  449. self.mailbox_dedupe_store is not None
  450. and self.email
  451. and self._last_create_account_error_code.strip().lower() == "user_already_exists"
  452. ):
  453. self.mailbox_dedupe_store.mark(self.email, reason="user_already_exists")
  454. return self._result(success=False, stage="create_account", error_message="create account failed")
  455. post_create_continue_url = self._auth_url(str(self._create_account_continue_url or "").strip())
  456. post_create_gate = ""
  457. if not self._is_existing_account and "add-phone" in post_create_continue_url:
  458. post_create_gate = "add_phone"
  459. self._log(
  460. "post-create continue_url requires phone gate; "
  461. "continuing oauth token acquisition attempt"
  462. )
  463. self._set_add_phone_trace(
  464. post_create_continue_url=post_create_continue_url,
  465. post_create_page_type="add_phone",
  466. create_account_http_status=self._last_create_account_http_status,
  467. )
  468. token_info: dict[str, Any] | None = None
  469. workspace_id = ""
  470. continue_url = ""
  471. callback_url = ""
  472. if not token_info and post_create_continue_url:
  473. token_info = self._try_create_account_callback_session_token(post_create_continue_url)
  474. if not token_info and self.oauth_start and self.session:
  475. # For existing accounts or when continue_url has no callback,
  476. # follow the original OAuth auth_url redirects to get callback
  477. oauth_callback = self._follow_redirects_with_session(
  478. self.session, self.oauth_start.auth_url,
  479. referer="https://auth.openai.com/about-you",
  480. )
  481. if oauth_callback:
  482. token_info = self._try_create_account_callback_session_token(oauth_callback)
  483. if not token_info:
  484. workspace_id = self._get_workspace_id()
  485. if workspace_id:
  486. continue_url = self._select_workspace(workspace_id) or ""
  487. if continue_url:
  488. callback_url = self._follow_redirects(continue_url) or ""
  489. if callback_url:
  490. token_info = self._handle_oauth_callback(callback_url)
  491. if not token_info:
  492. if post_create_gate == "add_phone":
  493. self._log("post-create add_phone: attempting direct session token extraction")
  494. token_info = self._try_direct_session_token()
  495. if not token_info:
  496. max_oauth_attempts = 1
  497. if post_create_gate == "add_phone":
  498. max_oauth_attempts = self._add_phone_oauth_max_attempts
  499. for oauth_attempt in range(1, max_oauth_attempts + 1):
  500. if oauth_attempt == 1:
  501. if post_create_gate == "add_phone" and self._post_create_login_delay_seconds > 0:
  502. self._log(
  503. "post-create add_phone: waiting before fresh login "
  504. f"({self._post_create_login_delay_seconds}s)"
  505. )
  506. time.sleep(self._post_create_login_delay_seconds)
  507. self._log("workspace flow failed; attempting password login for token")
  508. else:
  509. self._log(
  510. "add-phone oauth retry: "
  511. f"attempt {oauth_attempt}/{max_oauth_attempts}"
  512. )
  513. self._add_phone_oauth_attempt_counter = oauth_attempt
  514. token_info = self._login_for_token()
  515. if token_info:
  516. break
  517. if (
  518. post_create_gate == "add_phone"
  519. and oauth_attempt < max_oauth_attempts
  520. and self._should_short_circuit_add_phone_retry()
  521. ):
  522. self._log(
  523. "post-create add_phone: short-circuiting further fresh login retries "
  524. "because trace shows no workspace and no session token"
  525. )
  526. break
  527. if not token_info:
  528. if post_create_gate == "add_phone":
  529. # Solution B: include credentials for deferred retry queue
  530. mailbox_account = getattr(self.email_service, "_account", None)
  531. deferred_info: dict[str, Any] = {
  532. "email": self.email or "",
  533. "password": self.password or "",
  534. "registration_proxy_url": self.proxy_url or "",
  535. "registration_fingerprint_profile": "chrome120_win",
  536. }
  537. if mailbox_account is not None:
  538. deferred_info["mailbox_jwt"] = str(getattr(mailbox_account, "account_id", "") or "")
  539. deferred_info["mailbox_extra"] = dict(getattr(mailbox_account, "extra", {}) or {})
  540. trace_path = self._write_add_phone_trace_artifact(reason="hard_add_phone_gate")
  541. deferred_info["add_phone_trace_path"] = trace_path
  542. return self._result(
  543. success=False,
  544. stage="add_phone_gate",
  545. error_message="post-create flow requires phone gate",
  546. metadata={
  547. "post_create_continue_url": post_create_continue_url,
  548. "post_create_gate": post_create_gate,
  549. "add_phone_trace_path": trace_path,
  550. "deferred_credentials": deferred_info,
  551. },
  552. )
  553. return self._result(
  554. success=False,
  555. stage="token_acquisition",
  556. error_message="all token acquisition methods exhausted",
  557. )
  558. session_cookie = ""
  559. if self.session is not None:
  560. session_cookie = str(self.session.cookies.get("__Secure-next-auth.session-token") or "").strip()
  561. if not session_cookie:
  562. session_cookie = str((token_info or {}).get("session_token") or "").strip()
  563. result = RegistrationResult(
  564. success=True,
  565. stage="completed",
  566. email=self.email or "",
  567. password=self.password or "",
  568. account_id=str((token_info or {}).get("account_id") or "").strip(),
  569. workspace_id=workspace_id or "",
  570. access_token=str((token_info or {}).get("access_token") or "").strip(),
  571. refresh_token=str((token_info or {}).get("refresh_token") or "").strip(),
  572. id_token=str((token_info or {}).get("id_token") or "").strip(),
  573. session_token=session_cookie,
  574. logs=list(self.logs),
  575. metadata={
  576. "location": location,
  577. "device_id": device_id,
  578. "page_type": signup_result.page_type,
  579. "is_existing_account": self._is_existing_account,
  580. "continue_url": continue_url or "",
  581. "callback_url": callback_url or "",
  582. "has_oauth_token": bool(token_info),
  583. "expired": str((token_info or {}).get("expired") or ""),
  584. "last_refresh": str((token_info or {}).get("last_refresh") or ""),
  585. "email_domain": self._email_domain(),
  586. "create_account_http_status": self._last_create_account_http_status,
  587. "create_account_error_code": self._last_create_account_error_code,
  588. "create_account_error_message": self._last_create_account_error_message,
  589. "signup_http_status": self._last_signup_http_status,
  590. "signup_error_code": self._last_signup_error_code,
  591. "signup_error_message": self._last_signup_error_message,
  592. "signup_auth_reset_count": self._signup_auth_reset_count,
  593. "post_create_gate": post_create_gate,
  594. "post_create_continue_url": post_create_continue_url,
  595. },
  596. source="login" if self._is_existing_account else "register",
  597. )
  598. self._log("=" * 60)
  599. self._log(f"registration flow finished successfully for {result.email}")
  600. self._log("=" * 60)
  601. return result
  602. except Exception as exc:
  603. self._log(f"unexpected registration error: {exc}")
  604. return self._result(success=False, stage="unexpected_error", error_message=str(exc))
  605. finally:
  606. if self.mailbox_dedupe_store is not None and self._reserved_email:
  607. self.mailbox_dedupe_store.release(self._reserved_email)
  608. for _name, _func in {
  609. '_build_sentinel_header': _build_sentinel_header,
  610. '_auth_url': _auth_url,
  611. '_oauth_json_headers': _oauth_json_headers,
  612. '_extract_callback_url': _extract_callback_url,
  613. '_extract_callback_url_from_error': _extract_callback_url_from_error,
  614. '_extract_session_token': _extract_session_token,
  615. '_is_transient_transport_error': _is_transient_transport_error,
  616. '_session_request': _session_request,
  617. '_decode_oauth_session_cookie': _decode_oauth_session_cookie,
  618. '_fetch_client_auth_session_dump': _fetch_client_auth_session_dump,
  619. '_mailbox_context': _mailbox_context,
  620. '_capture_mailbox_ids': _capture_mailbox_ids,
  621. '_wait_for_mailbox_code': _wait_for_mailbox_code,
  622. '_check_ip_location': _check_ip_location,
  623. '_email_domain': _email_domain,
  624. '_create_email': _create_email,
  625. '_generate_password': _generate_password,
  626. '_init_session': _init_session,
  627. '_start_oauth': _start_oauth,
  628. '_start_oauth_via_chatgpt': _start_oauth_via_chatgpt,
  629. '_get_device_id': _get_device_id,
  630. '_check_sentinel': _check_sentinel,
  631. '_refresh_registration_session': _refresh_registration_session,
  632. '_submit_signup_form': _submit_signup_form,
  633. '_register_password': _register_password,
  634. '_send_verification_code': _send_verification_code,
  635. '_get_verification_code': _get_verification_code,
  636. '_validate_verification_code': _validate_verification_code,
  637. '_create_user_account': _create_user_account,
  638. '_extract_callback_url': _extract_callback_url,
  639. '_extract_callback_url_from_error': _extract_callback_url_from_error,
  640. '_extract_session_token': _extract_session_token,
  641. '_follow_redirects_with_session': _follow_redirects_with_session,
  642. '_load_oauth_session_payload': _load_oauth_session_payload,
  643. '_parse_token_response': _parse_token_response,
  644. '_parse_workspace_from_cookie': _parse_workspace_from_cookie,
  645. '_load_add_phone_oauth_max_attempts': _load_add_phone_oauth_max_attempts,
  646. '_load_wait_otp_timeout_seconds': _load_wait_otp_timeout_seconds,
  647. '_load_add_phone_oauth_otp_timeout_seconds': _load_add_phone_oauth_otp_timeout_seconds,
  648. '_load_post_create_login_delay_seconds': _load_post_create_login_delay_seconds,
  649. '_metadata': _metadata,
  650. '_get_workspace_id': _get_workspace_id,
  651. '_select_workspace': _select_workspace,
  652. '_follow_redirects': _follow_redirects,
  653. '_handle_oauth_callback': _handle_oauth_callback,
  654. '_try_create_account_callback_session_token': _try_create_account_callback_session_token,
  655. '_try_direct_session_token': _try_direct_session_token,
  656. '_parse_session_jwt': _parse_session_jwt,
  657. }.items():
  658. setattr(RegistrationEngine, _name, _func)