fix(register): align live sentinel flow with successful HAR

This commit is contained in:
Logic
2026-04-07 13:39:01 +08:00
parent ba02799b18
commit 3c6fce8d57
13 changed files with 1784 additions and 51 deletions

View File

@@ -1,28 +1,27 @@
from __future__ import annotations
"""Pure HTTP registration experiment with detailed auth-state diagnostics."""
import os
import re
import uuid
from pathlib import Path
from typing import Optional
from urllib.parse import unquote, urlencode, urljoin, urlparse
try:
from .http_client import HTTPClient
from .sentinel_solver import SentinelSolver
from .vmail_client import BaseMailClient
except ImportError: # pragma: no cover - allow direct script execution from source tree
from http_client import HTTPClient
from sentinel_solver import SentinelSolver
from vmail_client import BaseMailClient
class ChatGPTRegisterHTTPReverse:
def __init__(self, http: HTTPClient, mail: BaseMailClient):
def __init__(self, http: HTTPClient, mail: BaseMailClient, sentinel_solver: SentinelSolver | None = None):
self.http = http
self.mail = mail
self.base = "https://chatgpt.com"
self.auth_base = "https://auth.openai.com"
self.module_dir = Path(__file__).resolve().parent
self.sentinel_solver = sentinel_solver or SentinelSolver(http)
def _cookie_rows(self) -> list[tuple[str, str, str]]:
rows = []
@@ -79,27 +78,14 @@ class ChatGPTRegisterHTTPReverse:
except Exception:
return None
def _load_captured_sentinel(self, flow_name: str) -> str:
candidates = [
self.module_dir / "nodatadog.js",
self.module_dir.parent / "nodatadog.js",
Path.cwd() / "nodatadog.js",
]
content = ""
for path in candidates:
try:
content = path.read_text(encoding="utf-8")
break
except FileNotFoundError:
continue
if not content:
def _build_live_sentinel_token(self, flow_name: str) -> str:
return self.sentinel_solver.build_token(flow_name)
def _build_live_session_observer_token(self, flow_name: str) -> str:
build_session_observer_token = getattr(self.sentinel_solver, "build_session_observer_token", None)
if not callable(build_session_observer_token):
return ""
pattern = re.compile(r'"openai-sentinel-token": "((?:[^"\\]|\\.)*flow\\"\\"%s(?:[^"\\]|\\.)*)"' % re.escape(flow_name))
match = pattern.search(content)
if not match:
return ""
# Keep the captured literal as-is. The request header in the capture uses this exact string form.
return match.group(1)
return build_session_observer_token(flow_name)
def _bootstrap_chatgpt(self):
print("[2/9] Bootstrapping chatgpt.com cookies...")
@@ -217,6 +203,47 @@ class ChatGPTRegisterHTTPReverse:
def _register_endpoint(self, auth_url: str) -> str:
return f"{self._accounts_api_base(auth_url)}/user/register"
def _authorize_entry_page(self, auth_page_url: str) -> str:
if "log-in-or-create-account" in auth_page_url:
return auth_page_url
return f"{self.auth_base}/log-in-or-create-account?usernameKind=email"
def _authorize_continue(
self,
accounts_api_base: str,
auth_page_url: str,
email: str,
sentinel: str = "",
) -> str:
print("[5/10] Calling authorize/continue before register...")
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Origin": self.auth_base,
"Referer": auth_page_url,
"sec-fetch-site": "same-origin",
"sec-fetch-mode": "cors",
"sec-fetch-dest": "empty",
}
if sentinel:
headers["openai-sentinel-token"] = sentinel
response = self.http.request(
"POST",
f"{accounts_api_base}/authorize/continue",
json={
"username": {"kind": "email", "value": email},
"screen_hint": "login_or_signup",
},
headers=headers,
)
data = self._json_or_none(response) or {}
print(f" Authorize-continue status: {response.status_code} sentinel={bool(sentinel)}")
if response.status_code != 200 or not data.get("continue_url"):
raise RuntimeError(f"Authorize continue blocked: {self._response_excerpt(response)}")
continue_url = urljoin(auth_page_url, data["continue_url"])
print(f" Authorize continue URL: {continue_url}")
return self._open_continue_url(continue_url, auth_page_url, "[5/10]")
def _attempt_register(self, register_url: str, auth_page_url: str, email: str, password: str, sentinel: str = ""):
headers = {
"Content-Type": "application/json",
@@ -301,7 +328,14 @@ class ChatGPTRegisterHTTPReverse:
self._print_cookie_summary("After validate")
return data["continue_url"]
def _attempt_create_account(self, create_account_url: str, name: str, referer: str, sentinel: str = ""):
def _attempt_create_account(
self,
create_account_url: str,
name: str,
referer: str,
sentinel: str = "",
sentinel_so: str = "",
):
print("[8/9] Creating account profile via pure HTTP...")
headers = {
"Content-Type": "application/json",
@@ -311,6 +345,8 @@ class ChatGPTRegisterHTTPReverse:
}
if sentinel:
headers["openai-sentinel-token"] = sentinel
if sentinel_so:
headers["openai-sentinel-so-token"] = sentinel_so
response = self.http.request(
"POST",
create_account_url,
@@ -361,29 +397,39 @@ class ChatGPTRegisterHTTPReverse:
auth_url = self._signin_auth0(email, csrf_token)
auth_page_url = self._follow_auth_redirects(auth_url)
authorize_entry_page = self._authorize_entry_page(auth_page_url)
accounts_api_base = self._accounts_api_base(auth_url)
register_url = self._register_endpoint(auth_url)
print(f"[5/9] Pure HTTP register endpoint: {register_url}")
print(f"[6/10] Pure HTTP register endpoint: {register_url}")
response, data = self._attempt_register(register_url, auth_page_url, email, password)
if response.status_code != 200 or not data or not data.get("continue_url"):
captured = self._load_captured_sentinel("username_password_create")
if not captured:
raise RuntimeError(
f"Register failed without sentinel and no captured sentinel found: {self._response_excerpt(response)}"
)
print("[6/9] Retrying register with captured sentinel token...")
response, data = self._attempt_register(register_url, auth_page_url, email, password, sentinel=captured)
print("[5/10] Generating live sentinel token for authorize_continue...")
authorize_sentinel = self._build_live_sentinel_token("authorize_continue")
register_referer = self._authorize_continue(
accounts_api_base,
authorize_entry_page,
email,
sentinel=authorize_sentinel,
)
print("[7/10] Generating live sentinel token for register...")
register_sentinel = self._build_live_sentinel_token("username_password_create")
response, data = self._attempt_register(
register_url,
register_referer,
email,
password,
sentinel=register_sentinel,
)
if response.status_code != 200 or not data or not data.get("continue_url"):
raise RuntimeError(f"Register blocked: {self._response_excerpt(response)}")
continue_url1 = data["continue_url"]
print(f" Continue URL: {continue_url1}")
email_verification_url = self._open_continue_url(continue_url1, auth_page_url, "[6/9]")
email_verification_url = self._open_continue_url(continue_url1, register_referer, "[7/10]")
print("[7/9] Waiting for OTP...")
print("[8/10] Waiting for OTP...")
otp = self.mail.wait_for_otp(mailbox, timeout=120)
print(f" OTP: {otp}")
@@ -393,22 +439,17 @@ class ChatGPTRegisterHTTPReverse:
email_verification_url,
)
about_you_url = self._open_continue_url(continue_url2, email_verification_url, "[8/9]")
about_you_url = self._open_continue_url(continue_url2, email_verification_url, "[9/10]")
print("[10/10] Generating live sentinel token for create_account...")
create_sentinel = self._build_live_sentinel_token("oauth_create_account")
create_so_sentinel = self._build_live_session_observer_token("oauth_create_account")
create_response, create_data = self._attempt_create_account(
f"{accounts_api_base}/create_account",
name,
about_you_url,
sentinel=create_sentinel,
sentinel_so=create_so_sentinel,
)
if create_response.status_code != 200 or not create_data or not create_data.get("continue_url"):
captured = self._load_captured_sentinel("oauth_create_account")
if captured:
print("[9/9] Retrying create_account with captured sentinel token...")
create_response, create_data = self._attempt_create_account(
f"{accounts_api_base}/create_account",
name,
about_you_url,
sentinel=captured,
)
if create_response.status_code != 200 or not create_data or not create_data.get("continue_url"):
raise RuntimeError(f"Create account blocked: {self._response_excerpt(create_response)}")

View File

@@ -8,6 +8,7 @@ class Settings(BaseSettings):
yescaptcha_api_key: str = Field(default="", env="YESCAPTCHA_API_KEY")
socks5_proxy: str = Field(default="", env="SOCKS5_PROXY")
mail_provider: str = Field(default="vmail", env="MAIL_PROVIDER") # "vmail", "mailtm", or "yyds"
yyds_mail_domain: str = Field(default="", env="YYDS_MAIL_DOMAIN")
# Payment info
card_number: str = Field(default="", env="CARD_NUMBER")

429
src/sentinel_runner.js Normal file
View File

@@ -0,0 +1,429 @@
'use strict';
const crypto = require('node:crypto');
const { performance } = require('node:perf_hooks');
const { runFromInputs } = require('./sentinel_vm');
const ERROR_PREFIX = 'wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D';
const MAX_ATTEMPTS = 500000;
const DEFAULT_TIMEOUT_MS = 2000;
const DEFAULT_JS_HEAP_SIZE_LIMIT = 4294967296;
const SENTINEL_BOOTSTRAP_URL = 'https://sentinel.openai.com/backend-api/sentinel/sdk.js';
function encodeBase64Binary(value) {
if (typeof btoa === 'function') {
return btoa(value);
}
return Buffer.from(String(value), 'binary').toString('base64');
}
function encodeUtf8Json(value) {
const json = JSON.stringify(value);
const bytes = new TextEncoder().encode(json);
return encodeBase64Binary(String.fromCharCode(...bytes));
}
function buildFailureMessage(error) {
return ERROR_PREFIX + encodeUtf8Json(String(error ?? 'e'));
}
function hashHex(input) {
let hash = 2166136261;
for (let index = 0; index < input.length; index += 1) {
hash ^= input.charCodeAt(index);
hash = Math.imul(hash, 16777619) >>> 0;
}
hash ^= hash >>> 16;
hash = Math.imul(hash, 2246822507) >>> 0;
hash ^= hash >>> 13;
hash = Math.imul(hash, 3266489909) >>> 0;
hash ^= hash >>> 16;
return (hash >>> 0).toString(16).padStart(8, '0');
}
function randomPick(items) {
if (!Array.isArray(items) || items.length === 0) {
return undefined;
}
return items[Math.floor(Math.random() * items.length)];
}
function makeStorage() {
const store = new Map();
return {
length: 0,
getItem(key) {
return store.has(String(key)) ? store.get(String(key)) : null;
},
setItem(key, value) {
store.set(String(key), String(value));
this.length = store.size;
},
removeItem(key) {
store.delete(String(key));
this.length = store.size;
},
clear() {
store.clear();
this.length = 0;
},
};
}
function makeElement(tagName, attributes = {}) {
return {
tagName: String(tagName).toUpperCase(),
style: {},
children: [],
hidden: false,
visibility: 'visible',
ariaHidden: 'false',
innerText: '',
textContent: '',
...attributes,
appendChild(child) {
this.children.push(child);
return child;
},
removeChild(child) {
this.children = this.children.filter(item => item !== child);
},
getBoundingClientRect() {
return {
x: 0,
y: 0,
top: 0,
left: 0,
right: 84,
bottom: 16,
width: 84,
height: 16,
};
},
getAttribute(name) {
return this.attributes?.[name] ?? null;
},
};
}
function parseAcceptLanguage(value) {
const raw = String(value || 'zh-CN,zh;q=0.9,en;q=0.8');
const entries = raw.split(',').map(item => item.trim()).filter(Boolean);
const languages = entries.map(item => item.split(';', 1)[0]);
return {
language: languages[0] ?? 'zh-CN',
languages: languages.length > 0 ? languages : ['zh-CN', 'zh', 'en-US', 'en'],
};
}
function buildNavigator(options) {
const { language, languages } = parseAcceptLanguage(options.accept_language);
const navigatorProto = {
appName: 'Netscape',
mediaDevices: '[object MediaDevices]',
serviceWorker: '[object ServiceWorkerContainer]',
userActivation: '[object UserActivation]',
connection: '[object NetworkInformation]',
pdfViewerEnabled: true,
};
return Object.assign(Object.create(navigatorProto), {
userAgent:
options.user_agent ||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
language,
languages,
platform: options.platform || 'MacIntel',
vendor: options.vendor || 'Google Inc.',
deviceMemory: Number(options.device_memory || 8),
hardwareConcurrency: Number(options.hardware_concurrency || 8),
maxTouchPoints: Number(options.max_touch_points || 0),
webdriver: false,
});
}
function createRuntimeEnvironment(options = {}) {
const sdkUrl = options.sdk_url || SENTINEL_BOOTSTRAP_URL;
const sdkVersion = options.sdk_version || '20260219f9f6';
const search = options.location_search || `?sv=${sdkVersion}`;
const screenWidth = Number(options.screen_width || 1920);
const screenHeight = Number(options.screen_height || 1080);
const navigatorRef = buildNavigator(options);
const body = makeElement('body');
body.clientWidth = 1280;
body.clientHeight = 720;
const documentElement = makeElement('html', {
attributes: { 'data-build': options.data_build ?? null },
});
documentElement.clientWidth = 1280;
documentElement.clientHeight = 720;
const documentRef = {
body,
head: makeElement('head'),
documentElement,
referrer: options.referrer || '',
cookie: options.cookie || '',
visibilityState: 'visible',
scripts: [{ src: sdkUrl }],
createElement(tag) {
return makeElement(tag);
},
addEventListener() {},
removeEventListener() {},
};
const locationRef = {
href: `https://sentinel.openai.com/backend-api/sentinel/frame.html${search}`,
search,
};
const windowRef = {
Reflect,
Object,
Math,
Date,
JSON,
URL,
URLSearchParams,
document: documentRef,
navigator: navigatorRef,
location: locationRef,
history: {
length: 2,
state: null,
pushState() {},
replaceState() {},
},
screen: {
width: screenWidth,
height: screenHeight,
availWidth: screenWidth,
availHeight: Math.max(screenHeight - 40, screenHeight),
availLeft: 0,
availTop: 0,
colorDepth: 24,
pixelDepth: 24,
},
performance: {
now() {
return performance.now();
},
timeOrigin: performance.timeOrigin,
memory: {
jsHeapSizeLimit: Number(options.js_heap_size_limit || DEFAULT_JS_HEAP_SIZE_LIMIT),
},
},
localStorage: makeStorage(),
sessionStorage: makeStorage(),
setTimeout,
clearTimeout,
};
windowRef.window = windowRef;
windowRef.self = windowRef;
windowRef.top = windowRef;
windowRef.globalThis = windowRef;
return { windowRef, documentRef };
}
function randomNavigatorProbe(navigatorRef) {
const key = randomPick(Object.keys(Object.getPrototypeOf(navigatorRef)));
try {
return `${key}${navigatorRef[key].toString()}`;
} catch {
return `${key}`;
}
}
function getConfig(runtime, sid) {
const { windowRef, documentRef } = runtime;
const { navigator } = windowRef;
return [
windowRef.screen?.width + windowRef.screen?.height,
`${new Date()}`,
windowRef.performance?.memory?.jsHeapSizeLimit,
Math.random(),
navigator.userAgent,
randomPick(Array.from(documentRef.scripts || []).map(script => script?.src).filter(Boolean)),
(Array.from(documentRef.scripts || [])
.map(script => script?.src?.match('c/[^/]*/_'))
.filter(match => match?.length)[0] ?? [])[0] ?? documentRef.documentElement.getAttribute('data-build'),
navigator.language,
navigator.languages?.join(','),
Math.random(),
randomNavigatorProbe(navigator),
randomPick(Object.keys(documentRef)),
randomPick(Object.keys(windowRef)),
windowRef.performance.now(),
sid,
[...new URLSearchParams(windowRef.location.search).keys()].join(','),
navigator.hardwareConcurrency,
windowRef.performance.timeOrigin,
Number('ai' in windowRef),
Number('createPRNG' in windowRef),
Number('cache' in windowRef),
Number('data' in windowRef),
Number('solana' in windowRef),
Number('dump' in windowRef),
Number('InstallTrigger' in windowRef),
];
}
function runCheck(startTime, seed, difficulty, config, attempt) {
config[3] = attempt;
config[9] = Math.round(performance.now() - startTime);
const serialized = encodeUtf8Json(config);
const digest = hashHex(seed + serialized);
return digest.substring(0, difficulty.length) <= difficulty ? `${serialized}~S` : null;
}
function generateAnswerSync(seed, difficulty, runtime, sid) {
const startTime = performance.now();
try {
const config = getConfig(runtime, sid);
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
const answer = runCheck(startTime, seed, difficulty, config, attempt);
if (answer) {
return answer;
}
}
} catch (error) {
return buildFailureMessage(error);
}
return buildFailureMessage('proof generation exhausted');
}
function buildRequirementsToken(options = {}) {
const runtime = createRuntimeEnvironment({
...options,
sdk_url: options.sdk_version_url || options.sdk_url,
location_search: options.location_search_prepare ?? options.location_search,
});
const requirementsSeed = `${Math.random()}`;
const sid = crypto.randomUUID();
const answer = generateAnswerSync(requirementsSeed, '0', runtime, sid);
if (!answer || answer.startsWith(ERROR_PREFIX)) {
throw new Error(`sentinel prepare failed: ${answer || 'empty answer'}`);
}
return answer.startsWith('gAAAAAC') ? answer : `gAAAAAC${answer}`;
}
function buildEnforcementToken(chatReq, options = {}) {
const proof = chatReq?.proofofwork;
if (!proof?.required) {
return null;
}
const seed = proof.seed;
const difficulty = proof.difficulty;
if (typeof seed !== 'string' || typeof difficulty !== 'string') {
throw new Error(`invalid enforcement proof payload: ${JSON.stringify(proof)}`);
}
const runtime = createRuntimeEnvironment({
...options,
sdk_url: options.sdk_bootstrap_url || options.sdk_url || SENTINEL_BOOTSTRAP_URL,
location_search: options.location_search_enforcement ?? options.location_search ?? '',
});
const sid = crypto.randomUUID();
const answer = generateAnswerSync(seed, difficulty, runtime, sid);
if (!answer || answer.startsWith(ERROR_PREFIX)) {
throw new Error(`sentinel enforcement failed: ${answer || 'empty answer'}`);
}
return answer.startsWith('gAAAAAB') ? answer : `gAAAAAB${answer}`;
}
async function solveTurnstile(options = {}) {
if (!options.p) {
throw new Error('missing proof token p');
}
if (!options.dx) {
throw new Error('missing turnstile dx payload');
}
const runtime = createRuntimeEnvironment(options);
const result = await runFromInputs(options.p, options.dx, {
windowRef: runtime.windowRef,
documentRef: runtime.documentRef,
timeoutMs: Number(options.timeout_ms || DEFAULT_TIMEOUT_MS),
});
if (!result || result.channel !== 'resolve' || !result.encodedValue) {
throw new Error(`turnstile vm failed: ${JSON.stringify(result)}`);
}
return result.encodedValue;
}
async function solveSessionObserver(options = {}) {
if (!options.snapshot_dx) {
throw new Error('missing session observer snapshot_dx payload');
}
const runtime = createRuntimeEnvironment({
...options,
sdk_url: options.sdk_bootstrap_url || options.sdk_url || SENTINEL_BOOTSTRAP_URL,
location_search: options.location_search_enforcement ?? options.location_search ?? '',
});
const result = await runFromInputs('', options.snapshot_dx, {
windowRef: runtime.windowRef,
documentRef: runtime.documentRef,
timeoutMs: Number(options.timeout_ms || DEFAULT_TIMEOUT_MS),
});
if (!result || result.channel !== 'resolve' || !result.encodedValue) {
throw new Error(`session observer vm failed: ${JSON.stringify(result)}`);
}
return result.encodedValue;
}
async function readStdinJson() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
if (chunks.length === 0) {
return {};
}
const text = Buffer.concat(chunks).toString('utf8').trim();
return text ? JSON.parse(text) : {};
}
async function main() {
const mode = process.argv[2];
const input = await readStdinJson();
if (mode === 'prepare') {
const p = buildRequirementsToken(input);
process.stdout.write(JSON.stringify({ p }));
return;
}
if (mode === 'enforcement') {
const p = buildEnforcementToken(input.chat_req, input);
process.stdout.write(JSON.stringify({ p }));
return;
}
if (mode === 'turnstile') {
const t = await solveTurnstile(input);
process.stdout.write(JSON.stringify({ t }));
return;
}
if (mode === 'session-observer') {
const so = await solveSessionObserver(input);
process.stdout.write(JSON.stringify({ so }));
return;
}
throw new Error(`unknown mode: ${mode || '<missing>'}`);
}
main().catch(error => {
process.stderr.write(`${error.stack || String(error)}\n`);
process.exit(1);
});

