fix(register): align live sentinel flow with successful HAR
This commit is contained in:
@@ -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**
|
||||||
@@ -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/`
|
||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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("[6/9] Retrying register with captured sentinel token...")
|
)
|
||||||
response, data = self._attempt_register(register_url, auth_page_url, email, password, sentinel=captured)
|
|
||||||
|
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"):
|
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,22 +439,17 @@ 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,
|
||||||
|
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"):
|
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)}")
|
raise RuntimeError(f"Create account blocked: {self._response_excerpt(create_response)}")
|
||||||
|
|||||||
@@ -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
429
src/sentinel_runner.js
Normal 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
251
src/sentinel_solver.py
Normal 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
600
src/sentinel_vm.js
Normal 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,
|
||||||
|
};
|
||||||
@@ -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
0
tests/__init__.py
Normal file
129
tests/test_register_live_sentinel.py
Normal file
129
tests/test_register_live_sentinel.py
Normal 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()
|
||||||
146
tests/test_sentinel_solver.py
Normal file
146
tests/test_sentinel_solver.py
Normal 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()
|
||||||
40
tests/test_yyds_mail_client.py
Normal file
40
tests/test_yyds_mail_client.py
Normal 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()
|
||||||
Reference in New Issue
Block a user