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

@@ -0,0 +1,55 @@
# Register Sentinel Live Fix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace static register sentinel tokens with live `/backend-api/sentinel/req` + `turnstile.dx` generation.
**Architecture:** Python keeps the HTTP registration flow. A local Node runner handles the current sentinel proof generation and VM execution. Python assembles the final sentinel header and injects it into register/create-account requests.
**Tech Stack:** Python 3.11+, `curl_cffi`, built-in `unittest`, Node.js CommonJS.
---
### Task 1: Add failing tests for the new sentinel integration
**Files:**
- Create: `tests/test_sentinel_solver.py`
- Create: `tests/test_register_live_sentinel.py`
- [ ] **Step 1: Write failing tests for Python sentinel solving and register flow usage**
- [ ] **Step 2: Run `python -m unittest tests.test_sentinel_solver tests.test_register_live_sentinel -v` and verify failure**
### Task 2: Add the Node VM / proof runner
**Files:**
- Create: `src/sentinel_vm.js`
- Create: `src/sentinel_runner.js`
- [ ] **Step 1: Add the VM executor and runner CLI**
- [ ] **Step 2: Run a targeted local smoke command against the runner**
### Task 3: Add the Python sentinel solver
**Files:**
- Create: `src/sentinel_solver.py`
- Modify: `pyproject.toml`
- [ ] **Step 1: Implement SDK version discovery, Node subprocess calls, req fetch, and final token assembly**
- [ ] **Step 2: Run sentinel solver unit tests and verify pass**
### Task 4: Wire register flow to the live solver
**Files:**
- Modify: `src/chatgpt_register_http_reverse.py`
- [ ] **Step 1: Inject the sentinel solver into register flow and remove static fallback use**
- [ ] **Step 2: Run register flow unit tests and verify pass**
### Task 5: Verify end-to-end targeted checks
**Files:**
- Modify as needed based on failures from prior tasks
- [ ] **Step 1: Run `python -m unittest tests.test_sentinel_solver tests.test_register_live_sentinel -v`**
- [ ] **Step 2: Run one targeted Node runner smoke command**
- [ ] **Step 3: Summarize limitations and next checks**

View File

@@ -0,0 +1,36 @@
# Register Sentinel Live Generation Design
## Goal
Replace the expired static `nodatadog.js` sentinel fallback in the register flow with live sentinel generation based on the current `/backend-api/sentinel/req -> turnstile.dx -> VM` flow.
## Scope
- Only the `register` flow is in scope.
- `checkout` may benefit indirectly because it reuses registration, but payment flow changes are out of scope.
- `codex-login` is explicitly out of scope.
## Design
1. Add a Python `SentinelSolver` that:
- fetches the current sentinel SDK bootstrap to discover the active sentinel version;
- asks a local Node runner to generate the current `p` proof token using the live `getConfig()` / proof-of-work logic;
- calls `https://sentinel.openai.com/backend-api/sentinel/req` with `{p,id,flow}`;
- asks the Node runner to execute `turnstile.dx` and returns the raw encoded VM output;
- builds the final sentinel header as JSON with `p`, `t`, `c`, `id`, and `flow`.
2. Add a Node runtime that contains:
- a readable VM executor for `turnstile.dx`;
- a small browser-like environment shim;
- the current proof generation logic derived from the active SDK.
3. Wire `ChatGPTRegisterHTTPReverse.register()` to generate live sentinel tokens for:
- `username_password_create`
- `oauth_create_account`
4. Remove the old static-capture fallback from the register path.
## Failure Policy
If any live sentinel step fails (SDK version fetch, Node runtime, req response parse, VM execution, or final token assembly), registration stops immediately with a descriptive error.
## Files
- New: `src/sentinel_solver.py`
- New: `src/sentinel_vm.js`
- New: `src/sentinel_runner.js`
- Modify: `src/chatgpt_register_http_reverse.py`
- Modify: `pyproject.toml`
- New tests under `tests/`

View File

@@ -25,7 +25,7 @@ package-dir = { gptplus_auto = "src" }
include-package-data = true include-package-data = true
[tool.setuptools.package-data] [tool.setuptools.package-data]
gptplus_auto = ["nodatadog.js"] gptplus_auto = ["nodatadog.js", "sentinel_runner.js", "sentinel_vm.js"]
[dependency-groups] [dependency-groups]
dev = [] dev = []

View File

