refactor(open-sse): registry consolidation + DRY media/oauth/adhoc cleanup

- Single-source registry: oauth clientId/tokenUrl, usage URLs, image/embed
  configs, search defaultModel, codex fixedPort, google token url derive.
- Remove 29 unused OmniRoute providers (registry 100→71); media intact.
- De-adhoc: codex literals → registry format/oauth flags; reasoningInject,
  image/embed openrouter headers + xai bodyFields config-driven.
- Add REGISTRY_TEMPLATE.js + expand PROVIDER_DEFAULTS/schema JSDoc.
- Baselines updated; PROVIDERS 62 + alias 90 byte-for-byte, golden snapshots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-14 13:15:48 +07:00
co-authored by Cursor
parent 9105dd0e25
commit bb9e9aa91f
166 changed files with 3089 additions and 3067 deletions
+25 -184
View File
@@ -1,8 +1,9 @@
/**
* OAuth Configuration Constants
* OAuth Configuration Constants — static data lives in registry, re-exported here for consumers.
*/
import { platform, arch } from "os";
import { ANTIGRAVITY_OAUTH_CLIENT, GOOGLE_OAUTH_CLIENT } from "open-sse/providers/shared.js";
import { PROVIDER_OAUTH, PROVIDERS as REGISTRY_PROVIDERS } from "open-sse/providers/index.js";
/**
* Get the platform enum value based on the current OS.
@@ -18,101 +19,34 @@ function getOAuthPlatformEnum() {
}
// Claude OAuth Configuration (Authorization Code Flow with PKCE)
export const CLAUDE_CONFIG = {
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
authorizeUrl: "https://claude.ai/oauth/authorize",
tokenUrl: "https://api.anthropic.com/v1/oauth/token",
scopes: ["org:create_api_key", "user:profile", "user:inference"],
codeChallengeMethod: "S256",
};
export const CLAUDE_CONFIG = { ...PROVIDER_OAUTH["claude"] };
// Codex (OpenAI) OAuth Configuration (Authorization Code Flow with PKCE)
export const CODEX_CONFIG = {
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
authorizeUrl: "https://auth.openai.com/oauth/authorize",
tokenUrl: "https://auth.openai.com/oauth/token",
scope: "openid profile email offline_access",
codeChallengeMethod: "S256",
// Additional OpenAI-specific params
extraParams: {
id_token_add_organizations: "true",
codex_cli_simplified_flow: "true",
originator: "codex_cli_rs",
},
};
export const CODEX_CONFIG = { ...PROVIDER_OAUTH["codex"] };
// Gemini (Google) OAuth Configuration (Standard OAuth2)
export const GEMINI_CONFIG = {
...GOOGLE_OAUTH_CLIENT,
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
userInfoUrl: "https://www.googleapis.com/oauth2/v1/userinfo",
scopes: [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
],
};
// clientId/clientSecret from GOOGLE_OAUTH_CLIENT (shared.js) — not stored in registry
export const GEMINI_CONFIG = { ...GOOGLE_OAUTH_CLIENT, ...PROVIDER_OAUTH["gemini-cli"] };
// Qwen OAuth Configuration (Device Code Flow with PKCE)
export const QWEN_CONFIG = {
clientId: "f0304373b74a44d2b584a3fb70ca9e56",
deviceCodeUrl: "https://chat.qwen.ai/api/v1/oauth2/device/code",
tokenUrl: "https://chat.qwen.ai/api/v1/oauth2/token",
scope: "openid profile email model.completion",
codeChallengeMethod: "S256",
};
export const QWEN_CONFIG = { ...PROVIDER_OAUTH["qwen"] };
// Qoder OAuth Configuration (Device Token Flow with PKCE).
// Device tokens are long-lived (~30 days for access, ~360 for refresh).
// The upstream refresh endpoint at center.qoder.sh returns 403 for our
// flow — we accept that and surface it to the user as "re-login" instead
// of attempting to silently rotate.
export const QODER_CONFIG = {
openApiBaseUrl: "https://openapi.qoder.sh",
centerBaseUrl: "https://center.qoder.sh",
chatBaseUrl: "https://api3.qoder.sh",
deviceTokenUrl: "https://openapi.qoder.sh/api/v1/deviceToken/poll",
refreshUrl: "https://center.qoder.sh/algo/api/v3/user/refresh_token",
userInfoUrl: "https://openapi.qoder.sh/api/v1/userinfo",
quotaUsageUrl: "https://openapi.qoder.sh/api/v2/quota/usage",
loginUrl: "https://qoder.com/device/selectAccounts",
};
export const QODER_CONFIG = { ...PROVIDER_OAUTH["qoder"] };
// iFlow OAuth Configuration (Authorization Code)
export const IFLOW_CONFIG = {
clientId: "10009311001",
clientSecret: "4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW",
authorizeUrl: "https://iflow.cn/oauth",
tokenUrl: "https://iflow.cn/oauth/token",
userInfoUrl: "https://iflow.cn/api/oauth/getUserInfo",
extraParams: {
loginMethod: "phone",
type: "phone",
},
};
export const IFLOW_CONFIG = { ...PROVIDER_OAUTH["iflow"] };
// Antigravity OAuth Configuration (Standard OAuth2 with Google)
// clientId/clientSecret from ANTIGRAVITY_OAUTH_CLIENT (shared.js) — not stored in registry
// loadCodeAssistClientMetadata is dynamic (runtime platform detection)
export const ANTIGRAVITY_CONFIG = {
...ANTIGRAVITY_OAUTH_CLIENT,
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
userInfoUrl: "https://www.googleapis.com/oauth2/v1/userinfo",
scopes: [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/cclog",
"https://www.googleapis.com/auth/experimentsandconfigs",
],
// Antigravity specific
apiEndpoint: "https://cloudcode-pa.googleapis.com",
apiVersion: "v1internal",
loadCodeAssistEndpoint: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
onboardUserEndpoint: "https://cloudcode-pa.googleapis.com/v1internal:onboardUser",
loadCodeAssistUserAgent: "google-api-nodejs-client/9.15.1",
loadCodeAssistApiClient: "google-cloud-sdk vscode_cloudshelleditor/0.1",
// Numeric enums matching Antigravity binary ClientMetadata (see getOAuthClientMetadata below)
...PROVIDER_OAUTH["antigravity"],
loadCodeAssistClientMetadata: JSON.stringify({ ideType: 9, platform: getOAuthPlatformEnum(), pluginType: 2 }),
};
@@ -125,136 +59,43 @@ export function getOAuthClientMetadata() {
}
// OpenAI OAuth Configuration (Authorization Code Flow with PKCE)
export const OPENAI_CONFIG = {
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
authorizeUrl: "https://auth.openai.com/oauth/authorize",
tokenUrl: "https://auth.openai.com/oauth/token",
scope: "openid profile email offline_access",
codeChallengeMethod: "S256",
extraParams: {
id_token_add_organizations: "true",
originator: "openai_native",
},
};
export const OPENAI_CONFIG = { ...PROVIDER_OAUTH["openai"] };
// GitHub Copilot OAuth Configuration (Device Code Flow)
export const GITHUB_CONFIG = {
clientId: "Iv1.b507a08c87ecfe98",
deviceCodeUrl: "https://github.com/login/device/code",
tokenUrl: "https://github.com/login/oauth/access_token",
userInfoUrl: "https://api.github.com/user",
scopes: "read:user",
apiVersion: "2022-11-28", // Updated to supported version
copilotTokenUrl: "https://api.github.com/copilot_internal/v2/token",
userAgent: "GitHubCopilotChat/0.26.7",
editorVersion: "vscode/1.85.0",
editorPluginVersion: "copilot-chat/0.26.7",
};
export const GITHUB_CONFIG = { ...PROVIDER_OAUTH["github"] };
// Kiro OAuth Configuration
// Supports multiple auth methods:
// 1. AWS Builder ID (Device Code Flow)
// 2. AWS IAM Identity Center/IDC (Device Code Flow with custom startUrl/region)
// 3. Google/GitHub Social Login (Authorization Code Flow - manual callback)
// 4. Import Token (paste refresh token from Kiro IDE)
export const KIRO_CONFIG = {
// AWS SSO OIDC endpoints for Builder ID/IDC (Device Code Flow)
ssoOidcEndpoint: "https://oidc.us-east-1.amazonaws.com",
registerClientUrl: "https://oidc.us-east-1.amazonaws.com/client/register",
deviceAuthUrl: "https://oidc.us-east-1.amazonaws.com/device_authorization",
tokenUrl: "https://oidc.us-east-1.amazonaws.com/token",
// AWS Builder ID default start URL
startUrl: "https://view.awsapps.com/start",
// Client registration params
clientName: "kiro-oauth-client",
clientType: "public",
scopes: ["codewhisperer:completions", "codewhisperer:analysis", "codewhisperer:conversations"],
grantTypes: ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
issuerUrl: "https://identitycenter.amazonaws.com/ssoins-722374e8c3c8e6c6",
// Social auth endpoints (Google/GitHub via AWS Cognito)
socialAuthEndpoint: "https://prod.us-east-1.auth.desktop.kiro.dev",
socialLoginUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/login",
socialTokenUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/oauth/token",
socialRefreshUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken",
// Auth methods
authMethods: ["builder-id", "idc", "google", "github", "import"],
};
// Kiro OAuth Configuration (multi-method: AWS Builder ID / IDC / Social / Import Token)
export const KIRO_CONFIG = { ...PROVIDER_OAUTH["kiro"] };
// Cursor OAuth Configuration (Import Token from Cursor IDE)
// Cursor stores credentials in SQLite database: state.vscdb
// Keys: cursorAuth/accessToken, storage.serviceMachineId
// tokenStoragePaths: user-reference only, not stored in registry
export const CURSOR_CONFIG = {
// API endpoints
apiEndpoint: "https://api2.cursor.sh",
chatEndpoint: "/aiserver.v1.ChatService/StreamUnifiedChatWithTools",
modelsEndpoint: "/aiserver.v1.AiService/GetDefaultModelNudgeData",
// Additional endpoints
api3Endpoint: "https://api3.cursor.sh", // Telemetry
agentEndpoint: "https://agent.api5.cursor.sh", // Privacy mode
agentNonPrivacyEndpoint: "https://agentn.api5.cursor.sh", // Non-privacy mode
// Client metadata
clientVersion: "3.1.0",
clientType: "ide",
// Token storage locations (for user reference)
...PROVIDER_OAUTH["cursor"],
tokenStoragePaths: {
linux: "~/.config/Cursor/User/globalStorage/state.vscdb",
macos: "/Users/<user>/Library/Application Support/Cursor/User/globalStorage/state.vscdb",
windows: "%APPDATA%\\Cursor\\User\\globalStorage\\state.vscdb",
},
// Database keys
dbKeys: {
accessToken: "cursorAuth/accessToken",
machineId: "storage.serviceMachineId",
},
};
// Kimi Coding OAuth Configuration (Device Code Flow)
// clientId uses env override — dynamic, not stored in registry
export const KIMI_CODING_CONFIG = {
clientId: process.env.KIMI_CODING_OAUTH_CLIENT_ID || "17e5f671-d194-4dfb-9706-5516cb48c098",
deviceCodeUrl: "https://auth.kimi.com/api/oauth/device_authorization",
tokenUrl: "https://auth.kimi.com/api/oauth/token",
...PROVIDER_OAUTH["kimi-coding"],
clientId: process.env.KIMI_CODING_OAUTH_CLIENT_ID || REGISTRY_PROVIDERS["kimi-coding"]?.clientId,
};
// KiloCode OAuth Configuration (Custom Device Auth Flow)
export const KILOCODE_CONFIG = {
apiBaseUrl: "https://api.kilo.ai",
initiateUrl: "https://api.kilo.ai/api/device-auth/codes",
pollUrlBase: "https://api.kilo.ai/api/device-auth/codes",
};
export const KILOCODE_CONFIG = { ...PROVIDER_OAUTH["kilocode"] };
// Cline OAuth Configuration (Local Callback Flow via app.cline.bot)
export const CLINE_CONFIG = {
appBaseUrl: "https://app.cline.bot",
apiBaseUrl: "https://api.cline.bot",
authorizeUrl: "https://api.cline.bot/api/v1/auth/authorize",
tokenExchangeUrl: "https://api.cline.bot/api/v1/auth/token",
refreshUrl: "https://api.cline.bot/api/v1/auth/refresh",
};
export const CLINE_CONFIG = { ...PROVIDER_OAUTH["cline"] };
// GitLab Duo OAuth Configuration (Authorization Code Flow with PKCE)
// Supports both OAuth (PKCE) and Personal Access Token (PAT) modes
export const GITLAB_CONFIG = {
defaultBaseUrl: "https://gitlab.com",
authorizeUrlPath: "/oauth/authorize",
tokenUrlPath: "/oauth/token",
userInfoUrlPath: "/api/v4/user",
scope: "api read_user",
codeChallengeMethod: "S256",
};
export const GITLAB_CONFIG = { ...PROVIDER_OAUTH["gitlab"] };
// CodeBuddy (Tencent) OAuth Configuration (Browser OAuth Polling Flow)
// Step 1: POST /v2/plugin/auth/state?platform=CLI → get { state, authUrl }
// Step 2: Open authUrl in browser
// Step 3: Poll POST /v2/plugin/auth/token with state until success
export const CODEBUDDY_CONFIG = {
baseUrl: "https://copilot.tencent.com",
stateUrl: "https://copilot.tencent.com/v2/plugin/auth/state",
tokenUrl: "https://copilot.tencent.com/v2/plugin/auth/token",
refreshUrl: "https://copilot.tencent.com/v2/plugin/auth/token/refresh",
userAgent: "CLI/2.63.2 CodeBuddy/2.63.2",
platform: "CLI",
pollInterval: 5000,
};
export const CODEBUDDY_CONFIG = { ...PROVIDER_OAUTH["codebuddy"] };
// OAuth timeout (5 minutes)
export const OAUTH_TIMEOUT = 300000;
+3 -2
View File
@@ -4,9 +4,10 @@
* Source of truth: router-for-me/CLIProxyAPI internal/auth/xai/types.go
* Mirrors the upstream Go constants 1:1.
*/
import { PROVIDERS } from "open-sse/providers/index.js";
// xAI client_id for OAuth (PKCE public client)
export const XAI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
// xAI client_id for OAuth (PKCE public client) — single source: registry xai.transport
export const XAI_CLIENT_ID = PROVIDERS["xai"]?.clientId;
// OAuth issuer + endpoints
export const XAI_ISSUER = "https://auth.x.ai";
+2 -2
View File
@@ -200,8 +200,8 @@ const PROVIDERS = {
codex: {
config: CODEX_CONFIG,
flowType: "authorization_code_pkce",
fixedPort: 1455,
callbackPath: "/auth/callback",
fixedPort: CODEX_CONFIG.fixedPort,
callbackPath: CODEX_CONFIG.callbackPath,
buildAuthUrl: (config, redirectUri, state, codeChallenge) => {
const params = {
response_type: "code",
+1 -1
View File
@@ -76,7 +76,7 @@ export class CodexService extends OAuthService {
spinner.text = "Starting local server...";
// Start local server for callback (use fixed port 1455 like real Codex CLI)
const fixedPort = 1455;
const fixedPort = CODEX_CONFIG.fixedPort;
let callbackParams = null;
const { port, close } = await startLocalServer((params) => {
callbackParams = params;
+2 -1
View File
@@ -1,5 +1,6 @@
import http from "http";
import { URL } from "url";
import { CODEX_CONFIG } from "../constants/oauth.js";
/**
* Start a local HTTP server to receive OAuth callback
@@ -119,7 +120,7 @@ let codexProxyServer = null;
let codexProxyTimeout = null;
const CODEX_PROXY_TIMEOUT_MS = 300000; // 5 minutes
const CODEX_PORT = 1455;
const CODEX_PORT = CODEX_CONFIG.fixedPort;
// Pending exchange sessions keyed by state — used by server-side exchange mode
const pendingExchanges = new Map();
+2 -64
View File
@@ -1,64 +1,2 @@
/**
* Qoder API constants ported from CLIProxyAPIPlus qoder-provider branch.
*
* Endpoint set:
* openapi.qoder.sh - device flow + userinfo + quota usage
* center.qoder.sh - token refresh (best-effort, currently 403 for device tokens)
* api3.qoder.sh - inference (chat) + model list, requires COSY signing
* qoder.com/device - browser landing page for device authorization
*/
export const QODER_OPENAPI_BASE = "https://openapi.qoder.sh";
export const QODER_CENTER_BASE = "https://center.qoder.sh";
export const QODER_CHAT_BASE = "https://api3.qoder.sh";
export const QODER_LOGIN_URL = "https://qoder.com/device/selectAccounts";
// Device flow endpoints
export const QODER_DEVICE_TOKEN_URL = `${QODER_OPENAPI_BASE}/api/v1/deviceToken/poll`;
export const QODER_USERINFO_URL = `${QODER_OPENAPI_BASE}/api/v1/userinfo`;
export const QODER_QUOTA_USAGE_URL = `${QODER_OPENAPI_BASE}/api/v2/quota/usage`;
export const QODER_REFRESH_TOKEN_URL = `${QODER_CENTER_BASE}/algo/api/v3/user/refresh_token`;
// Inference endpoints (under /algo on api3.qoder.sh, all COSY-signed)
export const QODER_CHAT_SIG_PATH = "/api/v2/service/pro/sse/agent_chat_generation";
export const QODER_CHAT_URL = `${QODER_CHAT_BASE}/algo${QODER_CHAT_SIG_PATH}?FetchKeys=llm_model_result&AgentId=agent_common`;
export const QODER_CHAT_URL_ENCODED = `${QODER_CHAT_URL}&Encode=1`;
export const QODER_MODEL_LIST_URL = `${QODER_CHAT_BASE}/algo/api/v2/model/list`;
// COSY header constants. These are not arbitrary — the upstream signature
// validation matches them against the values used at signing time.
export const QODER_IDE_VERSION = "1.0.0";
export const QODER_CLIENT_TYPE = "5";
export const QODER_DATA_POLICY = "disagree";
export const QODER_LOGIN_VERSION = "v2";
export const QODER_MACHINE_OS = "x86_64_windows";
export const QODER_MACHINE_TYPE = "5";
// Canonical model identifiers. Identity map — keep as a map so callers can
// cheaply test "is this a known qoder model?" before sending the request.
export const QODER_MODEL_MAP = {
// Tier models
auto: "auto",
ultimate: "ultimate",
performance: "performance",
efficient: "efficient",
lite: "lite",
// Frontier models
qmodel: "qmodel",
qmodel_latest: "qmodel_latest",
dmodel: "dmodel",
dfmodel: "dfmodel",
gm51model: "gm51model",
kmodel: "kmodel",
mmodel: "mmodel",
};
// RSA public key for COSY encryption (extracted from Qoder IDE v0.9).
// Matches the CLIProxyAPIPlus branch and live qodercli traffic.
export const QODER_RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
XcW+ML9FoCI6AOvOzwIDAQAB
-----END PUBLIC KEY-----`;
// Re-export: qoder constants moved to open-sse/shared/qoder (open-sse self-contained, docs 00 §1b).
export * from "../../../open-sse/shared/qoder/constants.js";
+2 -175
View File
@@ -1,175 +1,2 @@
/**
* Qoder COSY (hybrid RSA+AES+MD5) signing, ported from CLIProxyAPIPlus
* qoder-provider branch (internal/auth/qoder/cosy.go).
*
* Every signed request carries:
* - an AES-128-CBC payload of the user info, the AES key wrapped in RSA
* - an MD5 signature over `payload || cosyKey || timestamp || body || sigPath`
* - the body's MD5 hash + length so the server can validate integrity
* - 17 Cosy-* / X-* headers fingerprinting the client (machine id, IDE
* version, organization id, etc.)
*
* The on-the-wire header keys use the same casing as qodercli:
* Cosy-Machineid, not Cosy-MachineID.
*/
import crypto from "crypto";
import { v4 as uuidv4 } from "uuid";
import {
QODER_CLIENT_TYPE,
QODER_DATA_POLICY,
QODER_IDE_VERSION,
QODER_LOGIN_VERSION,
QODER_MACHINE_OS,
QODER_MACHINE_TYPE,
QODER_RSA_PUBLIC_KEY,
} from "./constants.js";
// AES-128 wants a 16-byte key. Match qodercli/Veria: take the first 16 chars
// of a fresh UUID's canonical string (hyphens included). The key is fresh
// per request so even though the IV reuses the key bytes, each request still
// has a unique IV.
function generateAesKey() {
return uuidv4().slice(0, 16);
}
function pkcs7Pad(data, blockSize) {
const padding = blockSize - (data.length % blockSize);
const padded = Buffer.alloc(data.length + padding, padding);
data.copy(padded, 0);
return padded;
}
function aesEncryptCbcBase64(plaintext, keyStr) {
const keyBytes = Buffer.from(keyStr, "utf8");
if (keyBytes.length !== 16) {
throw new Error(`aes key must be 16 bytes, got ${keyBytes.length}`);
}
const iv = keyBytes.subarray(0, 16);
const cipher = crypto.createCipheriv("aes-128-cbc", keyBytes, iv);
cipher.setAutoPadding(false);
const padded = pkcs7Pad(Buffer.from(plaintext, "utf8"), 16);
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
return encrypted.toString("base64");
}
function rsaEncryptBase64(data) {
const encrypted = crypto.publicEncrypt(
{ key: QODER_RSA_PUBLIC_KEY, padding: crypto.constants.RSA_PKCS1_PADDING },
Buffer.from(data, "utf8"),
);
return encrypted.toString("base64");
}
function encryptUserInfo(userInfo) {
const aesKey = generateAesKey();
const plaintext = JSON.stringify(userInfo);
const infoB64 = aesEncryptCbcBase64(plaintext, aesKey);
const cosyKeyB64 = rsaEncryptBase64(aesKey);
return { cosyKey: cosyKeyB64, info: infoB64 };
}
function md5Hex(input) {
return crypto.createHash("md5").update(input).digest("hex");
}
/**
* Strip the leading "/algo" prefix from the request path. Matches qodercli
* convention. Empty input returns "".
*/
function computeSigPath(requestUrl) {
let pathname;
try {
pathname = new URL(requestUrl).pathname || "";
} catch {
return "";
}
if (pathname.startsWith("/algo")) {
return pathname.slice("/algo".length);
}
return pathname;
}
/**
* Generate a fresh machine UUID. Persisted on the connection record so
* every request from the same auth carries the same machineId.
*/
export function generateMachineId() {
return uuidv4();
}
/**
* Build the full Cosy-* header set for a single Qoder request.
*
* @param {Buffer|Uint8Array|string} body The exact bytes that will be sent.
* For GET requests pass an empty Buffer / "".
* @param {string} requestUrl Full request URL (used for sigPath).
* @param {object} creds
* @param {string} creds.userId Stable Qoder user id.
* @param {string} creds.authToken Device access token (`dt-...`).
* @param {string} [creds.name] Display name (optional).
* @param {string} [creds.email] Email (optional, can be empty).
* @param {string} [creds.machineId] Persisted machine UUID.
* @returns {Record<string, string>} Header map ready to merge onto fetch().
*/
export function buildCosyHeaders(body, requestUrl, creds) {
if (!creds?.userId) throw new Error("cosy: user id is empty");
if (!creds?.authToken) throw new Error("cosy: auth token is empty");
const bodyBuf = Buffer.isBuffer(body)
? body
: typeof body === "string"
? Buffer.from(body, "latin1")
: Buffer.from(body || []);
const { cosyKey, info } = encryptUserInfo({
uid: creds.userId,
security_oauth_token: creds.authToken,
name: creds.name || "",
aid: "",
email: creds.email || "",
});
const timestamp = String(Math.floor(Date.now() / 1000));
const requestId = uuidv4();
const payloadJson = JSON.stringify({
version: "v1",
requestId,
info,
cosyVersion: QODER_IDE_VERSION,
ideVersion: "",
});
const payloadB64 = Buffer.from(payloadJson, "utf8").toString("base64");
const sigPath = computeSigPath(requestUrl);
const sigInput = `${payloadB64}\n${cosyKey}\n${timestamp}\n${bodyBuf.toString("latin1")}\n${sigPath}`;
const sig = md5Hex(Buffer.from(sigInput, "latin1"));
const machineId = creds.machineId || generateMachineId();
const bodyHash = md5Hex(bodyBuf);
const bodyLength = String(bodyBuf.length);
return {
Authorization: `Bearer COSY.${payloadB64}.${sig}`,
"Cosy-Key": cosyKey,
"Cosy-User": creds.userId,
"Cosy-Date": timestamp,
"Cosy-Version": QODER_IDE_VERSION,
"Cosy-Machineid": machineId,
"Cosy-Machinetoken": machineId,
"Cosy-Machinetype": QODER_MACHINE_TYPE,
"Cosy-Machineos": QODER_MACHINE_OS,
"Cosy-Clienttype": QODER_CLIENT_TYPE,
"Cosy-Clientip": "127.0.0.1",
"Cosy-Bodyhash": bodyHash,
"Cosy-Bodylength": bodyLength,
"Cosy-Sigpath": sigPath,
"Cosy-Data-Policy": QODER_DATA_POLICY,
"Cosy-Organization-Id": "",
"Cosy-Organization-Tags": "",
"Login-Version": QODER_LOGIN_VERSION,
"X-Request-Id": uuidv4(),
};
}
// Re-export: qoder cosy helpers moved to open-sse/shared/qoder (docs 00 §1b).
export * from "../../../open-sse/shared/qoder/cosy.js";
+2 -55
View File
@@ -1,55 +1,2 @@
/**
* Qoder body encoding ported from qoder2api's QoderEncoding.java (via the
* CLIProxyAPIPlus qoder-provider branch).
*
* Algorithm:
* 1. base64-encode the plaintext bytes (standard alphabet).
* 2. Rearrange: split into thirds, reorder as [tail][mid][head].
* 3. Substitute each character via a custom alphabet mapping.
*
* The encoded body must be sent with `&Encode=1` appended to the URL so the
* server decodes in reverse. The obfuscation prevents Alibaba Cloud WAF from
* pattern-matching the plaintext request body.
*/
const QODER_STD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const QODER_CUSTOM_ALPHABET = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
const QODER_S2C = (() => {
const table = new Int16Array(128).fill(-1);
for (let i = 0; i < 64; i++) {
table[QODER_STD_ALPHABET.charCodeAt(i)] = QODER_CUSTOM_ALPHABET.charCodeAt(i);
}
table["=".charCodeAt(0)] = "$".charCodeAt(0);
return table;
})();
/**
* Encode plaintext bytes/string using Qoder's WAF-bypass scheme.
* @param {Buffer|Uint8Array|string} plaintext
* @returns {string} encoded string
*/
export function qoderEncodeBody(plaintext) {
const buf = Buffer.isBuffer(plaintext)
? plaintext
: typeof plaintext === "string"
? Buffer.from(plaintext, "utf8")
: Buffer.from(plaintext);
const std = buf.toString("base64");
const n = std.length;
const a = Math.floor(n / 3);
// [tail][mid][head]
const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
const out = Buffer.alloc(n);
for (let i = 0; i < n; i++) {
const c = rearranged.charCodeAt(i);
if (c < 128 && QODER_S2C[c] >= 0) {
out[i] = QODER_S2C[c];
} else {
out[i] = c;
}
}
return out.toString("latin1");
}
// Re-export: qoder encoding moved to open-sse/shared/qoder (docs 00 §1b).
export * from "../../../open-sse/shared/qoder/encoding.js";