251
src/sentinel_solver.py Normal file
View File

@@ -0,0 +1,251 @@
from __future__ import annotations
import importlib.resources
import json
import re
import subprocess
from pathlib import Path
from typing import Any
from urllib.parse import quote
SENTINEL_ORIGIN = "https://sentinel.openai.com"
SENTINEL_BOOTSTRAP_URL = f"{SENTINEL_ORIGIN}/backend-api/sentinel/sdk.js"
SDK_VERSION_RE = re.compile(r"script\.src\s*=\s*'https://sentinel\.openai\.com/sentinel/([^']+)/sdk\.js'")
class SentinelSolveError(RuntimeError):
pass
class SentinelSolver:
def __init__(
self,
http,
*,
node_binary: str = "node",
runner_path: str | Path | None = None,
timeout_seconds: float = 30.0,
) -> None:
self.http = http
self.node_binary = node_binary
self.runner_path = Path(runner_path) if runner_path else self._default_runner_path()
self.timeout_seconds = timeout_seconds
self._flow_cache: dict[str, dict[str, Any]] = {}
def build_token(self, flow: str) -> str:
cached = self._prepare_flow(flow)
chat_req = cached["chat_req"]
env = cached["env"]
proof = cached["prepare_proof"]
turnstile = chat_req.get("turnstile") if isinstance(chat_req, dict) else None
dx = turnstile.get("dx") if isinstance(turnstile, dict) else None
encoded_turnstile = None
if dx:
encoded_turnstile = self._run_node("turnstile", {**env, "p": proof, "dx": dx})["t"]
if not isinstance(encoded_turnstile, str) or not encoded_turnstile:
raise SentinelSolveError(
f"sentinel turnstile runner returned invalid result: {encoded_turnstile!r}"
)
enforcement_proof = self._run_node("enforcement", {**env, "chat_req": chat_req})["p"]
if not isinstance(enforcement_proof, str) or not enforcement_proof:
raise SentinelSolveError(
f"sentinel enforcement runner returned invalid proof: {enforcement_proof!r}"
)
token: dict[str, Any] = {
"p": enforcement_proof,
"t": encoded_turnstile,
"c": chat_req.get("token") if isinstance(chat_req, dict) else None,
"flow": flow,
}
did = self._cookie_value("oai-did") or getattr(self.http, "device_id", "")
if did:
token["id"] = did
return json.dumps(token, separators=(",", ":"))
def build_session_observer_token(self, flow: str) -> str:
cached = self._prepare_flow(flow)
chat_req = cached["chat_req"]
so_payload = chat_req.get("so") if isinstance(chat_req, dict) else None
snapshot_dx = so_payload.get("snapshot_dx") if isinstance(so_payload, dict) else None
if not snapshot_dx:
raise SentinelSolveError(f"sentinel session observer snapshot missing for flow {flow!r}")
so_value = self._run_node(
"session-observer",
{
**cached["env"],
"flow": flow,
"c": chat_req.get("token") if isinstance(chat_req, dict) else None,
"snapshot_dx": snapshot_dx,
"chat_req": chat_req,
},
)["so"]
if not isinstance(so_value, str) or not so_value:
raise SentinelSolveError(
f"sentinel session observer runner returned invalid payload: {so_value!r}"
)
token: dict[str, Any] = {
"so": so_value,
"c": chat_req.get("token") if isinstance(chat_req, dict) else None,
"flow": flow,
}
did = self._cookie_value("oai-did") or getattr(self.http, "device_id", "")
if did:
token["id"] = did
return json.dumps(token, separators=(",", ":"))
def _fetch_sdk_version(self) -> str:
response = self.http.request("GET", SENTINEL_BOOTSTRAP_URL)
text = getattr(response, "text", "") or ""
match = SDK_VERSION_RE.search(text)
if not match:
raise SentinelSolveError("failed to parse active sentinel SDK version")
return match.group(1)
def _fetch_req_payload(self, flow: str, proof: str, sdk_version: str) -> dict[str, Any]:
payload: dict[str, Any] = {"p": proof, "flow": flow}
did = self._cookie_value("oai-did") or getattr(self.http, "device_id", "")
if did:
payload["id"] = did
frame_url = f"{SENTINEL_ORIGIN}/backend-api/sentinel/frame.html?sv={quote(sdk_version)}"
self._warm_frame(frame_url)
response = self.http.request(
"POST",
f"{SENTINEL_ORIGIN}/backend-api/sentinel/req",
data=json.dumps(payload),
headers={
"Content-Type": "text/plain;charset=UTF-8",
"Accept": "*/*",
"Origin": SENTINEL_ORIGIN,
"Referer": frame_url,
},
)
if getattr(response, "status_code", 0) != 200:
raise SentinelSolveError(
f"sentinel req failed: {getattr(response, 'status_code', 'unknown')} {self._response_excerpt(response)}"
)
try:
data = response.json()
except Exception as error: # pragma: no cover - defensive
raise SentinelSolveError(f"sentinel req returned non-JSON response: {error}") from error
if not isinstance(data, dict):
raise SentinelSolveError(f"sentinel req returned invalid payload: {data!r}")
return data
def _prepare_flow(self, flow: str) -> dict[str, Any]:
cached = self._flow_cache.get(flow)
if cached:
return cached
version = self._fetch_sdk_version()
env = self._node_environment(version)
proof = self._run_node("prepare", env)["p"]
if not isinstance(proof, str) or not proof:
raise SentinelSolveError(f"sentinel prepare returned invalid proof: {proof!r}")
chat_req = self._fetch_req_payload(flow, proof, version)
cached = {
"sdk_version": version,
"env": env,
"prepare_proof": proof,
"chat_req": chat_req,
}
self._flow_cache[flow] = cached
return cached
def _node_environment(self, sdk_version: str) -> dict[str, Any]:
sdk_url = f"{SENTINEL_ORIGIN}/sentinel/{sdk_version}/sdk.js"
accept_language = self._session_header("Accept-Language")
return {
"sdk_version": sdk_version,
"sdk_url": sdk_url,
"sdk_bootstrap_url": SENTINEL_BOOTSTRAP_URL,
"sdk_version_url": sdk_url,
"location_search_prepare": f"?sv={sdk_version}",
"location_search_enforcement": "",
"user_agent": self._session_header("User-Agent"),
"accept_language": accept_language,
"hardware_concurrency": 8,
"screen_width": 1920,
"screen_height": 1080,
"js_heap_size_limit": 4294967296,
}
def _run_node(self, mode: str, payload: dict[str, Any]) -> dict[str, Any]:
command = [self.node_binary, str(self.runner_path), mode]
try:
completed = subprocess.run(
command,
input=json.dumps(payload),
text=True,
capture_output=True,
timeout=self.timeout_seconds,
check=False,
)
except FileNotFoundError as error:
raise SentinelSolveError(f"Node.js executable not found: {self.node_binary}") from error
except subprocess.TimeoutExpired as error:
raise SentinelSolveError(f"sentinel {mode} timed out") from error
if completed.returncode != 0:
stderr = (completed.stderr or "").strip()
stdout = (completed.stdout or "").strip()
detail = stderr or stdout or f"exit code {completed.returncode}"
raise SentinelSolveError(f"sentinel {mode} failed: {detail}")
stdout = (completed.stdout or "").strip()
if not stdout:
raise SentinelSolveError(f"sentinel {mode} returned empty stdout")
try:
data = json.loads(stdout)
except json.JSONDecodeError as error:
raise SentinelSolveError(f"sentinel {mode} returned invalid JSON: {stdout!r}") from error
if not isinstance(data, dict):
raise SentinelSolveError(f"sentinel {mode} returned invalid payload: {data!r}")
return data
def _cookie_value(self, name: str) -> str:
session = getattr(self.http, "session", None)
cookies = getattr(session, "cookies", None)
jar = getattr(cookies, "jar", None)
if not jar:
return ""
for cookie in jar:
if getattr(cookie, "name", "") == name:
return getattr(cookie, "value", "")
return ""
def _session_header(self, name: str) -> str:
session = getattr(self.http, "session", None)
headers = getattr(session, "headers", {}) or {}
return headers.get(name, "")
def _warm_frame(self, frame_url: str) -> None:
response = self.http.request(
"GET",
frame_url,
headers={
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Referer": SENTINEL_BOOTSTRAP_URL,
},
)
if getattr(response, "status_code", 0) not in {200, 204}:
raise SentinelSolveError(
f"sentinel frame warmup failed: {getattr(response, 'status_code', 'unknown')} {self._response_excerpt(response)}"
)
@staticmethod
def _response_excerpt(response, limit: int = 300) -> str:
text = getattr(response, "text", "") or ""
return text.replace("\n", " ").replace("\r", " ")[:limit]
@staticmethod
def _default_runner_path() -> Path:
try:
return Path(importlib.resources.files(__package__).joinpath("sentinel_runner.js"))
except Exception:
return Path(__file__).with_name("sentinel_runner.js")

