Files
OmniRoute/src/shared/providers/webSessionCredentials.ts

429 lines
17 KiB
TypeScript

import { WEB_COOKIE_PROVIDERS } from "@/shared/constants/providers";
export type WebSessionCredentialRequirement =
| {
kind: "cookie" | "token";
credentialName: string;
placeholder: string;
acceptsFullCookieHeader: boolean;
storageKeys: readonly string[];
/**
* #5465 — Optional i18n key for a provider-specific credential hint that
* REPLACES the generic "Required cookie: {credential}…" copy. Use when the
* generic template is confusing (e.g. t3.chat needs a localStorage value
* AND the Cookie header, so the one-line cookie hint reads circular).
*/
hintKey?: string;
hintFallback?: string;
/** Provider-specific replacement for the generic four-step DevTools guide. */
guideSteps?: readonly string[];
guideNote?: string;
}
| {
kind: "none";
credentialName: "";
placeholder: "";
acceptsFullCookieHeader: false;
storageKeys: readonly [];
};
export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
"chatgpt-web-codex": {
kind: "cookie",
credentialName: "ChatGPT Cookie header (full)",
placeholder: "__Secure-next-auth.session-token=...; cf_clearance=...",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "sessionToken", "session-token", "__Secure-next-auth.session-token"],
},
"zenmux-free": {
kind: "cookie",
credentialName: "Cookie header (full)",
placeholder: "paste the full Cookie header from zenmux.ai",
acceptsFullCookieHeader: true,
storageKeys: ["cookie"],
},
"tencent-aistudio-web": {
kind: "cookie",
credentialName: "Cookie header (full)",
placeholder: "paste the full Cookie header from aistudio.tencent.ai",
acceptsFullCookieHeader: true,
storageKeys: ["cookie"],
},
"tinycms-web": {
kind: "token",
credentialName: "app-config-uuid",
placeholder: "R...",
acceptsFullCookieHeader: false,
storageKeys: ["apiKey", "token", "uuid", "app-config-uuid"],
},
"chatgpt-web": {
kind: "cookie",
credentialName: "__Secure-next-auth.session-token",
placeholder: "__Secure-next-auth.session-token=...",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "sessionToken", "session-token", "__Secure-next-auth.session-token"],
},
"grok-web": {
kind: "cookie",
credentialName: "sso + sso-rw",
placeholder: "sso=...; sso-rw=...",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "sso", "sso-rw"],
// #7567 — grok.com's cf_clearance cookie is pinned to IP + User-Agent + TLS
// fingerprint of the browser that earned it, so pasting it from a different
// machine/IP causes a 403 that is actually correct Cloudflare behavior. Point
// users at the Custom User-Agent field under Advanced Settings + same IP/proxy,
// instead of the generic (and here misleading) single-cookie hint.
hintKey: "grokWebCookieHint",
hintFallback:
"grok.com's cf_clearance cookie is pinned to the IP, User-Agent, and TLS fingerprint of the browser where you copied it — pasting it from a different machine/IP causes a 403. Paste sso and sso-rw here, then open Advanced Settings and fill Custom User-Agent with the EXACT User-Agent string of that same browser, and use the same IP/proxy for this connection.",
},
"gemini-web": {
kind: "cookie",
credentialName: "__Secure-1PSID (optional: __Secure-1PSIDTS)",
placeholder: "__Secure-1PSID=...; __Secure-1PSIDTS=...",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "__Secure-1PSID", "__Secure-1PSIDTS"],
},
"notion-web": {
kind: "cookie",
credentialName: "token_v2 (optional: space_id, notion_browser_id)",
placeholder: "token_v2=...; space_id=...; notion_browser_id=...",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "token_v2", "space_id", "notion_browser_id"],
},
"gemini-business": {
kind: "cookie",
credentialName: "__Secure-1PSID (optional: __Secure-1PSIDTS)",
placeholder: "__Secure-1PSID=...; __Secure-1PSIDTS=... (from business.gemini.google)",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "__Secure-1PSID", "__Secure-1PSIDTS"],
},
"perplexity-web": {
kind: "cookie",
credentialName: "__Secure-next-auth.session-token",
placeholder: "__Secure-next-auth.session-token=...",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "sessionToken", "session-token", "__Secure-next-auth.session-token"],
},
hyperagent: {
kind: "cookie",
credentialName: "Session Cookie",
placeholder: "Paste full Cookie header from hyperagent.com",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "sessionCookie", "authCookie"],
},
"blackbox-web": {
kind: "cookie",
credentialName: "__Secure-authjs.session-token",
placeholder: "__Secure-authjs.session-token=...; other=value",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "sessionToken", "__Secure-authjs.session-token"],
},
"muse-spark-web": {
kind: "cookie",
// #9502: the WS protocol (#7528) needs both the ecto_1_sess cookie (GraphQL
// warmup/mode-switch) and a separate ecto1:... WS auth token (Authorization
// query param on wss://gateway.meta.ai/ws/clippy). The executor extracts the
// ecto1: token from the apiKey field via /ecto1:[^\s;]+/i.
credentialName: "ecto_1_sess + ecto1: WS auth token",
placeholder:
"ecto_1_sess=...; ecto1:... (WS auth token from meta.ai DevTools → Network → WS → clippy)",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "ecto_1_sess", "abra_sess"],
},
"hailuo-web": {
kind: "token",
credentialName: "_token",
placeholder: '_token=... (hailuo.ai → DevTools → Local Storage → "_token")',
acceptsFullCookieHeader: false,
storageKeys: ["token", "_token"],
},
"claude-web": {
kind: "cookie",
credentialName: "sessionKey",
placeholder: "sessionKey=... or full Cookie header from claude.ai",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "sessionKey"],
},
"deepseek-web": {
kind: "token",
credentialName: "userToken",
placeholder: "userToken=... or paste raw userToken",
acceptsFullCookieHeader: false,
storageKeys: ["token", "userToken"],
},
"copilot-web": {
kind: "token",
credentialName: "access_token",
placeholder: "access_token=... or a DevTools HAR export",
acceptsFullCookieHeader: false,
storageKeys: ["token", "access_token", "accessToken"],
},
"microsoft-designer-web": {
kind: "token",
credentialName: "access_token",
placeholder: "access_token=... (Authorization: Bearer header from the DallE.ashx request)",
acceptsFullCookieHeader: false,
storageKeys: ["token", "access_token", "accessToken"],
},
"copilot-m365-web": {
kind: "token",
credentialName: "access_token + chathubPath",
placeholder: "access_token=...; chathubPath=redacted",
acceptsFullCookieHeader: false,
storageKeys: ["token", "access_token", "accessToken", "chathubPath", "userTenant"],
},
"t3-web": {
kind: "cookie",
credentialName: "convex-session-id + Cookie header",
placeholder: "convex-session-id=abc123...; Cookie: ...",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "convex-session-id", "convexSessionId"],
// #5465 — the generic cookie hint reads circular for t3.chat (needs a
// localStorage value AND the Cookie header); use the step-by-step DevTools
// copy that already ships translated in every locale.
hintKey: "t3ChatWebCookieHint",
},
"adapta-web": {
kind: "cookie",
credentialName: "__client",
placeholder: "__client=... or full Cookie header from agent.adapta.one",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "__client"],
},
"inner-ai": {
kind: "cookie",
credentialName: "token + email",
placeholder: "token_value user@example.com",
acceptsFullCookieHeader: false,
storageKeys: ["token", "cookie", "email"],
},
huggingchat: {
kind: "cookie",
credentialName: "full Cookie header (hf-chat + token)",
placeholder:
"hf-chat=...; token=...; aws-waf-token=... (full Cookie header from huggingface.co)",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "hf-chat"],
},
"yuanbao-web": {
kind: "cookie",
credentialName: "full Cookie header (hy_user + hy_token)",
placeholder: "hy_user=...; hy_token=... (full Cookie header from yuanbao.tencent.com)",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "hy_user", "hy_token"],
},
"poe-web": {
kind: "cookie",
credentialName: "p-b",
placeholder: "p-b=... or full Cookie header from poe.com",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "p-b"],
},
"venice-web": {
kind: "cookie",
credentialName: "session",
placeholder: "session=... or full Cookie header from venice.ai",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "session"],
},
"v0-vercel-web": {
kind: "cookie",
credentialName: "__vercel_session",
placeholder: "__vercel_session=... or full Cookie header from v0.dev",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "__vercel_session"],
},
"kimi-web": {
kind: "token",
credentialName: "access_token",
placeholder: "access_token from www.kimi.com localStorage",
acceptsFullCookieHeader: true,
storageKeys: ["token", "access_token", "accessToken", "cookie", "kimi-auth"],
},
"doubao-web": {
kind: "cookie",
credentialName: "full Cookie header (sessionid + ttwid + s_v_web_id)",
placeholder:
"sessionid=...; ttwid=...; s_v_web_id=... (or fp=verify_... fallback from www.dola.com)",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "sessionid", "ttwid", "s_v_web_id", "fp"],
},
"qwen-web": {
kind: "cookie",
credentialName: "full Cookie header (must include cna, ssxmod_itna, token)",
placeholder:
"cna=...; token=...; ssxmod_itna=...; ssxmod_itna2=... (full Cookie header from chat.qwen.ai)",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "token", "ssxmod_itna", "ssxmod_itna2", "cna", "tongyi_sso_ticket"],
},
"duckduckgo-web": {
kind: "cookie",
credentialName: "duckai",
placeholder: "duckai=... or full Cookie header from duckduckgo.com",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "duckai"],
},
"t3-chat-web": {
kind: "token",
credentialName: "token",
placeholder: "Paste your T3 Chat token from t3.chat (Local Storage → token)",
acceptsFullCookieHeader: false,
storageKeys: ["token"],
},
"chatglm-web": {
kind: "cookie",
credentialName: "chatglm_session",
placeholder: "chatglm_session=... or full Cookie header from chatglm.cn",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "chatglm_session"],
},
"xiaomimimo-web": {
kind: "cookie",
credentialName: "session",
placeholder: "session=... or full Cookie header from aistudio.xiaomimimo.com",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "session"],
},
"manus-web": {
kind: "cookie",
credentialName: "manus_session",
placeholder: "manus_session=... or full Cookie header from manus.im",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "manus_session"],
},
"zai-web": {
kind: "token",
credentialName: 'Local Storage value named "token"',
placeholder: "eyJ... (chat.z.ai → DevTools → Application → Local Storage → token)",
acceptsFullCookieHeader: false,
storageKeys: ["token"],
hintKey: "zaiWebCredentialHint",
hintFallback:
'Copy only the "token" value from chat.z.ai Local Storage. Do not copy a Cookie header. OmniRoute uses its browser transport to obtain the per-request CAPTCHA proof.',
guideSteps: [
"Open chat.z.ai and sign in.",
"Open DevTools → Application → Local Storage → https://chat.z.ai.",
'Find the row named "token" and copy only its value. Do not copy any Cookie header.',
"Paste the token below and check the connection. OmniRoute handles the per-request CAPTCHA through its browser transport.",
],
guideNote:
"Treat the token like a password. Browser transport is enabled by default; do not set OMNIROUTE_BROWSER_POOL=off for this connection. If Z.ai signs you out or the token expires, repeat these steps with the new value.",
},
lmarena: {
kind: "cookie",
// arena.ai's auth cookie is `arena-auth-prod-v1` (the legacy hint said `session`,
// which never matched the real cookie name and confused users). #3810
//
// #4271: LMArena migrated to Supabase SSR chunked cookies — the single
// `arena-auth-prod-v1` cookie is now empty and the session is split across
// `arena-auth-prod-v1.0`, `arena-auth-prod-v1.1`, … Users must paste the FULL
// Cookie header so the executor can reconstruct the single cookie from chunks.
credentialName: "full Cookie header (arena-auth-prod-v1.0 + arena-auth-prod-v1.1)",
placeholder:
"arena-auth-prod-v1.0=...; arena-auth-prod-v1.1=...; other=value (full Cookie header from arena.ai)",
acceptsFullCookieHeader: true,
storageKeys: [
"cookie",
"arena-auth-prod-v1",
"arena-auth-prod-v1.0",
"arena-auth-prod-v1.1",
"session",
],
hintKey: "lmarenaWebCookieHint",
hintFallback:
"Open arena.ai, sign in, then copy the full Cookie header from a Network request. Include arena-auth-prod-v1.0 and arena-auth-prod-v1.1 (and further chunks if present), preferably with cf_clearance. Do not paste only the empty arena-auth-prod-v1 cookie. Optional: providerSpecificData.recaptchaV3Token if create-evaluation still returns 403.",
},
promptql: {
kind: "token",
credentialName: "Bearer JWT (optional: projectId, session Cookie)",
placeholder: "eyJ... (Authorization Bearer from prompt.ql.app)",
acceptsFullCookieHeader: false,
storageKeys: ["token", "jwt", "apiKey", "projectId", "project_id", "cookie"],
},
"adobe-firefly": {
// Prefer IMS access_token JWT (Bearer). Cookie from firefly.adobe.com alone
// only mints a guest IMS token. Kind stays "cookie" for multi-account UX;
// resolveAdobeAccessToken auto-detects JWT vs cookie and rejects guests.
kind: "cookie",
credentialName: "IMS access_token JWT (recommended) or multi-domain Cookie",
placeholder:
"Paste eyJ… JWT from Authorization: Bearer on firefly-3p generate request (not page Cookie alone)",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "token", "access_token", "accessToken"],
},
"conol-web": {
kind: "cookie",
credentialName: "__Secure-better-auth.session_token",
placeholder:
"__Secure-better-auth.session_token=... or full Cookie header from conol.ai",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "__Secure-better-auth.session_token"],
},
} satisfies Record<string, WebSessionCredentialRequirement> &
Record<keyof typeof WEB_COOKIE_PROVIDERS, WebSessionCredentialRequirement>;
export function getWebSessionCredentialRequirement(
providerId: unknown
): WebSessionCredentialRequirement | null {
if (typeof providerId !== "string") return null;
return (
WEB_SESSION_CREDENTIAL_REQUIREMENTS[
providerId as keyof typeof WEB_SESSION_CREDENTIAL_REQUIREMENTS
] ?? null
);
}
export function canUpdateProviderApiKey(authType: unknown, providerId: unknown): boolean {
if (authType === "apikey") return true;
if (authType !== "cookie") return false;
return getWebSessionCredentialRequirement(providerId)?.kind === "token";
}
export function requiresWebSessionCredential(providerId: unknown): boolean {
const requirement = getWebSessionCredentialRequirement(providerId);
return !!requirement && requirement.kind !== "none";
}
function hasNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
export function hasUsableWebSessionCredential(
providerId: unknown,
providerSpecificData: unknown
): boolean {
const requirement = getWebSessionCredentialRequirement(providerId);
if (!requirement || requirement.kind === "none") return false;
if (!providerSpecificData || typeof providerSpecificData !== "object") return false;
const data = providerSpecificData as Record<string, unknown>;
return requirement.storageKeys.some((key) => hasNonEmptyString(data[key]));
}
/**
* Resolve the value that a web-session import must store in the connection's
* `apiKey` column.
*
* `token`-kind providers (deepseek-web, copilot-web, copilot-m365-web,
* t3-chat-web, …) are authenticated from `apiKey`: both the connection
* validator (`validateDeepSeekWebProvider({ apiKey })`) and the executor
* (`extractUserToken` → `credentials.apiKey`) read the token there — never from
* `providerSpecificData`. The bulk web-session import used to leave `apiKey`
* null and stash the token only in `providerSpecificData`, so imported token-kind
* connections were never recognized. Return the credential for token-kind so the
* import stores it where those readers look.
*
* `cookie`-kind providers keep `apiKey` null — their executors read the full
* cookie from `providerSpecificData.cookie`.
*/
export function resolveWebSessionImportApiKey(
requirement: WebSessionCredentialRequirement | null,
credential: string
): string | null {
if (!requirement || requirement.kind !== "token") return null;
const trimmed = typeof credential === "string" ? credential.trim() : "";
return trimmed.length > 0 ? trimmed : null;
}