"""Sub2API bundle export — ChatGPT tokens → sub2api JSON format. Replicates the conversion logic from https://gpt.learnlicen.dpdns.org/ Input: registered account records (from register or login) Output: sub2api bundle JSON with team-scoped tokens """ from __future__ import annotations import json import time from datetime import datetime, timezone from pathlib import Path from typing import Any from chatgpt_register_sub2api.utils.jwt import ( backfill_id_token_claims, decode_jwt_payload, extract_account_info, ) def _now_iso() -> str: return datetime.now(timezone.utc).isoformat() def _coerce_expires_at( access_token: str, fallback_expired: str = "", ) -> int: """Extract exp from JWT or compute fallback (now + ~10 days).""" if access_token: info = extract_account_info(access_token) exp = info.get("exp", 0) if exp and exp > 0: return exp # Fallback: parse expired string or use now + 10 days if fallback_expired: try: return int(fallback_expired) except (TypeError, ValueError): pass return int(time.time()) + 863999 # ~10 days def build_credentials( account: dict[str, Any], ) -> dict[str, Any]: """Build the credentials dict for a single sub2api account entry. Args: account: Account record with keys: email, password, access_token, refresh_token, id_token, chatgpt_account_id, chatgpt_user_id, plan_type, etc. Returns: Sub2API credentials dict """ access_token = str(account.get("access_token") or "").strip() refresh_token = str(account.get("refresh_token") or "").strip() id_token = str(account.get("id_token") or "").strip() email = str(account.get("email") or "").strip() # Extract claims from access token JWT info = extract_account_info(access_token) if access_token else {} chatgpt_account_id = ( str(account.get("chatgpt_account_id") or "").strip() or info.get("chatgpt_account_id", "") ) chatgpt_user_id = ( str(account.get("chatgpt_user_id") or "").strip() or info.get("chatgpt_user_id", "") ) plan_type = ( str(account.get("plan_type") or "").strip() or info.get("plan_type", "free") ) organization_id = ( str(account.get("organization_id") or "").strip() or info.get("organization_id", "") ) user_email = email or info.get("email", "") expires_at = _coerce_expires_at( access_token, str(account.get("expired") or account.get("expires_at") or ""), ) # Backfill id_token claims if needed if id_token and (not chatgpt_account_id or not chatgpt_user_id): id_token = backfill_id_token_claims( id_token, chatgpt_account_id=chatgpt_account_id, account_id=chatgpt_user_id, ) return { "access_token": access_token, "refresh_token": refresh_token, "id_token": id_token, "chatgpt_account_id": chatgpt_account_id, "chatgpt_user_id": chatgpt_user_id, "client_id": account.get("client_id", "app_2SKx67EdpoN0G6j64rFvigXD"), "email": user_email, "expires_at": expires_at, "organization_id": organization_id, "plan_type": plan_type or "free", "session_token": str(account.get("session_token") or "").strip(), } def build_account_entry( account: dict[str, Any], ) -> dict[str, Any]: """Build a single sub2api account entry from an account record.""" email = str(account.get("email") or "").strip() source = str(account.get("source_type") or account.get("source") or "registration") return { "name": email, "platform": "openai", "type": "oauth", "credentials": build_credentials(account), "extra": { "email": email, "auth_provider": "chatgpt2api", "source": source, "openai_oauth_responses_websockets_v2_enabled": False, "openai_oauth_responses_websockets_v2_mode": "off", "privacy_mode": str(account.get("privacy_mode") or "training_off"), }, "concurrency": 10, "priority": 1, "rate_multiplier": 1, "auto_pause_on_expired": True, } def build_sub2api_bundle( accounts: list[dict[str, Any]], ) -> dict[str, Any]: """Build a complete sub2api bundle from a list of account records. Args: accounts: List of account dicts (from registration or login) Returns: Sub2API bundle dict ready for JSON serialization """ return { "exported_at": _now_iso(), "proxies": [], "accounts": [build_account_entry(acc) for acc in accounts], } def export_sub2api_json( accounts: list[dict[str, Any]], output_path: str | Path | None = None, ) -> str: """Export accounts as sub2api bundle JSON. Args: accounts: List of account records output_path: Path to write. If None, auto-generates as sub2api-YYYYMMDD-HHMMSS.json in cwd. Returns: JSON string of the sub2api bundle """ bundle = build_sub2api_bundle(accounts) json_str = json.dumps(bundle, ensure_ascii=False, indent=2) + "\n" if output_path is None: from datetime import datetime timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") output_path = Path(f"sub2api-{timestamp}.json") path = Path(output_path) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json_str, encoding="utf-8") return json_str, str(path.resolve()) def export_single_account_json( account: dict[str, Any], output_path: str | Path | None = None, ) -> str: """Export a single account as a CPA-style JSON (for debugging).""" creds = build_credentials(account) record = { "type": "codex", "email": str(account.get("email") or ""), "expired": creds.get("expires_at", ""), "id_token": creds.get("id_token", ""), "account_id": creds.get("chatgpt_account_id", ""), "disabled": False, "access_token": creds.get("access_token", ""), "session_token": creds.get("session_token", ""), "last_refresh": _now_iso(), "refresh_token": creds.get("refresh_token", ""), "plan_type": creds.get("plan_type", ""), } json_str = json.dumps(record, ensure_ascii=False, indent=2) + "\n" if output_path: Path(output_path).parent.mkdir(parents=True, exist_ok=True) Path(output_path).write_text(json_str, encoding="utf-8") return json_str