600
src/sentinel_vm.js Normal file
View File

@@ -0,0 +1,600 @@
'use strict';
const carrierSecrets = new WeakMap();
const OPCODES = Object.freeze({
RUN_ENCODED_PROGRAM: 0,
XOR_IN_PLACE: 1,
SET_LITERAL: 2,
RESOLVE: 3,
REJECT: 4,
APPEND_OR_ADD: 5,
READ_PROPERTY: 6,
CALL_WITH_REGISTER_ARGS: 7,
COPY_REGISTER: 8,
INSTRUCTION_QUEUE: 9,
WINDOW_OBJECT: 10,
FIND_SCRIPT_SRC_BY_REGEX: 11,
STORE_REGISTER_MAP: 12,
CALL_WITH_RAW_ARGS: 13,
JSON_PARSE: 14,
JSON_STRINGIFY: 15,
SECRET_KEY: 16,
CALL_AND_STORE_RESULT: 17,
BASE64_DECODE_IN_PLACE: 18,
BASE64_ENCODE_IN_PLACE: 19,
CALL_IF_EQUAL: 20,
CALL_IF_DELTA_EXCEEDS: 21,
RUN_BLOCK: 22,
CALL_IF_DEFINED: 23,
BIND_METHOD: 24,
NOOP_25: 25,
NOOP_26: 26,
REMOVE_OR_SUBTRACT: 27,
NOOP_28: 28,
LESS_THAN: 29,
DEFINE_VM_FUNCTION: 30,
MULTIPLY: 33,
AWAIT_VALUE: 34,
DIVIDE: 35,
});
function bindSecretToCarrier(carrier, secret) {
if (carrier && (typeof carrier === 'object' || typeof carrier === 'function')) {
carrierSecrets.set(carrier, String(secret ?? ''));
}
return carrier;
}
function createSecretCarrier(secret) {
return bindSecretToCarrier({}, secret);
}
function resolveSecretInput(secretInput) {
if (secretInput && (typeof secretInput === 'object' || typeof secretInput === 'function')) {
return carrierSecrets.get(secretInput) ?? '';
}
return String(secretInput ?? '');
}
function xorCipher(text, secret) {
const source = String(text ?? '');
const key = String(secret ?? '');
if (key.length === 0) {
return source;
}
let output = '';
for (let index = 0; index < source.length; index += 1) {
output += String.fromCharCode(
source.charCodeAt(index) ^ key.charCodeAt(index % key.length),
);
}
return output;
}
function defaultAtob(value) {
if (typeof atob === 'function') {
return atob(value);
}
return Buffer.from(String(value), 'base64').toString('binary');
}
function defaultBtoa(value) {
if (typeof btoa === 'function') {
return btoa(value);
}
return Buffer.from(String(value), 'binary').toString('base64');
}
function isPromiseLike(value) {
return Boolean(value) && typeof value.then === 'function';
}
function toErrorText(error) {
return String(error);
}
function isBase64Like(value, atobImpl, btoaImpl) {
if (typeof value !== 'string' || value.length === 0 || value.length % 4 !== 0) {
return false;
}
if (!/^[A-Za-z0-9+/]+=*$/.test(value)) {
return false;
}
try {
return btoaImpl(atobImpl(value)) === value;
} catch {
return false;
}
}
class ReadableSssdkInterpreter {
constructor(options = {}) {
this.windowRef = options.windowRef ?? globalThis.window ?? globalThis;
this.documentRef = options.documentRef ?? globalThis.document ?? { scripts: [] };
this.atob = options.atobImpl ?? defaultAtob;
this.btoa = options.btoaImpl ?? defaultBtoa;
this.defaultTimeoutMs = options.timeoutMs ?? 500;
this.hooks = options.hooks ?? {};
this.registers = new Map();
this.stepCount = 0;
this.executionTail = Promise.resolve();
}
static bindSecretToCarrier(carrier, secret) {
return bindSecretToCarrier(carrier, secret);
}
static createSecretCarrier(secret) {
return createSecretCarrier(secret);
}
resetMachine(secret) {
this.registers.clear();
this.stepCount = 0;
this.registers.set(OPCODES.RUN_ENCODED_PROGRAM, encodedProgram => {
return this._runEncodedProgramLikeOriginalDirect(
encodedProgram,
{ secret: String(this.getRegister(OPCODES.SECRET_KEY) ?? '') },
false,
);
});
this.registers.set(OPCODES.XOR_IN_PLACE, (targetRegister, keyRegister) => {
const currentValue = String(this.getRegister(targetRegister) ?? '');
const keyValue = String(this.getRegister(keyRegister) ?? '');
this.setRegister(targetRegister, xorCipher(currentValue, keyValue));
});
this.registers.set(OPCODES.SET_LITERAL, (targetRegister, literalValue) => {
this.setRegister(targetRegister, literalValue);
});
this.registers.set(OPCODES.APPEND_OR_ADD, (targetRegister, sourceRegister) => {
const currentValue = this.getRegister(targetRegister);
const sourceValue = this.getRegister(sourceRegister);
if (Array.isArray(currentValue)) {
currentValue.push(sourceValue);
return;
}
this.setRegister(targetRegister, currentValue + sourceValue);
});
this.registers.set(OPCODES.REMOVE_OR_SUBTRACT, (targetRegister, sourceRegister) => {
const currentValue = this.getRegister(targetRegister);
const sourceValue = this.getRegister(sourceRegister);
if (Array.isArray(currentValue)) {
const position = currentValue.indexOf(sourceValue);
if (position >= 0) {
currentValue.splice(position, 1);
}
return;
}
this.setRegister(targetRegister, currentValue - sourceValue);
});
this.registers.set(OPCODES.LESS_THAN, (targetRegister, leftRegister, rightRegister) => {
this.setRegister(
targetRegister,
this.getRegister(leftRegister) < this.getRegister(rightRegister),
);
});
this.registers.set(OPCODES.MULTIPLY, (targetRegister, leftRegister, rightRegister) => {
const leftValue = Number(this.getRegister(leftRegister));
const rightValue = Number(this.getRegister(rightRegister));
this.setRegister(targetRegister, leftValue * rightValue);
});
this.registers.set(OPCODES.DIVIDE, (targetRegister, leftRegister, rightRegister) => {
const leftValue = Number(this.getRegister(leftRegister));
const rightValue = Number(this.getRegister(rightRegister));
this.setRegister(targetRegister, rightValue === 0 ? 0 : leftValue / rightValue);
});
this.registers.set(OPCODES.READ_PROPERTY, (targetRegister, objectRegister, keyRegister) => {
const objectValue = this.getRegister(objectRegister);
const propertyKey = this.getRegister(keyRegister);
this.setRegister(targetRegister, objectValue[propertyKey]);
});
this.registers.set(OPCODES.CALL_WITH_REGISTER_ARGS, (functionRegister, ...argumentRegisters) => {
const callable = this.getRegister(functionRegister);
const resolvedArgs = argumentRegisters.map(registerId => this.getRegister(registerId));
return callable(...resolvedArgs);
});
this.registers.set(
OPCODES.CALL_AND_STORE_RESULT,
(targetRegister, functionRegister, ...argumentRegisters) => {
try {
const callable = this.getRegister(functionRegister);
const resolvedArgs = argumentRegisters.map(registerId => this.getRegister(registerId));
const returnValue = callable(...resolvedArgs);
if (isPromiseLike(returnValue)) {
return returnValue
.then(value => {
this.setRegister(targetRegister, value);
})
.catch(error => {
this.setRegister(targetRegister, toErrorText(error));
});
}
this.setRegister(targetRegister, returnValue);
} catch (error) {
this.setRegister(targetRegister, toErrorText(error));
}
},
);
this.registers.set(OPCODES.CALL_WITH_RAW_ARGS, (targetRegister, functionRegister, ...rawArgs) => {
try {
const callable = this.getRegister(functionRegister);
callable(...rawArgs);
} catch (error) {
this.setRegister(targetRegister, toErrorText(error));
}
});
this.registers.set(OPCODES.COPY_REGISTER, (targetRegister, sourceRegister) => {
this.setRegister(targetRegister, this.getRegister(sourceRegister));
});
this.registers.set(OPCODES.WINDOW_OBJECT, this.windowRef);
this.registers.set(OPCODES.FIND_SCRIPT_SRC_BY_REGEX, (targetRegister, regexRegister) => {
const regex = this.getRegister(regexRegister);
const scripts = Array.from(this.documentRef.scripts || []);
const firstMatch = scripts
.map(script => script?.src?.match?.(regex))
.filter(Boolean)[0];
this.setRegister(targetRegister, (firstMatch ?? [])[0] ?? null);
});
this.registers.set(OPCODES.STORE_REGISTER_MAP, targetRegister => {
this.setRegister(targetRegister, this.registers);
});
this.registers.set(OPCODES.JSON_PARSE, (targetRegister, sourceRegister) => {
this.setRegister(targetRegister, JSON.parse(String(this.getRegister(sourceRegister))));
});
this.registers.set(OPCODES.JSON_STRINGIFY, (targetRegister, sourceRegister) => {
this.setRegister(targetRegister, JSON.stringify(this.getRegister(sourceRegister)));
});
this.registers.set(OPCODES.BASE64_DECODE_IN_PLACE, targetRegister => {
this.setRegister(targetRegister, this.atob(String(this.getRegister(targetRegister))));
});
this.registers.set(OPCODES.BASE64_ENCODE_IN_PLACE, targetRegister => {
this.setRegister(targetRegister, this.btoa(String(this.getRegister(targetRegister))));
});
this.registers.set(
OPCODES.CALL_IF_EQUAL,
(leftRegister, rightRegister, functionRegister, ...rawArgs) => {
if (this.getRegister(leftRegister) === this.getRegister(rightRegister)) {
return this.getRegister(functionRegister)(...rawArgs);
}
return null;
},
);
this.registers.set(
OPCODES.CALL_IF_DELTA_EXCEEDS,
(leftRegister, rightRegister, thresholdRegister, functionRegister, ...rawArgs) => {
const delta = Math.abs(this.getRegister(leftRegister) - this.getRegister(rightRegister));
if (delta > this.getRegister(thresholdRegister)) {
return this.getRegister(functionRegister)(...rawArgs);
}
return null;
},
);
this.registers.set(OPCODES.CALL_IF_DEFINED, (guardRegister, functionRegister, ...rawArgs) => {
if (this.getRegister(guardRegister) !== undefined) {
return this.getRegister(functionRegister)(...rawArgs);
}
return null;
});
this.registers.set(OPCODES.BIND_METHOD, (targetRegister, objectRegister, keyRegister) => {
const objectValue = this.getRegister(objectRegister);
const propertyKey = this.getRegister(keyRegister);
this.setRegister(targetRegister, objectValue[propertyKey].bind(objectValue));
});
this.registers.set(OPCODES.AWAIT_VALUE, (targetRegister, sourceRegister) => {
try {
return Promise.resolve(this.getRegister(sourceRegister)).then(value => {
this.setRegister(targetRegister, value);
});
} catch {
return undefined;
}
});
this.registers.set(OPCODES.RUN_BLOCK, (targetRegister, nestedInstructions) => {
const previousQueue = this.getQueueSnapshot();
this.setQueue(this.cloneInstructionQueue(nestedInstructions));
return this.executeCurrentQueue()
.catch(error => {
this.setRegister(targetRegister, toErrorText(error));
})
.finally(() => {
this.setQueue(previousQueue);
});
});
this.registers.set(OPCODES.NOOP_25, () => {});
this.registers.set(OPCODES.NOOP_26, () => {});
this.registers.set(OPCODES.NOOP_28, () => {});
this.setRegister(OPCODES.SECRET_KEY, String(secret ?? ''));
}
getRegister(registerId) {
return this.registers.get(registerId);
}
setRegister(registerId, value) {
if (typeof this.hooks.onSetRegister === 'function') {
this.hooks.onSetRegister({
registerId,
value,
stepCount: this.stepCount,
});
}
this.registers.set(registerId, value);
}
getQueueSnapshot() {
return [...(this.getRegister(OPCODES.INSTRUCTION_QUEUE) ?? [])];
}
setQueue(queue) {
this.setRegister(OPCODES.INSTRUCTION_QUEUE, queue);
}
cloneInstructionQueue(queue) {
return Array.isArray(queue) ? [...queue] : [];
}
resolveSecretInput(secretInput) {
return resolveSecretInput(secretInput);
}
decodeProgram(encodedProgram, secret) {
const binary = this.atob(String(encodedProgram));
const jsonText = xorCipher(binary, secret);
return JSON.parse(jsonText);
}
queueExclusive(task) {
const next = this.executionTail.then(task, task);
this.executionTail = next.then(
() => undefined,
() => undefined,
);
return next;
}
withRunScope(scopeState, executor) {
const previousResolve = this.getRegister(OPCODES.RESOLVE);
const previousReject = this.getRegister(OPCODES.REJECT);
const previousDefineFunction = this.getRegister(OPCODES.DEFINE_VM_FUNCTION);
this.setRegister(OPCODES.RESOLVE, rawValue => {
scopeState.resolveWire(rawValue);
});
this.setRegister(OPCODES.REJECT, rawValue => {
scopeState.rejectWire(rawValue);
});
this.setRegister(
OPCODES.DEFINE_VM_FUNCTION,
(targetRegister, returnRegister, parameterRegistersOrProgram, maybeProgram) => {
const hasParameterRegisters = Array.isArray(maybeProgram);
const parameterRegisters = hasParameterRegisters ? parameterRegistersOrProgram : [];
const functionProgram = (hasParameterRegisters ? maybeProgram : parameterRegistersOrProgram) || [];
this.setRegister(targetRegister, (...runtimeArgs) => {
if (scopeState.isSettled()) {
return undefined;
}
const previousQueue = this.getQueueSnapshot();
if (hasParameterRegisters) {
for (let index = 0; index < parameterRegisters.length; index += 1) {
this.setRegister(parameterRegisters[index], runtimeArgs[index]);
}
}
this.setQueue(this.cloneInstructionQueue(functionProgram));
return this.executeCurrentQueue()
.then(() => this.getRegister(returnRegister))
.catch(error => toErrorText(error))
.finally(() => {
this.setQueue(previousQueue);
});
});
},
);
return Promise.resolve()
.then(executor)
.finally(() => {
this.setRegister(OPCODES.RESOLVE, previousResolve);
this.setRegister(OPCODES.REJECT, previousReject);
this.setRegister(OPCODES.DEFINE_VM_FUNCTION, previousDefineFunction);
});
}
async executeCurrentQueue() {
while (this.getQueueSnapshot().length > 0) {
const instruction = this.getRegister(OPCODES.INSTRUCTION_QUEUE).shift();
const [opcode, ...args] = instruction;
const handler = this.getRegister(opcode);
if (typeof handler !== 'function') {
throw new Error(`Unknown opcode: ${opcode}`);
}
const maybePromise = handler(...args);
if (isPromiseLike(maybePromise)) {
await maybePromise;
}
this.stepCount += 1;
}
}
runEncodedProgramLikeOriginal(encodedProgram, options = {}) {
return this.queueExclusive(() => {
return this._runEncodedProgramLikeOriginalDirect(encodedProgram, options, true);
});
}
_runEncodedProgramLikeOriginalDirect(encodedProgram, options = {}, resetMachine = true) {
const secret = String(options.secret ?? '');
const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
if (resetMachine) {
this.resetMachine(secret);
}
try {
return this.executeQueueLikeOriginal(
this.decodeProgram(encodedProgram, secret),
timeoutMs,
);
} catch (error) {
return Promise.resolve(this.btoa(`${this.stepCount}: ${toErrorText(error)}`));
}
}
executeQueueLikeOriginal(instructions, timeoutMs) {
const previousQueue = this.getQueueSnapshot();
return new Promise((resolve, reject) => {
let settled = false;
const finishResolve = wireValue => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve(wireValue);
};
const finishReject = wireValue => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
reject(wireValue);
};
const timer = setTimeout(() => {
finishResolve(String(this.stepCount));
}, timeoutMs);
const scopeState = {
isSettled: () => settled,
resolveWire: rawValue => {
finishResolve(this.btoa(String(rawValue)));
},
rejectWire: rawValue => {
finishReject(this.btoa(String(rawValue)));
},
};
this.withRunScope(scopeState, async () => {
this.setQueue(this.cloneInstructionQueue(instructions));
try {
await this.executeCurrentQueue();
} catch (error) {
finishResolve(this.btoa(`${this.stepCount}: ${toErrorText(error)}`));
} finally {
this.setQueue(previousQueue);
}
}).catch(error => {
finishResolve(this.btoa(`${this.stepCount}: ${toErrorText(error)}`));
this.setQueue(previousQueue);
});
});
}
async runFromInputs(secretInput, encodedPayload, options = {}) {
const secret = this.resolveSecretInput(secretInput);
return this.normalizeLikeOriginalResult(
() => this.runEncodedProgramLikeOriginal(encodedPayload, {
...options,
secret,
}),
);
}
normalizeLikeOriginalResult(runLikeOriginal) {
return Promise.resolve()
.then(async () => {
try {
const wireValue = await runLikeOriginal();
return this.normalizeWireValue(wireValue, false);
} catch (wireValue) {
return this.normalizeWireValue(wireValue, true);
}
});
}
normalizeWireValue(wireValue, rejected) {
if (!isBase64Like(wireValue, this.atob, this.btoa)) {
return {
channel: 'timeout',
encodedValue: null,
value: String(wireValue),
stepCount: Number(wireValue),
};
}
return {
channel: rejected ? 'reject' : 'resolve',
encodedValue: wireValue,
value: this.atob(wireValue),
stepCount: this.stepCount,
};
}
}
async function runFromInputs(secretInput, encodedPayload, options = {}) {
const interpreter = new ReadableSssdkInterpreter(options);
return interpreter.runFromInputs(secretInput, encodedPayload, options);
}
module.exports = {
OPCODES,
ReadableSssdkInterpreter,
bindSecretToCarrier,
createSecretCarrier,
resolveSecretInput,
runFromInputs,
xorCipher,
};

View File

@@ -265,11 +265,16 @@ class YYDSMailClient(BaseMailClient):
def create_mailbox(self) -> dict:
"""Create a mailbox. Returns dict with id, address, token."""
payload = {}
preferred_domain = settings.yyds_mail_domain.strip()
if preferred_domain:
payload["domain"] = preferred_domain
for attempt in range(10):
r = httpx.post(
f"{self.base}/v1/accounts",
headers=self.headers,
json={},
json=payload,
timeout=30,
)
if r.status_code == 201 or r.status_code == 200: