mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user