mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 23:02:10 +03:00
Adds a new chatgpt-web provider that routes through chatgpt.com's internal backend-api using a Plus/Pro subscription session cookie, enabling access to GPT-5.x models without an OpenAI API key. Heavier than perplexity-web/grok-web because chatgpt.com layers more bot protection — this PR builds out the full pipeline needed to look like a real browser session. ## New executor: open-sse/executors/chatgpt-web.ts Auth/request pipeline (per chat completion): 1. exchangeSession() GET /api/auth/session cookie -> JWT (cached ~5min) 2. fetchDpl() GET / scrape data-build + script src 3. runSessionWarmup() GET /backend-api/me, /conversations, /models 4. POST /sentinel/chat-requirements/prepare -> prepare_token 5. POST /sentinel/chat-requirements -> chat-requirements-token + PoW seed/diff 6. solveProofOfWork() SHA3-512 loop -> "gAAAAAB..." sentinel proof token 7. POST /backend-api/f/conversation with all sentinel headers 8. parse SSE stream -> OpenAI chat.completion[.chunk] format Notable details: - 18-element prekey config matching chat2api/openai-sentinel (browser fingerprint values, U+2212 MINUS SIGN in `webdriver−false`). Thin shapes get escalated to mandatory Turnstile. - Two-stage Sentinel handshake (/prepare + /chat-requirements) — sending only the prepare result returns a 403 "Unusual activity" response. - `turnstile.required: true` from Sentinel is treated as advisory; the conv endpoint accepts requests without a Turnstile token as long as PoW + chat- requirements-token are valid. Optional bring-your-own Turnstile via `providerSpecificData.turnstileToken` for accounts that hard-require it. - SSE parser tracks message_id and resets the accumulator on a new turn — chatgpt.com echoes prior assistant messages (with status finished_successfully) before sending the new turn. - entity["...","value", ...] internal markup stripped from output (browser renders these client-side). - Conversation-continuity cache disabled by default: we send history_and_training_disabled: true (Temporary Chat mode) and those conversation_ids expire too fast to reuse — re-using returned 404. Each request now sends conversation_id: null and replays full history, matching what Open WebUI and OpenAI-API-style clients send anyway. ## TLS impersonation: open-sse/services/chatgptTlsClient.ts ChatGPT's Cloudflare config pins cf_clearance to JA3/JA4 TLS fingerprint + HTTP/2 SETTINGS frame. Plain Node Undici fetch always returns cf-mitigated: challenge regardless of cookies. The wrapper module loads `tls-client-node` (Firefox 148 fingerprint) in native runtime mode (.so via koffi) — managed mode spawns a sidecar that conflicts with OmniRoute's global fetch proxy patch. - Lazy singleton TLSClient with process exit hooks - Streaming-capable (file tail) and non-streaming modes - Test injection point: __setTlsFetchOverrideForTesting() lets unit tests mock the client without touching globalThis.fetch ## Provider wiring - open-sse/executors/index.ts — register ChatGptWebExecutor with cgpt-web alias - open-sse/config/providerRegistry.ts — registry entry, format=openai, authHeader=cookie, model gpt-5.3-instant - src/shared/constants/providers.ts — WEB_COOKIE_PROVIDERS UI metadata (icon, color, authHint) - src/lib/providers/validation.ts — validateChatGptWebProvider hits /api/auth/session via the TLS client, detects cf-mitigated/HTML responses and returns a clear "paste full Cookie line" hint instead of a generic "Invalid" - next.config.mjs — mark tls-client-node, koffi, tough-cookie as external packages (Turbopack can't bundle the native .so) ## Cookie format Validator and executor accept any of: - bare value: "eyJhbGc..." - unchunked cookie line: "__Secure-next-auth.session-token=eyJ..." - chunked cookie line: "__Secure-next-auth.session-token.0=...; __Secure-next-auth.session-token.1=..." - full DevTools Cookie header line: "Cookie: __Secure-next-auth.session-token.0=...; cf_clearance=...; ..." NextAuth chunks the JWE when it exceeds 4KB; chunked cookies pass through verbatim (NextAuth reassembles server-side). Recommend pasting the full DevTools Cookie line so cf_clearance, __cf_bm, _cfuvid, _puid travel along — without cf_clearance, Cloudflare blocks the request before NextAuth sees it. ## Tests tests/unit/chatgpt-web.test.ts — 27 tests, all passing: - Registration + alias resolution - Token exchange (cookie -> Bearer flow) - Token cache TTL - Refreshed cookie surfaced via onCredentialsRefreshed callback - Sentinel call ordering (session -> prepare -> chat-requirements -> conv) - Sentinel chat-requirements-token forwarded on conv request - PoW token has gAAAAAB prefix - Turnstile.required: true does NOT block conv (passes through) - Non-streaming chat.completion JSON - Streaming SSE chunks ending with [DONE] - Cumulative-parts diffing yields non-overlapping deltas - Errors: 401 session, 403 sentinel, 429 conv rate-limit - Empty messages -> 400 without any fetch - Missing apiKey -> 401 without any fetch - Cookie format: bare value, unchunked, chunked, "Cookie: ..." DevTools line - Conversation continuity: each call starts a fresh conversation - Browser-like headers on conv POST (UA, Origin, Sec-Fetch-Site, Accept) - Payload shape (action, model=gpt-5-3, history_and_training_disabled) - Provider registry contains chatgpt-web with gpt-5.3-instant model Verification: typecheck:core clean, lint clean (no new warnings), end-to-end manually verified across single-turn, multi-turn (memory preserved), streaming, and Open WebUI-style sequential growing-history flows. ## References - bogdanfinn/tls-client (Go) — TLS impersonation upstream - fatihkabakk/tls-client-node — Node bindings - lanqian528/chat2api — Sentinel/PoW/prekey reference impl (Python) - leetanshaj/openai-sentinel — Prekey config + SHA3-512 solver Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
195 lines
5.4 KiB
JavaScript
195 lines
5.4 KiB
JavaScript
import createNextIntlPlugin from "next-intl/plugin";
|
|
|
|
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
|
|
const distDir = process.env.NEXT_DIST_DIR || ".next";
|
|
|
|
/** @type {import('next').NextConfig} */
|
|
const nextConfig = {
|
|
distDir,
|
|
// Turbopack config: redirect native modules to stubs at build time
|
|
turbopack: {
|
|
resolveAlias: {
|
|
// Point mitm/manager to a stub during build (native child_process/fs can't be bundled)
|
|
"@/mitm/manager": "./src/mitm/manager.stub.ts",
|
|
},
|
|
},
|
|
output: "standalone",
|
|
outputFileTracingExcludes: {
|
|
// Planning/task docs are not runtime assets and can break standalone copies
|
|
// when broad fs/path tracing pulls the whole repository into the NFT graph.
|
|
"/*": [
|
|
"./.git/**/*",
|
|
"./_tasks/**/*",
|
|
"./_references/**/*",
|
|
"./_ideia/**/*",
|
|
"./_mono_repo/**/*",
|
|
"./coverage/**/*",
|
|
"./test-results/**/*",
|
|
"./playwright-report/**/*",
|
|
"./app.__qa_backup/**/*",
|
|
"./tests/**/*",
|
|
],
|
|
},
|
|
serverExternalPackages: [
|
|
"pino",
|
|
"pino-pretty",
|
|
"thread-stream",
|
|
"better-sqlite3",
|
|
"keytar",
|
|
"wreq-js",
|
|
"zod",
|
|
"tls-client-node",
|
|
"koffi",
|
|
"tough-cookie",
|
|
"child_process",
|
|
"fs",
|
|
"path",
|
|
"os",
|
|
"crypto",
|
|
"net",
|
|
"tls",
|
|
"http",
|
|
"https",
|
|
"stream",
|
|
"buffer",
|
|
"util",
|
|
],
|
|
transpilePackages: ["@omniroute/open-sse"],
|
|
allowedDevOrigins: ["localhost", "127.0.0.1", "192.168.*"],
|
|
typescript: {
|
|
// TODO: Re-enable after fixing all sub-component useTranslations scope issues
|
|
ignoreBuildErrors: true,
|
|
},
|
|
images: {
|
|
unoptimized: true,
|
|
},
|
|
webpack: (config, { isServer, webpack }) => {
|
|
if (isServer) {
|
|
// Webpack IgnorePlugin: skip thread-stream test files that contain
|
|
// intentionally broken syntax/imports (they cause Turbopack build errors)
|
|
config.plugins.push(
|
|
new webpack.IgnorePlugin({
|
|
resourceRegExp: /\/test\//,
|
|
contextRegExp: /thread-stream/,
|
|
})
|
|
);
|
|
// ── Turbopack / Next.js 16 module-hash patch (#394, #396, #398) ────────
|
|
//
|
|
// Next.js 16 (with or without Turbopack) compiles the instrumentation hook
|
|
// into a separate chunk and emits hashed require() calls such as:
|
|
// require('better-sqlite3-90e2652d1716b047')
|
|
// require('zod-dcb22c6336e0bc69')
|
|
// require('pino-28069d5257187539')
|
|
//
|
|
// These hashed names don't exist in node_modules and cause a 500 at
|
|
// startup on all npm global installs (issues #394, #396, #398).
|
|
//
|
|
// We use two strategies:
|
|
// 1. Exact-name externals for all known server-side packages.
|
|
// 2. Hash-strip catch-all: any require('<name>-<16hexchars>[/subpath]')
|
|
// strips the hash suffix and falls through to the real package name.
|
|
//
|
|
const HASH_PATTERN = /^(.+)-[0-9a-f]{16}(\/.*)?$/;
|
|
|
|
const KNOWN_EXTERNALS = new Set([
|
|
"better-sqlite3",
|
|
"keytar",
|
|
"wreq-js",
|
|
"zod",
|
|
"pino",
|
|
"pino-pretty",
|
|
"child_process",
|
|
"fs",
|
|
"path",
|
|
"os",
|
|
"crypto",
|
|
"net",
|
|
"tls",
|
|
"http",
|
|
"https",
|
|
"stream",
|
|
"buffer",
|
|
"util",
|
|
]);
|
|
|
|
const prev = config.externals ?? [];
|
|
const prevArr = Array.isArray(prev) ? prev : [prev];
|
|
config.externals = [
|
|
...prevArr,
|
|
({ request }, callback) => {
|
|
// Case 1: Exact known package — treat as external
|
|
if (KNOWN_EXTERNALS.has(request)) {
|
|
return callback(null, `commonjs ${request}`);
|
|
}
|
|
// Case 2: Hash-suffixed name — strip hash, preserve subpath
|
|
// e.g. "better-sqlite3-90e2652d1716b047" → "better-sqlite3"
|
|
// "zod-dcb22c6336e0bc69" → "zod"
|
|
// "zod-dcb22c6336e0bc69/v3" → "zod/v3"
|
|
// "zod-dcb22c6336e0bc69/v4-mini" → "zod/v4-mini"
|
|
const hashMatch = request?.match?.(HASH_PATTERN);
|
|
if (hashMatch) {
|
|
const resolved = hashMatch[2] ? `${hashMatch[1]}${hashMatch[2]}` : hashMatch[1];
|
|
return callback(null, `commonjs ${resolved}`);
|
|
}
|
|
callback();
|
|
},
|
|
];
|
|
} else {
|
|
// Ignore native Node.js modules in browser bundle
|
|
config.resolve.fallback = {
|
|
...config.resolve.fallback,
|
|
fs: false,
|
|
path: false,
|
|
child_process: false,
|
|
net: false,
|
|
tls: false,
|
|
crypto: false,
|
|
};
|
|
}
|
|
return config;
|
|
},
|
|
|
|
async rewrites() {
|
|
return [
|
|
{
|
|
source: "/chat/completions",
|
|
destination: "/api/v1/chat/completions",
|
|
},
|
|
{
|
|
source: "/responses",
|
|
destination: "/api/v1/responses",
|
|
},
|
|
{
|
|
source: "/responses/:path*",
|
|
destination: "/api/v1/responses/:path*",
|
|
},
|
|
{
|
|
source: "/models",
|
|
destination: "/api/v1/models",
|
|
},
|
|
{
|
|
source: "/v1/v1/:path*",
|
|
destination: "/api/v1/:path*",
|
|
},
|
|
{
|
|
source: "/v1/v1",
|
|
destination: "/api/v1",
|
|
},
|
|
{
|
|
source: "/codex/:path*",
|
|
destination: "/api/v1/responses",
|
|
},
|
|
{
|
|
source: "/v1/:path*",
|
|
destination: "/api/v1/:path*",
|
|
},
|
|
{
|
|
source: "/v1",
|
|
destination: "/api/v1",
|
|
},
|
|
];
|
|
},
|
|
};
|
|
|
|
export default withNextIntl(nextConfig);
|