准备邮箱

This commit is contained in:
dela
2026-01-10 17:42:39 +08:00
parent 3e3b27cf3a
commit cc7ad129d8
7 changed files with 646 additions and 128 deletions

87
docs/tempmail.md Normal file
View File

@@ -0,0 +1,87 @@
## API 接口
### 🔐 根管理员令牌Root Admin Override
当请求方携带与服务端环境变量 `JWT_TOKEN`完全一致的令牌时,将跳过会话 Cookie/JWT 校验直接被识别为最高管理员strictAdmin
- 配置项:
- `wrangler.toml``[vars]``JWT_TOKEN="你的超管令牌"`
- 令牌携带方式(任选其一):
- Header标准`Authorization: Bearer <JWT_TOKEN>`
- Header自定义`X-Admin-Token: <JWT_TOKEN>`
- Query`?admin_token=<JWT_TOKEN>`
- 生效范围:
- 所有受保护的后端接口:`/api/*`
- 会话检查:`GET /api/session`
- 收信回调:`POST /receive`
- 管理页服务端访问判定(`/admin`/`/admin.html`)与未知路径的认证判断
- 行为说明:
- 命中令牌后,鉴权载荷为:`{ role: 'admin', username: '__root__', userId: 0 }`
- `strictAdmin` 判定对 `__root__` 为 true与严格管理员等价
- 若未携带或不匹配,则回退到原有 Cookie/JWT 会话验证
- 使用示例:
- cURLAuthorization 头):
```bash
curl -H "Authorization: Bearer <JWT_TOKEN>" https://your.domain/api/mailboxes
```
- cURLX-Admin-Token
```bash
curl -H "X-Admin-Token: <JWT_TOKEN>" https://your.domain/api/domains
```
- GETQuery
```
GET /api/session?admin_token=<JWT_TOKEN>
```
- 风险与建议(务必阅读):
- 严格保密 `JWT_TOKEN`,并定期更换
### 🎲 邮箱管理
- `GET /api/generate` - 生成新的临时邮箱
- 返回: `{ "email": "random@domain.com", "expires": timestamp }`
- `GET /api/mailboxes` - 获取历史邮箱列表
- 参数: `limit`(页面大小), `offset`(偏移量)
- 返回: 邮箱列表数组
- `DELETE /api/mailbox/{address}` - 删除指定邮箱
- 返回: `{ "success": true }`
### 📧 邮件操作
- `GET /api/emails?mailbox=email@domain.com` - 获取邮件列表
- 返回: 邮件列表数组,包含发件人、主题、时间等信息
- `GET /api/email/{id}` - 获取邮件详情
- 返回: 完整的邮件内容包括HTML和纯文本
- `DELETE /api/email/{id}` - 删除单个邮件
- 返回: `{ "success": true, "deleted": true, "message": "邮件已删除" }`
- `DELETE /api/emails?mailbox=email@domain.com` - 清空邮箱所有邮件
- 返回: `{ "success": true, "deletedCount": 5, "previousCount": 5 }`
### 🔐 认证相关
- `POST /api/login` - 用户登录
- 参数: `{ "username": "用户名", "password": "密码" }`
- 返回: `{ success: true, role, can_send, mailbox_limit }` 并设置会话 Cookie
- `POST /api/logout` - 用户退出
- 返回: `{ "success": true }`
### 🔧 系统接口
- `GET /api/domains` - 获取可用域名列表
- 返回: 域名数组
### 👤 用户管理(管理后台)
- `GET /api/users` - 获取用户列表
- 返回: 用户数组(含 id/username/role/mailbox_limit/can_send/mailbox_count/created_at
- `GET /api/users/{userId}/mailboxes` - 获取指定用户的邮箱列表
- 返回: 邮箱数组address/created_at
- `POST /api/users` - 创建用户
- 参数: `{ username, password, role }`role: `user` | `admin`
- 返回: `{ success: true }`
- `PATCH /api/users/{userId}` - 更新用户
- 参数示例: `{ username?, password?, mailboxLimit?, can_send?, role? }`
- 返回: `{ success: true }`
- `DELETE /api/users/{userId}` - 删除用户
- 返回: `{ success: true }`
- `POST /api/users/assign` - 给用户分配邮箱
- 参数: `{ username, address }`
- 返回: `{ success: true }`

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env python3
# download_sdk.py
"""下载 OpenAI Sentinel SDK"""
import requests
from pathlib import Path
SDK_URL = "https://sentinel.openai.com/sentinel/97790f37/sdk.js"
OUTPUT_PATH = Path("assets/sdk.js")
def download_sdk():
print(f"Downloading SDK from {SDK_URL}...")
OUTPUT_PATH.parent.mkdir(exist_ok=True)
resp = requests.get(SDK_URL)
resp.raise_for_status()
with open(OUTPUT_PATH, 'wb') as f:
f.write(resp.content)
print(f"✓ SDK saved to {OUTPUT_PATH}")
print(f" Size: {len(resp.content) / 1024:.1f} KB")
if __name__ == '__main__':
download_sdk()

View File

@@ -5,7 +5,7 @@ from typing import Dict, Optional
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
from urllib.parse import unquote
from urllib.parse import unquote, urlparse
from .fingerprint import BrowserFingerprint
from config import HTTP_CONFIG, DEBUG
@@ -36,7 +36,10 @@ class HTTPClient:
adapter = TLSAdapter()
self.session.mount('https://', adapter)
self.cookies = fingerprint.get_cookies()
# Use the session cookie jar as the single source of truth so that
# cookies set on redirects (resp.history) are not lost.
self.session.cookies.update(fingerprint.get_cookies())
self.cookies = self.session.cookies
self.csrf_token = None
# 确保自动解压
self.session.headers.update({
@@ -45,9 +48,9 @@ class HTTPClient:
'Accept-Encoding': 'gzip, deflate',
})
def extract_csrf_from_cookies(self) -> Optional[str]:
"""从 cookies 中提取 CSRF token"""
csrf_cookie = self.cookies.get('__Host-next-auth.csrf-token')
def extract_csrf_from_cookies(self, domain: str = "auth.openai.com") -> Optional[str]:
"""从 cookies 中提取 CSRF token(按域名)"""
csrf_cookie = self.cookies.get('__Host-next-auth.csrf-token', domain=domain)
if not csrf_cookie:
return None
@@ -75,12 +78,10 @@ class HTTPClient:
print(f"[HTTP] GET {url}")
resp = self.session.get(url, **kwargs)
# 更新 cookies
self.cookies.update(resp.cookies.get_dict())
# 尝试提取 CSRF
csrf = self.extract_csrf_from_cookies()
# 尝试提取 CSRF按请求域名
domain = urlparse(url).hostname or "auth.openai.com"
csrf = self.extract_csrf_from_cookies(domain=domain)
if csrf:
self.csrf_token = csrf
@@ -90,7 +91,7 @@ class HTTPClient:
return resp
def get_csrf_token(self) -> Optional[str]:
def get_csrf_token(self, domain: str = "auth.openai.com") -> Optional[str]:
"""
获取 CSRF token
@@ -100,7 +101,7 @@ class HTTPClient:
3. 如果都失败,返回 None某些流程可能不需要
"""
# 1. 从 cookie 提取
csrf = self.extract_csrf_from_cookies()
csrf = self.extract_csrf_from_cookies(domain=domain)
if csrf:
self.csrf_token = csrf
return csrf
@@ -110,7 +111,7 @@ class HTTPClient:
print("[HTTP] Requesting CSRF token from /api/auth/csrf...")
try:
url = "https://auth.openai.com/api/auth/csrf"
url = f"https://{domain}/api/auth/csrf"
resp = self.session.get(
url,
@@ -119,11 +120,8 @@ class HTTPClient:
timeout=HTTP_CONFIG['timeout']
)
# 更新 cookies
self.cookies.update(resp.cookies.get_dict())
# 尝试从 cookie 提取
csrf = self.extract_csrf_from_cookies()
csrf = self.extract_csrf_from_cookies(domain=domain)
if csrf:
self.csrf_token = csrf
return csrf

114
modules/pow_solver.py Normal file
View File

@@ -0,0 +1,114 @@
import time
import json
import base64
from typing import List
DEBUG = True
class ProofOfWorkSolver:
"""解决 OpenAI Sentinel 的 Proof of Work challenge"""
def __init__(self):
# FNV-1a 常量
self.FNV_OFFSET = 2166136261
self.FNV_PRIME = 16777619
def fnv1a_hash(self, data: str) -> str:
"""FNV-1a hash 算法"""
hash_value = self.FNV_OFFSET
for char in data:
hash_value ^= ord(char)
hash_value = (hash_value * self.FNV_PRIME) & 0xFFFFFFFF
# 额外的混合步骤(从 JS 代码复制)
hash_value ^= hash_value >> 16
hash_value = (hash_value * 2246822507) & 0xFFFFFFFF
hash_value ^= hash_value >> 13
hash_value = (hash_value * 3266489909) & 0xFFFFFFFF
hash_value ^= hash_value >> 16
# 转为 8 位十六进制字符串
return format(hash_value, '08x')
def serialize_array(self, arr: List) -> str:
"""模拟 JS 的 T() 函数JSON.stringify + Base64"""
json_str = json.dumps(arr, separators=(',', ':'))
return base64.b64encode(json_str.encode()).decode()
def build_fingerprint_array(self, nonce: int, elapsed_ms: int) -> List:
"""构建指纹数组(简化版)"""
return [
0, # [0] screen dimensions
"", # [1] timestamp
0, # [2] memory
nonce, # [3] nonce ← 关键
"", # [4] user agent
"", # [5] random element
"", # [6] script src
"", # [7] language
"", # [8] languages
elapsed_ms, # [9] elapsed time ← 关键
"", # [10] random function
"", # [11] keys
"", # [12] window keys
0, # [13] performance.now()
"", # [14] uuid
"", # [15] URL params
0, # [16] hardware concurrency
0 # [17] timeOrigin
]
def solve(self, seed: str, difficulty: str, max_iterations: int = 10000000) -> str:
"""
解决 PoW challenge
Args:
seed: Challenge seed
difficulty: 目标难度(十六进制字符串)
max_iterations: 最大尝试次数
Returns:
序列化的答案(包含 nonce
"""
if DEBUG:
print(f"[PoW] Solving challenge:")
print(f" Seed: {seed}")
print(f" Difficulty: {difficulty}")
start_time = time.time()
for nonce in range(max_iterations):
elapsed_ms = int((time.time() - start_time) * 1000)
# 构建指纹数组
fingerprint = self.build_fingerprint_array(nonce, elapsed_ms)
# 序列化
serialized = self.serialize_array(fingerprint)
# 计算 hash(seed + serialized)
hash_input = seed + serialized
hash_result = self.fnv1a_hash(hash_input)
# 检查是否满足难度要求
# 比较方式hash 的前 N 位(作为整数)<= difficulty作为整数
difficulty_len = len(difficulty)
hash_prefix = hash_result[:difficulty_len]
if hash_prefix <= difficulty:
elapsed = time.time() - start_time
if DEBUG:
print(f"[PoW] ✓ Found solution in {elapsed:.2f}s")
print(f" Nonce: {nonce}")
print(f" Hash: {hash_result}")
print(f" Serialized: {serialized[:100]}...")
# 返回 serialized + "~S" (表示成功)
return serialized + "~S"
# 每 100k 次迭代打印进度
if DEBUG and nonce > 0 and nonce % 100000 == 0:
print(f"[PoW] Tried {nonce:,} iterations...")
raise Exception(f"Failed to solve PoW after {max_iterations:,} iterations")

View File

@@ -10,7 +10,7 @@ from .fingerprint import BrowserFingerprint
from .sentinel_solver import SentinelSolver
from .http_client import HTTPClient
from config import AUTH_BASE_URL, DEBUG
from modules.pow_solver import ProofOfWorkSolver
class OpenAIRegistrar:
"""完整的 OpenAI 注册流程"""
@@ -19,8 +19,9 @@ class OpenAIRegistrar:
self.fingerprint = BrowserFingerprint(session_id)
self.solver = SentinelSolver(self.fingerprint)
self.http_client = HTTPClient(self.fingerprint)
self.pow_solver = ProofOfWorkSolver() # 新增
def _step1_init_through_chatgpt(self):
def _step1_init_through_chatgpt(self, email: str):
"""通过 ChatGPT web 初始化注册流程"""
if DEBUG:
print("\n=== STEP 1: Initializing through ChatGPT web ===")
@@ -36,12 +37,9 @@ class OpenAIRegistrar:
resp = self.http_client.session.get(
chatgpt_url,
headers=headers,
cookies=self.http_client.cookies,
timeout=30
)
self.http_client.cookies.update(resp.cookies.get_dict())
if DEBUG:
print(f"[1.1] Visited ChatGPT: {resp.status_code}")
print(f" Cookies: {list(self.http_client.cookies.keys())}")
@@ -57,12 +55,9 @@ class OpenAIRegistrar:
resp = self.http_client.session.get(
csrf_url,
headers=csrf_headers,
cookies=self.http_client.cookies,
timeout=30
)
self.http_client.cookies.update(resp.cookies.get_dict())
if DEBUG:
print(f"[1.2] CSRF API response: {resp.status_code}")
@@ -103,7 +98,7 @@ class OpenAIRegistrar:
'ext-oai-did': self.fingerprint.session_id,
'auth_session_logging_id': auth_session_logging_id,
'screen_hint': 'signup', # 明确指定注册
'login_hint': '', # 先留空,让服务器决定
'login_hint': email, # 🔥 关键:传入邮箱
}
signin_data = {
@@ -124,7 +119,6 @@ class OpenAIRegistrar:
params=signin_params,
data=signin_data,
headers=signin_headers,
cookies=self.http_client.cookies,
allow_redirects=False,
timeout=30
)
@@ -133,7 +127,6 @@ class OpenAIRegistrar:
if DEBUG:
print(f"[1.3] NextAuth response: {resp.status_code}")
self.http_client.cookies.update(resp.cookies.get_dict())
if DEBUG:
content_type = resp.headers.get("Content-Type", "")
content_encoding = resp.headers.get("Content-Encoding", "")
@@ -189,18 +182,23 @@ class OpenAIRegistrar:
resp = self.http_client.session.get(
oauth_url,
headers=auth_headers,
cookies=self.http_client.cookies,
allow_redirects=True, # 自动跟随重定向
timeout=30
)
self.http_client.cookies.update(resp.cookies.get_dict())
if DEBUG:
print(f" Final status: {resp.status_code}")
print(f" Final URL: {resp.url if hasattr(resp, 'url') else 'N/A'}")
print(f"[1.5] Cookies after OAuth:")
print(f" {list(self.http_client.cookies.keys())}")
if resp.history:
print(" Redirect chain cookies:")
for i, h in enumerate(resp.history, 1):
cookies_set = list(h.cookies.keys())
print(f" {i}. {h.status_code} {h.url} -> set {cookies_set}")
auth_cookie_names = list(self.http_client.session.cookies.get_dict(domain='auth.openai.com').keys())
if auth_cookie_names:
print(f" auth.openai.com cookies: {sorted(auth_cookie_names)}")
# 检查必需的 cookies
required_cookies = ['login_session', 'oai-did']
@@ -215,6 +213,15 @@ class OpenAIRegistrar:
if DEBUG:
print(f" ✓ All required cookies present")
# 1.6 确保获取 auth.openai.com 的 CSRF用于后续 /register 请求)
try:
auth_csrf = self.http_client.get_csrf_token(domain="auth.openai.com")
if DEBUG and auth_csrf:
print(f"[1.6] ✓ auth CSRF: {auth_csrf[:20]}...")
except Exception as e:
if DEBUG:
print(f"[1.6] Warning: Failed to get auth CSRF: {e}")
@@ -235,96 +242,349 @@ class OpenAIRegistrar:
if DEBUG:
print(f"✗ Sentinel initialization failed: {e}")
raise
def _step3_attempt_register(self, email: str, password: str) -> Dict:
"""尝试注册(会触发 enforcement"""
def _step2_5_submit_sentinel(self):
"""Step 2.5: 提交 Sentinel token 到 OpenAI"""
if DEBUG:
print("\n=== STEP 3: Attempting registration ===")
print("\n=== STEP 2.5: Submitting Sentinel token ===")
url = f"{AUTH_BASE_URL}/api/accounts/user/register"
# 如果 sentinel_token 是字符串,先解析
if isinstance(self.sentinel_token, str):
token_data = json.loads(self.sentinel_token)
else:
token_data = self.sentinel_token
url = "https://sentinel.openai.com/backend-api/sentinel/req"
# 正确的 payload 格式(根据真实抓包)
payload = {
'username': email,
'password': password
"p": token_data['p'],
"id": self.fingerprint.session_id,
"flow": "username_password_create"
}
# 添加额外的 headers
headers = self.http_client.fingerprint.get_headers(with_sentinel=self.sentinel_token)
headers['Accept'] = 'application/json'
headers['Referer'] = f'{AUTH_BASE_URL}/create-account/password'
headers['Origin'] = AUTH_BASE_URL
headers = self.http_client.fingerprint.get_headers(host='sentinel.openai.com')
headers['Content-Type'] = 'text/plain;charset=UTF-8' # 注意text/plain
headers['Origin'] = 'https://sentinel.openai.com'
headers['Referer'] = 'https://sentinel.openai.com/backend-api/sentinel/frame.html'
headers['Accept'] = '*/*'
headers['Sec-Fetch-Site'] = 'same-origin'
headers['Sec-Fetch-Mode'] = 'cors'
headers['Sec-Fetch-Dest'] = 'empty'
# 添加 Datadog tracing headers模拟真实浏览器
headers.update({
'traceparent': f'00-0000000000000000{secrets.token_hex(8)}-{secrets.token_hex(8)}-01',
'tracestate': 'dd=s:1;o:rum',
'x-datadog-origin': 'rum',
'x-datadog-parent-id': str(secrets.randbits(63)),
'x-datadog-sampling-priority': '1',
'x-datadog-trace-id': str(secrets.randbits(63))
})
if DEBUG:
print(f"[2.5] POST {url}")
resp = self.http_client.session.post(
url,
json=payload,
headers=headers,
cookies=self.http_client.cookies,
timeout=30
)
if DEBUG:
print(f" Status: {resp.status_code}")
if resp.status_code != 200:
raise Exception(f"Failed to submit Sentinel token: {resp.status_code} {resp.text}")
try:
data = resp.json()
if DEBUG:
print(f" ✓ Sentinel accepted")
print(f" Persona: {data.get('persona')}")
print(f" Token expires in: {data.get('expire_after')}s")
# 检查是否需要额外验证
if data.get('turnstile', {}).get('required'):
print(f" ⚠ Turnstile required")
if data.get('proofofwork', {}).get('required'):
print(f" ⚠ Proof of Work required (difficulty: {data.get('proofofwork', {}).get('difficulty')})")
# 保存 token可能后续需要
self.sentinel_response = data
return data
except Exception as e:
if DEBUG:
print(f" ✗ Failed to parse response: {e}")
print(f" Raw: {resp.text[:200]}")
raise
def _step2_6_solve_pow(self):
"""Step 2.6: 解 Proof of Work"""
if DEBUG:
print("\n=== STEP 2.6: Solving Proof of Work ===")
pow_data = self.sentinel_response.get('proofofwork', {})
if not pow_data.get('required'):
if DEBUG:
print("[2.6] PoW not required, skipping")
return None
seed = pow_data.get('seed')
difficulty = pow_data.get('difficulty')
if not seed or not difficulty:
raise Exception(f"Missing PoW parameters")
# 解 PoW
self.pow_answer = self.pow_solver.solve(seed, difficulty)
if DEBUG:
print(f"[2.6] ✓ PoW solved: {self.pow_answer[:50]}...")
return self.pow_answer
def _step2_7_submit_pow(self):
"""Step 2.7: 提交 PoW 答案到 Sentinel"""
if DEBUG:
print("\n=== STEP 2.7: Submitting PoW ===")
url = "https://sentinel.openai.com/backend-api/sentinel/req"
# 再次生成 requirements token或重用之前的
requirements_token = self.sentinel_token.split('"p": "')[1].split('"')[0]
payload = {
"p": requirements_token,
"id": self.fingerprint.session_id,
"answer": self.pow_answer
}
headers = self.http_client.fingerprint.get_headers(host='sentinel.openai.com')
headers['Content-Type'] = 'text/plain;charset=UTF-8'
headers['Origin'] = 'https://sentinel.openai.com'
headers['Referer'] = 'https://sentinel.openai.com/backend-api/sentinel/frame.html'
# 补全 Sec-Fetch-* headers
headers['Sec-Fetch-Dest'] = 'empty'
headers['Sec-Fetch-Mode'] = 'cors'
headers['Sec-Fetch-Site'] = 'same-origin'
if DEBUG:
print(f"[2.7] POST {url}")
print(f" Answer: {self.pow_answer[:50]}...")
resp = self.http_client.session.post(
url,
json=payload,
headers=headers,
timeout=30
)
if DEBUG:
print(f" Status: {resp.status_code}")
print(f" Response: {resp.text}")
if resp.status_code != 200:
# 如果是 403打印完整响应以便调试
if DEBUG and resp.status_code == 403:
print(f" ✗ Cloudflare blocked")
print(f" Cookies sent:")
for cookie in self.http_client.session.cookies:
if cookie.domain in ['sentinel.openai.com', '.chatgpt.com']:
print(f" {cookie.name}={cookie.value[:30]}...")
raise Exception(f"Failed to submit PoW: {resp.status_code}")
# 解析响应
result = resp.json()
if DEBUG:
print(f" ✓ PoW accepted")
if 'token' in result:
print(f" Token: {result['token'][:50]}...")
if 'turnstile' in result:
print(f" ⚠ Turnstile still required")
# 保存最终响应
self.sentinel_response = result
# 保存 oai-sc cookie如果返回了
if 'Set-Cookie' in resp.headers or any('oai-sc' in c.name for c in self.http_client.session.cookies):
if DEBUG:
print(f" ✓ oai-sc cookie received")
return result
def _step3_attempt_register(self, email: str, password: str) -> Optional[Dict]:
"""尝试注册(不验证邮箱)"""
if DEBUG:
print("\n=== STEP 3: Attempting registration ===")
# 🔍 调试:检查 oai-client-auth-session cookie 的内容
if DEBUG:
import base64
import json
auth_session_cookie = self.http_client.cookies.get('oai-client-auth-session', '')
if auth_session_cookie:
try:
# 解码 cookie格式base64.签名)
cookie_data = auth_session_cookie.split('.')[0]
decoded = base64.b64decode(cookie_data + '==') # 补齐 padding
session_data = json.loads(decoded)
print(f"[3.0] oai-client-auth-session content:")
print(f" session_id: {session_data.get('session_id', 'N/A')}")
print(f" username: {session_data.get('username', 'N/A')}")
print(f" email: {session_data.get('email', 'N/A')}")
print(f" openai_client_id: {session_data.get('openai_client_id', 'N/A')}")
# 检查是否有 username/email 字段
if 'username' not in session_data and 'email' not in session_data:
print(f" ⚠ WARNING: No username/email in session!")
print(f" This is likely why we get 'Invalid session'")
except Exception as e:
print(f"[3.0] Failed to decode oai-client-auth-session: {e}")
else:
print(f"[3.0] ⚠ oai-client-auth-session cookie not found!")
# 3.1 提取 CSRF token从 oai-login-csrf_dev_* cookie
# auth_csrf_token = None
# for cookie_name, cookie_value in self.http_client.cookies.items():
# if cookie_name.startswith('oai-login-csrf_dev_'):
# auth_csrf_token = cookie_value
# if DEBUG:
# print(f"[3.1] ✓ Extracted CSRF: {cookie_name}")
# break
# if not auth_csrf_token:
# if DEBUG:
# print(f"[3.1] ✗ No oai-login-csrf_dev_* cookie found")
# print(f" Available cookies: {list(self.http_client.cookies.keys())}")
# raise Exception("No CSRF token available for Step 3")
# 3.2 准备注册请求(正确的 payload
url = "https://auth.openai.com/api/accounts/user/register"
payload = {
"username": email, # 不是 "email"
"password": password,
}
# 3.3 准备 headers完全匹配真实请求
headers = self.http_client.fingerprint.get_headers(host='auth.openai.com')
# 添加 Sentinel token
if hasattr(self, 'sentinel_token') and self.sentinel_token:
headers['Openai-Sentinel-Token'] = self.sentinel_token # 注意大小写
headers.update({
'Content-Type': 'application/json',
'Accept': 'application/json',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Origin': 'https://auth.openai.com',
'Referer': 'https://auth.openai.com/create-account/password', # 注意是 /password
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Priority': 'u=1, i',
})
# 添加 Datadog tracing headers匹配真实请求
import secrets
headers.update({
'X-Datadog-Trace-Id': str(secrets.randbits(63)),
'X-Datadog-Parent-Id': str(secrets.randbits(63)),
'X-Datadog-Sampling-Priority': '1',
'X-Datadog-Origin': 'rum',
'Traceparent': f'00-0000000000000000{secrets.token_hex(8)}-{secrets.token_hex(8)}-01',
'Tracestate': 'dd=s:1;o:rum',
})
# 3.4 准备 cookies使用所有 auth.openai.com 的 cookies
# 不要手动过滤,让 requests 自动处理域名匹配
if DEBUG:
print(f"[3.2] Request URL: {url}")
print(f"[3.3] Payload: {payload}")
print(f"[3.4] Cookies to send: {list(self.http_client.cookies.keys())}")
# 检查关键 cookies
required = ['login_session', 'oai-client-auth-session', 'auth-session-minimized']
missing = [c for c in required if c not in self.http_client.cookies]
if missing:
print(f" ⚠ WARNING: Missing critical cookies: {missing}")
# 3.5 发送注册请求
resp = self.http_client.session.post(
url,
json=payload,
headers=headers,
# 不手动指定 cookies让 session 自动处理
timeout=30
)
if DEBUG:
print(f"Response status: {resp.status_code}")
# 更新 cookies
self.http_client.cookies.update(resp.cookies.get_dict())
# 403: Enforcement 挑战(预期)
if resp.status_code == 403:
try:
challenge = resp.json()
if DEBUG:
print(f"✓ Got enforcement challenge!")
print(f" Type: {challenge.get('type', 'unknown')}")
if 'proofofwork' in challenge:
pow_data = challenge['proofofwork']
print(f" PoW seed: {pow_data.get('seed', 'N/A')}")
print(f" Difficulty: {pow_data.get('difficulty', 'N/A')}")
return challenge
# 3.6 处理响应
try:
data = resp.json()
except Exception as e:
if resp.status_code == 200:
if DEBUG:
print(f"✗ Failed to parse challenge: {e}")
print(resp.text[:500])
raise
print(f"✓ Registration successful!")
print(f" Response: {data}")
# 检查是否需要邮箱验证
continue_url = data.get('continue_url', '')
if 'email-otp' in continue_url:
if DEBUG:
print(f" → Next step: Email OTP verification")
return {
'success': True,
'requires_verification': True,
'continue_url': continue_url,
'method': data.get('method', 'GET'),
'data': data
}
else:
return {'success': True, 'data': data}
elif resp.status_code == 409:
error = data.get('error', {})
error_code = error.get('code', '')
if DEBUG:
print(f"✗ 409 Conflict: {error}")
if error_code == 'invalid_state':
# Session 无效
raise Exception(f"Invalid session: {error}")
elif error_code == 'email_taken' or 'already' in str(error).lower():
# 邮箱已被使用
if DEBUG:
print(f" Email already registered")
return None
else:
raise Exception(f"Registration conflict: {data}")
elif resp.status_code == 400:
if DEBUG:
print(f"✗ 400 Bad Request: {data}")
raise Exception(f"Bad request: {data}")
else:
if DEBUG:
print(f"✗ Unexpected status: {data}")
raise Exception(f"Registration failed with {resp.status_code}: {data}")
# 200: 成功
elif resp.status_code == 200:
except json.JSONDecodeError as e:
if DEBUG:
print("✓ Registration succeeded!")
return {'success': True, 'data': resp.json()}
print(f"✗ Failed to parse JSON response")
print(f" Content: {resp.text[:500]}")
raise Exception(f"Invalid JSON response: {e}")
# 409: Invalid session
elif resp.status_code == 409:
error = resp.json()
except Exception as e:
if DEBUG:
print(f"409: {error}")
raise Exception(f"Invalid session: {error}")
# 400: 参数错误
elif resp.status_code == 400:
error = resp.json()
if DEBUG:
print(f"✗ 400: {error}")
raise Exception(f"Bad request: {error}")
# 其他错误
else:
if DEBUG:
print(f"✗ Unexpected status: {resp.status_code}")
print(resp.text[:500])
raise Exception(f"Unexpected status: {resp.status_code}\n{resp.text}")
print(f"Exception: {e}")
raise
@@ -384,10 +644,13 @@ class OpenAIRegistrar:
try:
# Step 1: 通过 ChatGPT web 初始化(获取正确的 session
self._step1_init_through_chatgpt()
self._step1_init_through_chatgpt(email)
# Step 2: 初始化 Sentinel
self._step2_init_sentinel()
self._step2_5_submit_sentinel()
self._step2_6_solve_pow()
self._step2_7_submit_pow()
# Step 3: 尝试注册(触发 enforcement
challenge = self._step3_attempt_register(email, password)