@@ -1,28 +1,27 @@
from __future__ import annotations from __future__ import annotations
"""Pure HTTP registration experiment with detailed auth-state diagnostics.""" """Pure HTTP registration experiment with detailed auth-state diagnostics."""
import os
import re
import uuid import uuid
from pathlib import Path
from typing import Optional from typing import Optional
from urllib.parse import unquote, urlencode, urljoin, urlparse from urllib.parse import unquote, urlencode, urljoin, urlparse
try: try:
from .http_client import HTTPClient from .http_client import HTTPClient
from .sentinel_solver import SentinelSolver
from .vmail_client import BaseMailClient from .vmail_client import BaseMailClient
except ImportError: # pragma: no cover - allow direct script execution from source tree except ImportError: # pragma: no cover - allow direct script execution from source tree
from http_client import HTTPClient from http_client import HTTPClient
from sentinel_solver import SentinelSolver
from vmail_client import BaseMailClient from vmail_client import BaseMailClient
class ChatGPTRegisterHTTPReverse: 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.http = http
self.mail = mail self.mail = mail
self.base = "https://chatgpt.com" self.base = "https://chatgpt.com"
self.auth_base = "https://auth.openai.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]]: def _cookie_rows(self) -> list[tuple[str, str, str]]:
rows = [] rows = []
@@ -79,27 +78,14 @@ class ChatGPTRegisterHTTPReverse:
except Exception: except Exception:
return None return None
def _load_captured_sentinel(self, flow_name: str) -> str: def _build_live_sentinel_token(self, flow_name: str) -> str:
candidates = [ return self.sentinel_solver.build_token(flow_name)
self.module_dir / "nodatadog.js",
self.module_dir.parent / "nodatadog.js", def _build_live_session_observer_token(self, flow_name: str) -> str:
Path.cwd() / "nodatadog.js", build_session_observer_token = getattr(self.sentinel_solver, "build_session_observer_token", None)
] if not callable(build_session_observer_token):
content = ""
for path in candidates:
try:
content = path.read_text(encoding="utf-8")
break
except FileNotFoundError:
continue
if not content:
return "" return ""
pattern = re.compile(r'"openai-sentinel-token": "((?:[^"\\]|\\.)*flow\\"\\"%s(?:[^"\\]|\\.)*)"' % re.escape(flow_name)) return build_session_observer_token(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)
def _bootstrap_chatgpt(self): def _bootstrap_chatgpt(self):
print("[2/9] Bootstrapping chatgpt.com cookies...") print("[2/9] Bootstrapping chatgpt.com cookies...")
@@ -217,6 +203,47 @@ class ChatGPTRegisterHTTPReverse:
def _register_endpoint(self, auth_url: str) -> str: def _register_endpoint(self, auth_url: str) -> str:
return f"{self._accounts_api_base(auth_url)}/user/register" 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 = ""): def _attempt_register(self, register_url: str, auth_page_url: str, email: str, password: str, sentinel: str = ""):
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -301,7 +328,14 @@ class ChatGPTRegisterHTTPReverse:
self._print_cookie_summary("After validate") self._print_cookie_summary("After validate")
return data["continue_url"] 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...") print("[8/9] Creating account profile via pure HTTP...")
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -311,6 +345,8 @@ class ChatGPTRegisterHTTPReverse:
} }
if sentinel: if sentinel:
headers["openai-sentinel-token"] = sentinel headers["openai-sentinel-token"] = sentinel
if sentinel_so:
headers["openai-sentinel-so-token"] = sentinel_so
response = self.http.request( response = self.http.request(
"POST", "POST",
create_account_url, create_account_url,
@@ -361,29 +397,39 @@ class ChatGPTRegisterHTTPReverse:
auth_url = self._signin_auth0(email, csrf_token) auth_url = self._signin_auth0(email, csrf_token)
auth_page_url = self._follow_auth_redirects(auth_url) 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) accounts_api_base = self._accounts_api_base(auth_url)
register_url = self._register_endpoint(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) print("[5/10] Generating live sentinel token for authorize_continue...")
if response.status_code != 200 or not data or not data.get("continue_url"): authorize_sentinel = self._build_live_sentinel_token("authorize_continue")
captured = self._load_captured_sentinel("username_password_create") register_referer = self._authorize_continue(
if not captured: accounts_api_base,
raise RuntimeError( authorize_entry_page,
f"Register failed without sentinel and no captured sentinel found: {self._response_excerpt(response)}" 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,
) )
print("[6/9] Retrying register with captured sentinel token...")
response, data = self._attempt_register(register_url, auth_page_url, email, password, sentinel=captured)
if response.status_code != 200 or not data or not data.get("continue_url"): if response.status_code != 200 or not data or not data.get("continue_url"):
raise RuntimeError(f"Register blocked: {self._response_excerpt(response)}") raise RuntimeError(f"Register blocked: {self._response_excerpt(response)}")
continue_url1 = data["continue_url"] continue_url1 = data["continue_url"]
print(f" Continue URL: {continue_url1}") 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) otp = self.mail.wait_for_otp(mailbox, timeout=120)
print(f" OTP: {otp}") print(f" OTP: {otp}")
@@ -393,21 +439,16 @@ class ChatGPTRegisterHTTPReverse:
email_verification_url, 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( create_response, create_data = self._attempt_create_account(
f"{accounts_api_base}/create_account", f"{accounts_api_base}/create_account",
name, name,
about_you_url, about_you_url,
) sentinel=create_sentinel,
if create_response.status_code != 200 or not create_data or not create_data.get("continue_url"): sentinel_so=create_so_sentinel,
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"): if create_response.status_code != 200 or not create_data or not create_data.get("continue_url"):

View File

@@ -8,6 +8,7 @@ class Settings(BaseSettings):
yescaptcha_api_key: str = Field(default="", env="YESCAPTCHA_API_KEY") yescaptcha_api_key: str = Field(default="", env="YESCAPTCHA_API_KEY")
socks5_proxy: str = Field(default="", env="SOCKS5_PROXY") socks5_proxy: str = Field(default="", env="SOCKS5_PROXY")
mail_provider: str = Field(default="vmail", env="MAIL_PROVIDER") # "vmail", "mailtm", or "yyds" 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 # Payment info
card_number: str = Field(default="", env="CARD_NUMBER") 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: def create_mailbox(self) -> dict:
"""Create a mailbox. Returns dict with id, address, token.""" """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): for attempt in range(10):
r = httpx.post( r = httpx.post(
f"{self.base}/v1/accounts", f"{self.base}/v1/accounts",
headers=self.headers, headers=self.headers,
json={}, json=payload,
timeout=30, timeout=30,
) )
if r.status_code == 201 or r.status_code == 200: if r.status_code == 201 or r.status_code == 200:

0
tests/__init__.py Normal file
View File

View File

@@ -0,0 +1,129 @@
import unittest
from types import SimpleNamespace
from unittest.mock import Mock, patch
class FakeResponse:
def __init__(self, *, status_code=200, text="", json_data=None, headers=None):
self.status_code = status_code
self.text = text
self._json_data = json_data
self.headers = headers or {"content-type": "application/json"}
def json(self):
return self._json_data
class RegisterLiveSentinelTests(unittest.TestCase):
def test_register_authorize_continue_and_so_token_are_used(self):
from src.chatgpt_register_http_reverse import ChatGPTRegisterHTTPReverse
flow_history: list[str] = []
def build_token(flow: str) -> str:
flow_history.append(flow)
return f"{flow}-token"
def build_session_observer_token(flow: str) -> str:
flow_history.append("session_observer")
return "session-observer-token"
fake_http = SimpleNamespace(
request=Mock(return_value=FakeResponse(json_data={"accessToken": "at", "user": {"id": "uid"}})),
session=SimpleNamespace(cookies=SimpleNamespace(jar=[])),
)
fake_mail = SimpleNamespace(
create_mailbox=Mock(return_value={"address": "user@example.com", "id": "box-1", "password": ""}),
wait_for_otp=Mock(return_value="123456"),
)
solver = SimpleNamespace(
build_token=Mock(side_effect=build_token),
build_session_observer_token=Mock(side_effect=build_session_observer_token),
)
register = ChatGPTRegisterHTTPReverse(fake_http, fake_mail, sentinel_solver=solver)
order: list[str] = []
create_call_args: dict[str, str | None] = {}
def fake_authorize_continue(*args, **kwargs):
order.append("authorize_continue")
return "https://auth.openai.com/create-account/password"
def fake_attempt_register(*args, **kwargs):
order.append("attempt_register")
return FakeResponse(status_code=200), {"continue_url": "continue-1"}
def fake_attempt_create_account(*args, sentinel=None, sentinel_so=None, **kwargs):
order.append("attempt_create")
create_call_args["sentinel"] = sentinel
create_call_args["sentinel_so"] = sentinel_so
return FakeResponse(status_code=200), {"continue_url": "continue-3"}
with patch.object(register, "_bootstrap_chatgpt", return_value="csrf"), \
patch.object(register, "_signin_auth0", return_value="auth-url"), \
patch.object(register, "_follow_auth_redirects", return_value="https://auth.openai.com/create-account/password"), \
patch.object(register, "_accounts_api_base", return_value="https://auth.openai.com/api/accounts"), \
patch.object(register, "_register_endpoint", return_value="https://auth.openai.com/api/accounts/user/register"), \
patch.object(register, "_authorize_continue", side_effect=fake_authorize_continue, create=True) as authorize_continue, \
patch.object(register, "_open_continue_url", side_effect=["email-url", "about-you-url"]), \
patch.object(register, "_validate_otp", return_value="continue-2"), \
patch.object(register, "_finalize_auth_callback"), \
patch.object(register, "_attempt_register", side_effect=fake_attempt_register) as attempt_register, \
patch.object(register, "_attempt_create_account", side_effect=fake_attempt_create_account) as attempt_create:
result = register.register("Passw0rd!", "Jane Doe")
self.assertEqual(result["email"], "user@example.com")
self.assertEqual(
flow_history,
[
"authorize_continue",
"username_password_create",
"oauth_create_account",
"session_observer",
],
)
self.assertTrue(authorize_continue.called)
self.assertEqual(authorize_continue.call_args.args[1], "https://auth.openai.com/log-in-or-create-account?usernameKind=email")
self.assertLess(order.index("authorize_continue"), order.index("attempt_register"))
self.assertEqual(attempt_register.call_args.args[1], "https://auth.openai.com/create-account/password")
self.assertEqual(attempt_register.call_args.kwargs["sentinel"], "username_password_create-token")
self.assertEqual(create_call_args["sentinel"], "oauth_create_account-token")
self.assertEqual(create_call_args["sentinel_so"], "session-observer-token")
solver.build_session_observer_token.assert_called_once_with("oauth_create_account")
def test_authorize_continue_posts_screen_hint_and_json_headers(self):
from src.chatgpt_register_http_reverse import ChatGPTRegisterHTTPReverse
captured = {}
def fake_request(method, url, **kwargs):
captured['method'] = method
captured['url'] = url
captured['kwargs'] = kwargs
return FakeResponse(status_code=200, json_data={"continue_url": "https://auth.openai.com/create-account/password"})
fake_http = SimpleNamespace(
request=fake_request,
session=SimpleNamespace(cookies=SimpleNamespace(jar=[])),
)
fake_mail = SimpleNamespace()
register = ChatGPTRegisterHTTPReverse(fake_http, fake_mail, sentinel_solver=SimpleNamespace())
with patch.object(register, '_open_continue_url', return_value='https://auth.openai.com/create-account/password'):
result = register._authorize_continue(
'https://auth.openai.com/api/accounts',
'https://auth.openai.com/log-in-or-create-account?usernameKind=email',
'user@example.com',
sentinel='authorize-token',
)
self.assertEqual(result, 'https://auth.openai.com/create-account/password')
self.assertEqual(captured['method'], 'POST')
self.assertEqual(captured['kwargs']['json']['screen_hint'], 'login_or_signup')
self.assertEqual(captured['kwargs']['headers']['Accept'], 'application/json')
self.assertEqual(captured['kwargs']['headers']['openai-sentinel-token'], 'authorize-token')
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,146 @@
import json
import unittest
from types import SimpleNamespace
from unittest.mock import patch
class FakeResponse:
def __init__(self, *, text="", status_code=200, json_data=None, headers=None):
self.text = text
self.status_code = status_code
self._json_data = json_data
self.headers = headers or {}
def json(self):
if self._json_data is None:
raise ValueError("no json")
return self._json_data
class FakeHTTP:
def __init__(self):
self.calls = []
self.device_id = "device-fallback-id"
self.session = SimpleNamespace(
headers={
"User-Agent": "Mozilla/5.0 Test",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
},
cookies=SimpleNamespace(
jar=[SimpleNamespace(name="oai-did", value="cookie-did", domain="chatgpt.com")]
),
)
self.req_payloads = []
def request(self, method, url, **kwargs):
self.calls.append((method, url, kwargs))
if url.endswith("/backend-api/sentinel/sdk.js"):
return FakeResponse(
text=(
"window.SentinelSDK = {};"
"script.src = 'https://sentinel.openai.com/sentinel/20260219f9f6/sdk.js';"
)
)
if "/backend-api/sentinel/frame.html?sv=" in url:
return FakeResponse(text="<html></html>")
if url.endswith("/backend-api/sentinel/req"):
data = kwargs.get("data")
if data:
payload = json.loads(data)
else:
payload = {}
self.req_payloads.append(payload)
return FakeResponse(
json_data={
"token": "collector-token",
"proofofwork": {"required": True, "seed": "seed-1", "difficulty": "0"},
"turnstile": {"required": True, "dx": "encoded-dx"},
"so": {"required": True, "snapshot_dx": "snapshot-dx"},
}
)
raise AssertionError(f"unexpected request: {method} {url}")
class SentinelSolverTests(unittest.TestCase):
@patch("subprocess.run")
def test_build_token_uses_enforcement_p_not_prepare_p(self, run_mock):
from src.sentinel_solver import SentinelSolver
def fake_run(command, **kwargs):
mode = command[-1]
payload = json.loads(kwargs["input"])
if mode == "prepare":
return SimpleNamespace(returncode=0, stdout=json.dumps({"p": "prepare-proof"}), stderr="")
if mode == "turnstile":
self.assertEqual(payload["p"], "prepare-proof")
self.assertEqual(payload["dx"], "encoded-dx")
return SimpleNamespace(returncode=0, stdout=json.dumps({"t": "wire-turnstile"}), stderr="")
if mode == "enforcement":
self.assertEqual(payload["chat_req"]["proofofwork"]["seed"], "seed-1")
return SimpleNamespace(returncode=0, stdout=json.dumps({"p": "enforcement-proof"}), stderr="")
raise AssertionError(command)
run_mock.side_effect = fake_run
http = FakeHTTP()
solver = SentinelSolver(http)
token = solver.build_token("username_password_create")
parsed = json.loads(token)
self.assertEqual(
parsed,
{
"p": "enforcement-proof",
"t": "wire-turnstile",
"c": "collector-token",
"id": "cookie-did",
"flow": "username_password_create",
},
)
self.assertEqual(http.calls[0][0:2], ("GET", "https://sentinel.openai.com/backend-api/sentinel/sdk.js"))
self.assertTrue(http.calls[1][1].startswith("https://sentinel.openai.com/backend-api/sentinel/frame.html?sv="))
self.assertEqual(http.calls[2][0:2], ("POST", "https://sentinel.openai.com/backend-api/sentinel/req"))
self.assertEqual(http.req_payloads[-1]["p"], "prepare-proof")
self.assertEqual(http.req_payloads[-1]["flow"], "username_password_create")
@patch("subprocess.run")
def test_build_session_observer_token_uses_cached_chat_req_snapshot(self, run_mock):
from src.sentinel_solver import SentinelSolver
def fake_run(command, **kwargs):
mode = command[-1]
payload = json.loads(kwargs["input"])
if mode == "prepare":
return SimpleNamespace(returncode=0, stdout=json.dumps({"p": "prepare-proof"}), stderr="")
if mode == "turnstile":
return SimpleNamespace(returncode=0, stdout=json.dumps({"t": "wire-turnstile"}), stderr="")
if mode == "enforcement":
return SimpleNamespace(returncode=0, stdout=json.dumps({"p": "enforcement-proof"}), stderr="")
if mode == "session-observer":
self.assertEqual(payload["flow"], "oauth_create_account")
self.assertEqual(payload["c"], "collector-token")
self.assertEqual(payload["snapshot_dx"], "snapshot-dx")
return SimpleNamespace(returncode=0, stdout=json.dumps({"so": "wire-so"}), stderr="")
raise AssertionError(command)
run_mock.side_effect = fake_run
http = FakeHTTP()
solver = SentinelSolver(http)
so_token = solver.build_session_observer_token("oauth_create_account")
self.assertEqual(
json.loads(so_token),
{
"so": "wire-so",
"c": "collector-token",
"id": "cookie-did",
"flow": "oauth_create_account",
},
)
self.assertEqual(http.req_payloads[-1]["flow"], "oauth_create_account")
self.assertEqual(http.req_payloads[-1]["p"], "prepare-proof")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,40 @@
import unittest
from types import SimpleNamespace
from unittest.mock import patch
class YYDSMailClientDomainTests(unittest.TestCase):
@patch('src.vmail_client.httpx.post')
def test_create_mailbox_sends_configured_domain(self, post_mock):
from src.vmail_client import YYDSMailClient, settings
captured = {}
def fake_post(url, headers=None, json=None, timeout=None):
captured['url'] = url
captured['headers'] = headers
captured['json'] = json
captured['timeout'] = timeout
return SimpleNamespace(
status_code=201,
json=lambda: {
'data': {
'id': 'box-1',
'address': 'chosen@20001028.xyz',
'token': 'temp-token',
}
},
)
post_mock.side_effect = fake_post
with patch.object(settings, 'yyds_mail_domain', '20001028.xyz', create=True):
client = YYDSMailClient()
mailbox = client.create_mailbox()
self.assertEqual(mailbox['address'], 'chosen@20001028.xyz')
self.assertEqual(captured['json'], {'domain': '20001028.xyz'})
if __name__ == '__main__':
unittest.main()