View File

@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"curl-cffi>=0.14.0",
"playwright>=1.57.0",
"pyexecjs>=1.5.1",
"requests>=2.32.5",
]

81
uv.lock generated
View File

@@ -8,6 +8,7 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "curl-cffi" },
{ name = "playwright" },
{ name = "pyexecjs" },
{ name = "requests" },
]
@@ -15,6 +16,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "curl-cffi", specifier = ">=0.14.0" },
{ name = "playwright", specifier = ">=1.57.0" },
{ name = "pyexecjs", specifier = ">=1.5.1" },
{ name = "requests", specifier = ">=2.32.5" },
]
@@ -165,6 +167,45 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/7c/d2ba86b0b3e1e2830bd94163d047de122c69a8df03c5c7c36326c456ad82/curl_cffi-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:2eed50a969201605c863c4c31269dfc3e0da52916086ac54553cfa353022425c", size = 1425067, upload-time = "2025-12-16T03:25:06.454Z" },
]
[[package]]
name = "greenlet"
version = "3.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
{ url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" },
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
{ url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" },
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" },
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" },
{ url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" },
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" },
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" },
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" },
{ url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" },
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" },
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" },
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" },
{ url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" },
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" },
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" },
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" },
{ url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" },
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" },
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" },
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },
]
[[package]]
name = "idna"
version = "3.11"
@@ -174,6 +215,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "playwright"
version = "1.57.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet" },
{ name = "pyee" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/b6/e17543cea8290ae4dced10be21d5a43c360096aa2cce0aa7039e60c50df3/playwright-1.57.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:9351c1ac3dfd9b3820fe7fc4340d96c0d3736bb68097b9b7a69bd45d25e9370c", size = 41985039, upload-time = "2025-12-09T08:06:18.408Z" },
{ url = "https://files.pythonhosted.org/packages/8b/04/ef95b67e1ff59c080b2effd1a9a96984d6953f667c91dfe9d77c838fc956/playwright-1.57.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4a9d65027bce48eeba842408bcc1421502dfd7e41e28d207e94260fa93ca67e", size = 40775575, upload-time = "2025-12-09T08:06:22.105Z" },
{ url = "https://files.pythonhosted.org/packages/60/bd/5563850322a663956c927eefcf1457d12917e8f118c214410e815f2147d1/playwright-1.57.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:99104771abc4eafee48f47dac2369e0015516dc1ce8c409807d2dd440828b9a4", size = 41985042, upload-time = "2025-12-09T08:06:25.357Z" },
{ url = "https://files.pythonhosted.org/packages/56/61/3a803cb5ae0321715bfd5247ea871d25b32c8f372aeb70550a90c5f586df/playwright-1.57.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:284ed5a706b7c389a06caa431b2f0ba9ac4130113c3a779767dda758c2497bb1", size = 45975252, upload-time = "2025-12-09T08:06:29.186Z" },
{ url = "https://files.pythonhosted.org/packages/83/d7/b72eb59dfbea0013a7f9731878df8c670f5f35318cedb010c8a30292c118/playwright-1.57.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a1bae6c0a07839cdeaddbc0756b3b2b85e476c07945f64ece08f1f956a86f1", size = 45706917, upload-time = "2025-12-09T08:06:32.549Z" },
{ url = "https://files.pythonhosted.org/packages/e4/09/3fc9ebd7c95ee54ba6a68d5c0bc23e449f7235f4603fc60534a364934c16/playwright-1.57.0-py3-none-win32.whl", hash = "sha256:1dd93b265688da46e91ecb0606d36f777f8eadcf7fbef12f6426b20bf0c9137c", size = 36553860, upload-time = "2025-12-09T08:06:35.864Z" },
{ url = "https://files.pythonhosted.org/packages/58/d4/dcdfd2a33096aeda6ca0d15584800443dd2be64becca8f315634044b135b/playwright-1.57.0-py3-none-win_amd64.whl", hash = "sha256:6caefb08ed2c6f29d33b8088d05d09376946e49a73be19271c8cd5384b82b14c", size = 36553864, upload-time = "2025-12-09T08:06:38.915Z" },
{ url = "https://files.pythonhosted.org/packages/6a/60/fe31d7e6b8907789dcb0584f88be741ba388413e4fbce35f1eba4e3073de/playwright-1.57.0-py3-none-win_arm64.whl", hash = "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", size = 32837940, upload-time = "2025-12-09T08:06:42.268Z" },
]
[[package]]
name = "pycparser"
version = "2.23"
@@ -183,6 +243,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "pyee"
version = "13.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" },
]
[[package]]
name = "pyexecjs"
version = "1.5.1"
@@ -216,6 +288,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "urllib3"
version = "2.6.3"