feat(oauth): zed/trae/windsurf providers + harden callback proxies

- zed live model discovery; codebuddy-intl handler; remove duplicate workbuddy
- split oauth providers.js into per-provider files (facade re-export)
- fold 5 standard refresh providers into config-driven generic
- hide trae/windsurf from registry (no tool calling support)
- fix login-CSRF + SSRF on trae/windsurf/zed local callback proxies
  via loopback-origin guard + strict state validation + apiOrigins allowlist
- move zed RSA private key transit to POST body; redact proxy logs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
decolua
2026-07-25 17:25:19 +07:00
parent 783e271c16
commit 8e04fe1734
46 changed files with 4569 additions and 2203 deletions

View File

@@ -21,7 +21,6 @@ import { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
import { MimoFreeExecutor } from "./mimo-free.js";
import { CodeBuddyExecutor } from "./codebuddy-cn.js";
import { CodeBuddyIntlExecutor } from "./codebuddy-intl.js";
import { WorkBuddyExecutor } from "./workbuddy.js";
import TraeExecutor from "./trae.js";
import ZedExecutor from "./zed.js";
import WindsurfExecutor from "./windsurf.js";
@@ -56,7 +55,6 @@ const executors = {
mmf: new MimoFreeExecutor(), // Alias for mimo-free
"codebuddy-cn": new CodeBuddyExecutor(),
"codebuddy-intl": new CodeBuddyIntlExecutor(),
workbuddy: new WorkBuddyExecutor(),
trae: new TraeExecutor(),
zed: new ZedExecutor(),
windsurf: new WindsurfExecutor(),
@@ -99,7 +97,6 @@ export { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
export { MimoFreeExecutor } from "./mimo-free.js";
export { CodeBuddyExecutor } from "./codebuddy-cn.js";
export { CodeBuddyIntlExecutor } from "./codebuddy-intl.js";
export { WorkBuddyExecutor } from "./workbuddy.js";
export { default as TraeExecutor } from "./trae.js";
export { default as ZedExecutor } from "./zed.js";
export { default as WindsurfExecutor } from "./windsurf.js";

View File

@@ -1,22 +1,339 @@
import { DefaultExecutor } from "./default.js";
import { BaseExecutor } from "./base.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { PROVIDERS } from "../config/providers.js";
// Trae executor — inject x-cloudide-token (raw access token) + Authorization Bearer.
// Mirrors trae_account.rs request_trae_json header set.
export default class TraeExecutor extends DefaultExecutor {
// Trae executor — SOLO remote agent API.
//
// Flow:
// 1. POST {base}/chat_sessions → { code:0, data:{ chat_session_id, message_id } }
// 2. GET {base}/chat_sessions/{id}/events?reply_to_message_id={message_id}
// → text/event-stream. Assistant text streams in `plan_item` events under
// the `thought` field (cumulative per plan-item id). `token_usage` carries
// usage; `done` ends the turn; `error` carries upstream errors.
//
// Auth: header `Authorization: Cloud-IDE-JWT <jwt>` (RS256, ~14-day lifetime).
// Identity fields for common_params live in credentials.providerSpecificData.
const STREAM_TIMEOUT_MS = parseInt(process.env.TRAE_STREAM_TIMEOUT_MS || "300000", 10);
const TRAE_UA =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
function flattenQuery(messages) {
const parts = [];
for (const m of messages) {
let content = "";
if (typeof m.content === "string") content = m.content;
else if (Array.isArray(m.content)) {
content = m.content
.map((p) => {
if (typeof p === "string") return p;
if (p && typeof p === "object") return String(p.text ?? "");
return "";
})
.join("");
}
if (m.role === "system") parts.push(`[System]\n${content}`);
else if (m.role === "assistant") parts.push(`[Assistant]\n${content}`);
else parts.push(content);
}
// Trae expects query as a JSON-encoded string of typed content blocks.
return JSON.stringify([{ type: "text", data: { content: parts.join("\n\n") } }]);
}
export default class TraeExecutor extends BaseExecutor {
constructor() {
super("trae");
super("trae", PROVIDERS.trae);
}
base() {
return (this.config.baseUrl || "https://core-normal.trae.ai/api/remote/v1").replace(/\/$/, "");
}
buildHeaders(credentials, stream = true) {
const headers = super.buildHeaders(credentials, stream);
const token = credentials?.accessToken;
if (token) {
// Raw token (no Bearer prefix) on x-cloudide-token — matches official client.
headers["x-cloudide-token"] = token;
headers["Authorization"] = `Bearer ${token}`;
}
return headers;
const token = credentials?.accessToken || "";
const psd = credentials?.providerSpecificData || {};
return {
Authorization: `Cloud-IDE-JWT ${token}`,
"Content-Type": "application/json",
"X-Trae-Client-Type": "web",
"X-Preferenced-Language": psd.appLanguage || "en",
"x-user-region": psd.userRegion || "US",
Referer: "https://solo.trae.ai/",
"User-Agent": TRAE_UA,
Accept: stream ? "text/event-stream" : "application/json",
};
}
// TODO verify: if Chat is JSON-RPC shaped, override transformRequest here.
// SOLO session modes: "code" (model picker) vs "work" (fast auto lane).
resolveMode(model) {
const m = (model || "").trim().toLowerCase();
if (m === "work" || m === "auto-work" || m === "solo-work") {
return { mode: "work", strategy: "auto", modelName: "" };
}
const auto = !m || m === "auto";
return { mode: "code", strategy: auto ? "auto" : "manual", modelName: auto ? "" : model };
}
// common_params is a JSON-encoded string embedded inside initial_message.
commonParams(psd, mode, sessionId) {
const cp = {
language: "en-us",
app_language: psd.appLanguage || "en",
quality: "stable",
app_version: psd.appVersion || "1.0.0.1229",
web_id: psd.webId || "",
user_identity: psd.userIdentity || "Free",
is_freshman: "0",
biz_user_id: psd.bizUserId || "",
user_unique_id: psd.userUniqueId || "",
scope: psd.scope || "marscode-us",
tenant: psd.tenant || "marscode",
region: psd.region || "US-East",
aiRegion: psd.aiRegion || psd.region || "US-East",
is_privacy_mode: 0,
privacy_mode: "off",
solo_chat_mode: mode,
};
if (sessionId) cp.biz_session_id = sessionId;
return JSON.stringify(cp);
}
// POST /chat_sessions — creates a session and submits the first turn.
async createSession(headers, query, model, psd, signal) {
const { mode, strategy, modelName } = this.resolveMode(model);
const body = {
mode,
environment_id: "default",
initial_message: {
chat_session_id: "",
content: [],
query,
model_name: modelName,
agent_type: "solo_agent_remote",
model_selection_strategy: strategy,
common_params: this.commonParams(psd, mode),
},
env: "remote",
auto_create_project: false,
origin: "web",
};
const res = await proxyAwareFetch(`${this.base()}/chat_sessions`, {
method: "POST",
headers,
body: JSON.stringify(body),
signal,
}, null);
const text = await res.text();
if (!res.ok) throw new Error(`[${res.status}] ${text}`);
const json = JSON.parse(text);
if (json?.code !== 0) throw new Error(`Trae create_session: ${JSON.stringify(json)}`);
return { sessionId: json.data.chat_session_id, messageId: json.data.message_id };
}
// GET /events SSE → invoke onEvent(eventType, dataObj) per frame.
// Resolves when `done`/`error` arrives, the stream ends, or timeout fires.
async streamEvents(headers, sessionId, replyTo, onEvent, signal) {
const url = `${this.base()}/chat_sessions/${sessionId}/events?reply_to_message_id=${encodeURIComponent(replyTo)}`;
const ctrl = new AbortController();
if (signal?.aborted) ctrl.abort();
const timer = setTimeout(() => ctrl.abort(new Error("trae stream timeout")), STREAM_TIMEOUT_MS);
const onAbort = () => ctrl.abort();
if (signal) signal.addEventListener("abort", onAbort, { once: true });
try {
const res = await proxyAwareFetch(url, { method: "GET", headers, signal: ctrl.signal }, null);
if (!res.ok || !res.body) throw new Error(`[${res.status}] events stream failed`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";
let ev = null;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let nl;
while ((nl = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, nl).replace(/\r$/, "");
buf = buf.slice(nl + 1);
if (line.startsWith("event:")) ev = line.slice(6).trim();
else if (line.startsWith("data:")) {
const payload = line.slice(5).trim();
let data;
try { data = JSON.parse(payload); } catch { data = { _raw: payload }; }
if (onEvent(ev, data)) {
await reader.cancel().catch(() => {});
return;
}
} else if (line === "") ev = null;
}
}
} finally {
clearTimeout(timer);
if (signal) signal.removeEventListener("abort", onAbort);
}
}
async execute({ model, body, stream, credentials, signal }) {
const headers = this.buildHeaders(credentials, stream !== false);
const psd = credentials?.providerSpecificData || {};
const query = flattenQuery(body?.messages || []);
const responseId = `chatcmpl-trae-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
const errResponse = (status, message) => new Response(
JSON.stringify({ error: { message, type: "api_error", code: "" } }),
{ status, headers: { "Content-Type": "application/json" } }
);
let session;
try {
session = await this.createSession(headers, query, model, psd, signal);
} catch (err) {
return { response: errResponse(502, err?.message ? String(err.message) : String(err)), url: this.base(), headers, transformedBody: body };
}
// Shared per-turn state: plan_item thoughts (cumulative, longest wins).
const order = [];
const thoughts = {};
let sent = 0;
let usage = null;
let errorEvent = null;
const renderNewText = (data) => {
const pid = data.id;
if (!pid) return "";
if (!(pid in thoughts)) order.push(pid);
const t = data.thought || "";
if (t.length >= (thoughts[pid] || "").length) thoughts[pid] = t;
const full = order.map((i) => thoughts[i]).join("");
const piece = full.slice(sent);
sent = full.length;
return piece;
};
if (stream !== false) {
const enc = new TextEncoder();
const sse = new ReadableStream({
start: async (controller) => {
const emit = (obj) => controller.enqueue(enc.encode(`data: ${JSON.stringify(obj)}\n\n`));
emit({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
});
try {
await this.streamEvents(headers, session.sessionId, session.messageId, (ev, data) => {
if (ev === "error") { errorEvent = data; return true; }
if (ev === "token_usage") usage = data;
if (ev === "plan_item") {
const piece = renderNewText(data);
if (piece) {
emit({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [{ index: 0, delta: { content: piece }, finish_reason: null }],
});
}
}
return ev === "done";
}, signal);
if (errorEvent) {
emit({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [],
error: { message: `trae ${errorEvent.code || ""}: ${errorEvent.message || ""}`, type: "api_error" },
});
} else {
emit({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
});
if (usage) {
emit({
id: responseId,
object: "chat.completion.chunk",
created,
model,
choices: [],
usage: {
prompt_tokens: usage.prompt_tokens || 0,
completion_tokens: usage.completion_tokens || 0,
total_tokens: usage.total_tokens || 0,
},
});
}
}
controller.enqueue(enc.encode("data: [DONE]\n\n"));
controller.close();
} catch (err) {
controller.error(err);
}
},
});
return {
response: new Response(sse, {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
}),
url: this.base(),
headers,
transformedBody: body,
};
}
// Non-streaming: drive to completion, return chat.completion JSON.
try {
await this.streamEvents(headers, session.sessionId, session.messageId, (ev, data) => {
if (ev === "error") { errorEvent = data; return true; }
if (ev === "token_usage") usage = data;
if (ev === "plan_item") renderNewText(data);
return ev === "done";
}, signal);
} catch (err) {
return { response: errResponse(502, err?.message ? String(err.message) : String(err)), url: this.base(), headers, transformedBody: body };
}
if (errorEvent) {
return { response: errResponse(502, `trae ${errorEvent.code || ""}: ${errorEvent.message || ""}`), url: this.base(), headers, transformedBody: body };
}
const content = order.map((i) => thoughts[i]).join("");
const out = {
id: responseId,
object: "chat.completion",
created,
model,
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
};
if (usage) {
out.usage = {
prompt_tokens: usage.prompt_tokens || 0,
completion_tokens: usage.completion_tokens || 0,
total_tokens: usage.total_tokens || 0,
};
}
return {
response: new Response(JSON.stringify(out), { status: 200, headers: { "Content-Type": "application/json" } }),
url: this.base(),
headers,
transformedBody: body,
};
}
// Refresh hook placeholder — Cloud-IDE-JWT is long-lived (~14d); refresh via
// ExchangeToken (refresh→access) is wired in services/tokenRefresh/providers.js.
async refreshCredentials() {
return null;
}
}

View File

@@ -1,40 +1,586 @@
import { DefaultExecutor } from "./default.js";
import { BaseExecutor } from "./base.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { PROVIDERS } from "../config/providers.js";
import { randomUUID } from "node:crypto";
// Windsurf chat = Codeium binary protobuf gRPC-Web.
// The .proto schema for exa.server_pb.ServerService is NOT in either source
// repo, so request/response encode+decode cannot be implemented truthfully.
// Auth, headers, quota are wired; the chat payload is intentionally a hard
// failure rather than a fabricated protobuf body.
export class WindsurfExecutor extends DefaultExecutor {
// WindsurfExecutor — Codeium gRPC-web chat.
//
// Wire protocol: gRPC-web over HTTPS (Content-Type: application/grpc-web+proto).
// Service: exa.language_server_pb.LanguageServerService
// Method: GetChatMessage (unary request → streamed CompletionChunk frames)
//
// Auth: credentials.accessToken = Codeium apiKey (sk-ws-... or Firebase-derived)
// — placed in Metadata.api_key protobuf field of every request + Bearer header.
const WS_BASE_URL = "https://server.codeium.com";
const WS_SERVICE = "exa.language_server_pb.LanguageServerService";
const WS_METHOD_CHAT = "GetChatMessage";
const WS_CHAT_URL = `${WS_BASE_URL}/${WS_SERVICE}/${WS_METHOD_CHAT}`;
const WS_IDE_NAME = "windsurf";
const WS_IDE_VERSION = "3.14.0";
const WS_EXT_VERSION = "3.14.0";
const WS_LOCALE = "en-US";
// ─── Model alias map (catalog name → Windsurf wire name) ─────────────────────
const MODEL_ALIAS_MAP = {
// ── Cognition SWE ───────────────────────────────────────────────────────
"swe-1.6-fast": "swe-1-6-fast",
"swe-1.6": "swe-1-6",
"swe-1.5-fast": "swe-1-5-fast",
"swe-1.5": "swe-1-5",
// ── Claude Opus 4.7 — effort-tiered ─────────────────────────────────────
"claude-opus-4.7-max": "claude-opus-4-7-max",
"claude-opus-4.7-xhigh": "claude-opus-4-7-xhigh",
"claude-opus-4.7-high": "claude-opus-4-7-high",
"claude-opus-4.7-medium": "claude-opus-4-7-medium",
"claude-opus-4.7-low": "claude-opus-4-7-low",
"claude-opus-4.7-review": "opus-4-7-review",
// ── Claude Opus/Sonnet 4.6 ──────────────────────────────────────────────
"claude-sonnet-4.6-thinking-1m": "claude-sonnet-4-6-thinking-1m",
"claude-sonnet-4.6-1m": "claude-sonnet-4-6-1m",
"claude-sonnet-4.6-thinking": "claude-sonnet-4-6-thinking",
"claude-sonnet-4.6": "claude-sonnet-4-6",
"claude-opus-4.6-thinking": "claude-opus-4-6-thinking",
"claude-opus-4.6": "claude-opus-4-6",
// ── Claude 4.5 ──────────────────────────────────────────────────────────
"claude-opus-4.5-thinking": "MODEL_CLAUDE_4_5_OPUS_THINKING",
"claude-opus-4.5": "MODEL_CLAUDE_4_5_OPUS",
"claude-sonnet-4.5-thinking": "MODEL_PRIVATE_3",
"claude-sonnet-4.5": "MODEL_PRIVATE_2",
"claude-haiku-4.5": "MODEL_PRIVATE_11",
// ── GPT-5.5 ─────────────────────────────────────────────────────────────
"gpt-5.5-xhigh-fast": "gpt-5-5-xhigh-priority",
"gpt-5.5-high-fast": "gpt-5-5-high-priority",
"gpt-5.5-medium-fast": "gpt-5-5-medium-priority",
"gpt-5.5-low-fast": "gpt-5-5-low-priority",
"gpt-5.5-none-fast": "gpt-5-5-none-priority",
"gpt-5.5-xhigh": "gpt-5-5-xhigh",
"gpt-5.5-high": "gpt-5-5-high",
"gpt-5.5-medium": "gpt-5-5-medium",
"gpt-5.5-low": "gpt-5-5-low",
"gpt-5.5-none": "gpt-5-5-none",
"gpt-5.5-review": "gpt-5-5-review",
"gpt-5.5": "gpt-5-5-medium",
// ── GPT-5.4 ─────────────────────────────────────────────────────────────
"gpt-5.4-xhigh-fast": "gpt-5-4-xhigh-priority",
"gpt-5.4-high-fast": "gpt-5-4-high-priority",
"gpt-5.4-medium-fast": "gpt-5-4-medium-priority",
"gpt-5.4-low-fast": "gpt-5-4-low-priority",
"gpt-5.4-none-fast": "gpt-5-4-none-priority",
"gpt-5.4-xhigh": "gpt-5-4-xhigh",
"gpt-5.4-high": "gpt-5-4-high",
"gpt-5.4-medium": "gpt-5-4-medium",
"gpt-5.4-low": "gpt-5-4-low",
"gpt-5.4-none": "gpt-5-4-none",
"gpt-5.4-mini-xhigh": "gpt-5-4-mini-xhigh",
"gpt-5.4-mini-high": "gpt-5-4-mini-high",
"gpt-5.4-mini-medium": "gpt-5-4-mini-medium",
"gpt-5.4-mini-low": "gpt-5-4-mini-low",
"gpt-5.4": "gpt-5-4-medium",
// ── GPT-5.3-Codex ───────────────────────────────────────────────────────
"gpt-5.3-codex-xhigh-fast": "gpt-5-3-codex-xhigh-priority",
"gpt-5.3-codex-high-fast": "gpt-5-3-codex-high-priority",
"gpt-5.3-codex-medium-fast": "gpt-5-3-codex-medium-priority",
"gpt-5.3-codex-low-fast": "gpt-5-3-codex-low-priority",
"gpt-5.3-codex-xhigh": "gpt-5-3-codex-xhigh",
"gpt-5.3-codex-high": "gpt-5-3-codex-high",
"gpt-5.3-codex-medium": "gpt-5-3-codex-medium",
"gpt-5.3-codex-low": "gpt-5-3-codex-low",
"gpt-5.3-codex": "gpt-5-3-codex-medium",
// ── GPT-5.2 ─────────────────────────────────────────────────────────────
"gpt-5.2-xhigh": "MODEL_GPT_5_2_XHIGH",
"gpt-5.2-high": "MODEL_GPT_5_2_HIGH",
"gpt-5.2-medium": "MODEL_GPT_5_2_MEDIUM",
"gpt-5.2-low": "MODEL_GPT_5_2_LOW",
"gpt-5.2-none": "MODEL_GPT_5_2_NONE",
"gpt-5.2": "MODEL_GPT_5_2_MEDIUM",
// ── GPT-5 ───────────────────────────────────────────────────────────────
"gpt-5": "gpt-5",
// ── GPT-4.1 / 4o ────────────────────────────────────────────────────────
"gpt-4.1": "MODEL_CHAT_GPT_4_1_2025_04_14",
"gpt-4.1-mini": "gpt-4.1-mini",
"gpt-4o": "MODEL_CHAT_GPT_4O_2024_08_06",
// ── Gemini ──────────────────────────────────────────────────────────────
"gemini-3.1-pro-high": "gemini-3-1-pro-high",
"gemini-3.1-pro-low": "gemini-3-1-pro-low",
"gemini-3.1-pro": "gemini-3-1-pro-high",
"gemini-3.0-flash-high": "MODEL_GOOGLE_GEMINI_3_0_FLASH_HIGH",
"gemini-3.0-flash-medium": "MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM",
"gemini-3.0-flash-low": "MODEL_GOOGLE_GEMINI_3_0_FLASH_LOW",
"gemini-3.0-flash-minimal": "MODEL_GOOGLE_GEMINI_3_0_FLASH_MINIMAL",
"gemini-3.0-flash": "MODEL_GOOGLE_GEMINI_3_0_FLASH_HIGH",
"gemini-2.5-pro": "MODEL_GOOGLE_GEMINI_2_5_PRO",
// ── Others ──────────────────────────────────────────────────────────────
"deepseek-v4": "deepseek-v4",
"kimi-k2.6": "kimi-k2-6",
"kimi-k2.5": "kimi-k2-5",
"glm-5.1": "glm-5-1",
};
export function resolveWsModelId(model) {
return MODEL_ALIAS_MAP[model] ?? model;
}
// ─── Minimal protobuf encoder ────────────────────────────────────────────────
// Wire types: 0 = varint, 2 = length-delimited.
function encodeVarint(value) {
const bytes = [];
let v = value >>> 0;
while (v > 0x7f) {
bytes.push((v & 0x7f) | 0x80);
v >>>= 7;
}
bytes.push(v & 0x7f);
return new Uint8Array(bytes);
}
function concatBytes(arrays) {
const total = arrays.reduce((n, a) => n + a.length, 0);
const out = new Uint8Array(total);
let off = 0;
for (const a of arrays) {
out.set(a, off);
off += a.length;
}
return out;
}
const TEXT_ENC = new TextEncoder();
const TEXT_DEC = new TextDecoder();
function encodeField(fieldNum, payload) {
const tag = encodeVarint((fieldNum << 3) | 2);
const len = encodeVarint(payload.length);
return concatBytes([tag, len, payload]);
}
function encodeString(fieldNum, value) {
return encodeField(fieldNum, TEXT_ENC.encode(value));
}
function encodeMessage(fieldNum, msg) {
return encodeField(fieldNum, msg);
}
// ─── Protobuf message builders ───────────────────────────────────────────────
function buildMetadata(apiKey, sessionId) {
return concatBytes([
encodeString(1, apiKey),
encodeString(2, WS_IDE_NAME),
encodeString(3, WS_IDE_VERSION),
encodeString(4, WS_EXT_VERSION),
encodeString(5, sessionId),
encodeString(6, WS_LOCALE),
]);
}
function buildModelOrAlias(model) {
return encodeString(1, model);
}
function buildChatMessage(msg) {
const parts = [encodeString(1, msg.role), encodeString(2, msg.content)];
if (msg.toolCallId) parts.push(encodeString(3, msg.toolCallId));
return concatBytes(parts);
}
export function buildGetChatMessageRequest(apiKey, model, messages) {
const sessionId = randomUUID();
const cascadeId = randomUUID();
const parts = [
encodeMessage(1, buildMetadata(apiKey, sessionId)), // metadata
encodeString(2, cascadeId), // cascade_id
encodeMessage(3, buildModelOrAlias(model)), // model_or_alias
];
for (const msg of messages) {
parts.push(encodeMessage(4, buildChatMessage(msg))); // repeated messages
}
return concatBytes(parts);
}
// ─── gRPC-web framing ────────────────────────────────────────────────────────
export function grpcWebFrame(payload) {
const frame = new Uint8Array(5 + payload.length);
frame[0] = 0x00; // no compression
const view = new DataView(frame.buffer);
view.setUint32(1, payload.length, false); // big-endian length
frame.set(payload, 5);
return frame;
}
// ─── Protobuf response decoder ───────────────────────────────────────────────
// CompletionChunk (oneof):
// field 1 → ContentChunk { field 1: string text }
// field 2 → ToolCallChunk (skipped)
// field 3 → DoneChunk { field 1: UsageStats{ field1: prompt, field2: completion } }
// field 4 → ErrorChunk { field 1: string message }
function readVarint(buf, offset) {
let result = 0;
let shift = 0;
while (offset < buf.length) {
const b = buf[offset++];
result |= (b & 0x7f) << shift;
if ((b & 0x80) === 0) break;
shift += 7;
}
return [result >>> 0, offset];
}
function decodeStringField(buf, targetField) {
let offset = 0;
while (offset < buf.length) {
let tag;
[tag, offset] = readVarint(buf, offset);
const fieldNum = tag >>> 3;
const wireType = tag & 0x07;
if (wireType === 2) {
let len;
[len, offset] = readVarint(buf, offset);
const payload = buf.slice(offset, offset + len);
offset += len;
if (fieldNum === targetField) return TEXT_DEC.decode(payload);
} else if (wireType === 0) {
let v;
[v, offset] = readVarint(buf, offset);
} else if (wireType === 1) {
offset += 8;
} else if (wireType === 5) {
offset += 4;
} else {
break;
}
}
return null;
}
function decodeDoneChunk(buf) {
// DoneChunk: field 1 = UsageStats (nested)
// UsageStats: field 1 = prompt_tokens (varint), field 2 = completion_tokens (varint)
let offset = 0;
let usageBytes = null;
while (offset < buf.length) {
let tag;
[tag, offset] = readVarint(buf, offset);
const fieldNum = tag >>> 3;
const wireType = tag & 0x07;
if (wireType === 2) {
let len;
[len, offset] = readVarint(buf, offset);
if (fieldNum === 1) usageBytes = buf.slice(offset, offset + len);
offset += len;
} else if (wireType === 0) {
let v;
[v, offset] = readVarint(buf, offset);
} else {
break;
}
}
if (!usageBytes) return [0, 0];
let promptTokens = 0;
let completionTokens = 0;
offset = 0;
while (offset < usageBytes.length) {
let tag;
[tag, offset] = readVarint(usageBytes, offset);
const fieldNum = tag >>> 3;
const wireType = tag & 0x07;
if (wireType === 0) {
let v;
[v, offset] = readVarint(usageBytes, offset);
if (fieldNum === 1) promptTokens = v;
else if (fieldNum === 2) completionTokens = v;
} else if (wireType === 2) {
let len;
[len, offset] = readVarint(usageBytes, offset);
offset += len;
} else {
break;
}
}
return [promptTokens, completionTokens];
}
export function decodeCompletionChunk(buf) {
let offset = 0;
while (offset < buf.length) {
let tag;
[tag, offset] = readVarint(buf, offset);
const fieldNum = tag >>> 3;
const wireType = tag & 0x07;
if (wireType === 2) {
let len;
[len, offset] = readVarint(buf, offset);
const payload = buf.slice(offset, offset + len);
offset += len;
if (fieldNum === 1) {
const text = decodeStringField(payload, 1);
if (text !== null) return { kind: "content", text };
} else if (fieldNum === 3) {
const usage = decodeDoneChunk(payload);
return { kind: "done", promptTokens: usage[0], completionTokens: usage[1] };
} else if (fieldNum === 4) {
const msg = decodeStringField(payload, 1);
return { kind: "error", message: msg ?? "unknown windsurf error" };
}
// field 2 = ToolCallChunk — not yet handled; skip
} else if (wireType === 0) {
let v;
[v, offset] = readVarint(buf, offset);
} else if (wireType === 1) {
offset += 8;
} else if (wireType === 5) {
offset += 4;
} else {
break;
}
}
return { kind: "unknown" };
}
// ─── OpenAI messages → Windsurf wire ─────────────────────────────────────────
function openAIMessagesToWs(messages) {
const out = [];
for (const m of messages) {
const role = String(m.role || "user");
let content = "";
if (typeof m.content === "string") {
content = m.content;
} else if (Array.isArray(m.content)) {
for (const part of m.content) {
if (part && typeof part === "object" && part.type === "text") {
content += String(part.text || "");
}
}
}
out.push({ role, content, toolCallId: m.tool_call_id });
}
return out;
}
// ─── WindsurfExecutor ────────────────────────────────────────────────────────
export class WindsurfExecutor extends BaseExecutor {
constructor() {
super("windsurf");
super("windsurf", PROVIDERS.windsurf || { id: "windsurf", baseUrl: WS_CHAT_URL });
}
buildUrl() {
return WS_CHAT_URL;
}
buildHeaders(credentials, stream = true) {
const headers = {
"Content-Type": "application/proto",
"Connect-Protocol-Version": "1",
ideName: "Windsurf",
extensionName: "codeium.windsurf",
...(this.config.headers || {}),
const token = credentials?.accessToken || credentials?.apiKey || "";
return {
"Content-Type": "application/grpc-web+proto",
Accept: "application/grpc-web+proto",
// Codeium apiKey also goes in Metadata.api_key (protobuf field) — see request body.
...(token ? { Authorization: `Bearer ${token}` } : {}),
"User-Agent": `windsurf/${WS_IDE_VERSION}`,
"X-Grpc-Web": "1",
};
// apiKey from RegisterUser (sk-ws-..., Firebase-derived, or Devin ide_token).
const token = credentials?.apiKey || credentials?.accessToken;
if (token) headers["Authorization"] = `Bearer ${token}`;
return headers;
}
// TODO(proto): implement once Codeium server_pb .proto is recovered.
// - encode request: chat history + model + system → protobuf bytes
// - decode response: stream protobuf frames → OpenAI-shaped chunks
async execute() {
throw new Error(
"Windsurf chat (Codeium protobuf) not yet implemented — needs .proto schema. Auth/quota wired."
);
// Request body is built manually in execute() — requires model + messages.
transformRequest() {
return null;
}
async execute({ model, body, stream, credentials, signal, log, upstreamExtraHeaders, proxyOptions = null }) {
const apiKey = credentials?.accessToken || credentials?.apiKey || "";
const wsModel = resolveWsModelId(model);
const b = body ?? {};
const rawMessages = Array.isArray(b.messages) ? b.messages : [];
let wsMessages = openAIMessagesToWs(rawMessages);
if (wsMessages.length === 0) {
wsMessages.push({ role: "user", content: "" });
}
const protoPayload = buildGetChatMessageRequest(apiKey, wsModel, wsMessages);
const framedPayload = grpcWebFrame(protoPayload);
const url = this.buildUrl();
const headers = this.buildHeaders(credentials);
if (upstreamExtraHeaders) Object.assign(headers, upstreamExtraHeaders);
log?.debug?.("WS", `Windsurf → ${wsModel} (${wsMessages.length} messages)`);
const upstream = await proxyAwareFetch(url, {
method: "POST",
headers,
body: framedPayload,
signal,
}, proxyOptions);
if (!upstream.ok && upstream.status !== 200) {
return { response: upstream, url, headers, transformedBody: protoPayload };
}
const sseResponse = this.transformToSSE(upstream, model);
return { response: sseResponse, url, headers, transformedBody: protoPayload };
}
// Convert a gRPC-web binary response into an OpenAI-compatible SSE stream.
transformToSSE(upstream, model) {
const responseId = `chatcmpl-ws-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
const executor = this;
const sseStream = new ReadableStream({
async start(controller) {
const enc = new TextEncoder();
let roleEmitted = false;
let totalText = "";
let promptTokens = 0;
let completionTokens = 0;
let hadError = null;
const emit = (data) => controller.enqueue(enc.encode(data));
try {
let pending = new Uint8Array(0);
const reader = upstream.body?.getReader();
const handleFrame = (flag, payload) => {
if (flag === 0x80) {
// Trailer frame — contains grpc-status, grpc-message
const trailer = TEXT_DEC.decode(payload);
const statusMatch = /grpc-status:\s*(\d+)/i.exec(trailer);
if (statusMatch && statusMatch[1] !== "0") {
const msgMatch = /grpc-message:\s*(.+)/i.exec(trailer);
hadError = msgMatch
? decodeURIComponent(msgMatch[1].trim())
: `gRPC status ${statusMatch[1]}`;
}
return;
}
if (flag !== 0x00) return; // skip unknown flags
const chunk = executor.constructor.decodeCompletionChunk
? executor.constructor.decodeCompletionChunk(payload)
: decodeCompletionChunk(payload);
if (chunk.kind === "content" && chunk.text) {
totalText += chunk.text;
if (!roleEmitted) {
emit(`data: ${JSON.stringify({
id: responseId, object: "chat.completion.chunk", created, model,
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }],
})}\n\n`);
roleEmitted = true;
}
emit(`data: ${JSON.stringify({
id: responseId, object: "chat.completion.chunk", created, model,
choices: [{ index: 0, delta: { content: chunk.text }, finish_reason: null }],
})}\n\n`);
} else if (chunk.kind === "done") {
promptTokens = chunk.promptTokens;
completionTokens = chunk.completionTokens;
} else if (chunk.kind === "error") {
hadError = chunk.message;
}
};
const drainFrames = () => {
let offset = 0;
while (offset + 5 <= pending.length) {
const flag = pending[offset];
const len =
(pending[offset + 1] << 24) |
(pending[offset + 2] << 16) |
(pending[offset + 3] << 8) |
pending[offset + 4];
if (len < 0 || offset + 5 + len > pending.length) break;
handleFrame(flag, pending.slice(offset + 5, offset + 5 + len));
offset += 5 + len;
}
if (offset > 0) pending = pending.slice(offset);
};
if (reader) {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!value) continue;
pending = pending.length === 0 ? value : concatBytes([pending, value]);
drainFrames();
}
} finally {
reader.releaseLock();
}
}
drainFrames();
if (hadError) {
emit(`data: ${JSON.stringify({
error: { message: hadError, type: "windsurf_error", code: "upstream_error" },
})}\n\n`);
emit("data: [DONE]\n\n");
controller.close();
return;
}
// Unary fallback: nothing streamed but text decoded → emit as one chunk.
if (!roleEmitted && totalText) {
emit(`data: ${JSON.stringify({
id: responseId, object: "chat.completion.chunk", created, model,
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }],
})}\n\n`);
emit(`data: ${JSON.stringify({
id: responseId, object: "chat.completion.chunk", created, model,
choices: [{ index: 0, delta: { content: totalText }, finish_reason: null }],
})}\n\n`);
}
const finishPayload = {
id: responseId, object: "chat.completion.chunk", created, model,
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
};
if (promptTokens > 0 || completionTokens > 0) {
finishPayload.usage = {
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
total_tokens: promptTokens + completionTokens,
};
}
emit(`data: ${JSON.stringify(finishPayload)}\n\n`);
emit("data: [DONE]\n\n");
} catch (err) {
const msg = err?.message ? String(err.message) : String(err);
emit(`data: ${JSON.stringify({
error: { message: `Windsurf stream error: ${msg}`, type: "windsurf_error" },
})}\n\n`);
emit("data: [DONE]\n\n");
}
controller.close();
},
});
return new Response(sseStream, {
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
// apiKey is long-lived (Firebase-derived or Devin ide_token); refresh handled out-of-band.
async refreshCredentials() {
// Windsurf apiKey is long-lived (like cursor); refresh handled out-of-band.
return null;
}
}

View File

@@ -1,31 +0,0 @@
import { DefaultExecutor } from "./default.js";
/**
* WorkBuddyExecutor — talks to https://www.codebuddy.cn/v2/chat/completions
*
* WorkBuddy is a B2B/enterprise skin of CodeBuddy CN (same codebuddy.cn
* OpenAI-compatible gateway). Behavior mirrors CodeBuddyExecutor:
* gateway rejects non-stream requests, and reasoning must be surfaced via
* OpenAI-style reasoning_effort + reasoning_summary:"auto" (vendor-native
* thinking shapes are not honored by the unified gateway).
*/
export class WorkBuddyExecutor extends DefaultExecutor {
constructor() {
super("workbuddy");
}
transformRequest(model, body, stream, credentials) {
const transformed = super.transformRequest(model, body, stream, credentials);
transformed.stream = true;
const eff = transformed.reasoning_effort;
if (eff === "none" || eff === "off") {
delete transformed.reasoning_effort;
} else if (eff) {
transformed.reasoning_summary = "auto";
}
return transformed;
}
}
export default WorkBuddyExecutor;

View File

@@ -100,10 +100,11 @@ import p97 from "./xiaomi-tokenplan.js";
import p98 from "./youcom.js";
import p99 from "./alims-intl.js";
import p100 from "./codebuddy-intl.js";
import p101 from "./workbuddy.js";
import p102 from "./trae.js";
// Temporarily hidden — no tool calling support (trae SOLO agent / windsurf gRPC skip ToolCallChunk).
// Re-enable by uncommenting both the import and the array entry below.
// import p102 from "./trae.js";
import p103 from "./zed.js";
import p104 from "./windsurf.js";
// import p104 from "./windsurf.js";
export default [
p0,
@@ -207,8 +208,7 @@ export default [
p98,
p99,
p100,
p101,
p102,
// p102, // trae — hidden, no tool calling
p103,
p104,
// p104, // windsurf — hidden, no tool calling
];

View File

@@ -1,7 +1,8 @@
// Trae (ByteDance marscode) provider registry entry.
// Auth + exchange URLs verified from cockpit-tools/src-tauri/src/modules/trae_oauth.rs.
// Region origins verified from trae_account.rs lines 63-66.
// Chat endpoint path /cloudide/api/v3/trae/Chat is GUESSED (TODO verify upstream).
// Chat = SOLO remote agent API:
// POST {base}/chat_sessions → {data:{chat_session_id, message_id}}
// GET {base}/chat_sessions/{id}/events?reply_to_message_id=... → SSE
// Auth: Authorization: Cloud-IDE-JWT <jwt>
export default {
id: "trae",
alias: "tr",
@@ -20,21 +21,19 @@ export default {
notice: { signupUrl: "https://www.trae.ai" },
},
transport: {
// IDE flow (cockpit-tools verified): x-cloudide-token auth, OpenAI-shaped SSE.
baseUrl: "https://api.marscode.com/cloudide/api/v3/trae/Chat",
// SOLO remote agent base — verified working chat endpoint.
baseUrl: "https://core-normal.trae.ai/api/remote/v1",
format: "openai",
headers: {
"x-app-version": "3.5.54",
"x-app-type": "stable",
"x-env": "production",
"client_id": "ono9krqynydwx5",
"User-Agent": "Trae/1.0.0 antigravity-cockpit-tools",
"X-Trae-Client-Type": "web",
"X-Preferenced-Language": "en",
"Referer": "https://solo.trae.ai/",
},
// Auth: x-cloudide-token + Authorization: Bearer — injected by executor buildHeaders.
// Auth: Cloud-IDE-JWT scheme on Authorization — injected by executor buildHeaders.
auth: {
combined: true,
header: "x-cloudide-token",
scheme: "raw",
header: "Authorization",
scheme: "Cloud-IDE-JWT",
},
usage: {
url: "https://api.marscode.com/cloudide/api/v3/trae/GetUserInfo",
@@ -61,7 +60,7 @@ export default {
// Trae refresh uses custom JSON body, not OAuth form — handled by refresh.js, not config-driven.
refresh: { encoding: "json" },
},
// Model catalog sourced from OmniRoute (IDE flow, core-normal.trae.ai).
// Model catalog (IDE flow, core-normal.trae.ai).
models: [
{ id: "auto", name: "Auto (Server Picks)" },
{ id: "work", name: "Work (Fast)" },

View File

@@ -1,6 +1,7 @@
// Windsurf provider registry — Firebase+Codeium+Devin auth chain.
// Chat transport is Codeium protobuf gRPC-Web: endpoint + schema are GUESS,
// the cockpit-tools source only documents auth/quota (SeatManagement) paths.
// Chat = Codeium gRPC-web protobuf:
// POST {base} Content-Type: application/grpc-web+proto
// Service: exa.language_server_pb.LanguageServerService / GetChatMessage
export default {
id: "windsurf",
alias: "ws",
@@ -17,19 +18,16 @@ export default {
hasOAuth: true,
authModes: ["oauth", "apikey"],
// TODO(chat): Codeium ServerService protobuf schema unknown — endpoint is a guess.
transport: {
// GUESS: Codeium chat lives under /exa.server_pb.ServerService/GetChatMessage.
baseUrl: "https://server.codeium.com/exa.server_pb.ServerService/GetChatMessage",
format: "windsurf",
baseUrl: "https://server.codeium.com/exa.language_server_pb.LanguageServerService/GetChatMessage",
format: "openai",
headers: {
"Content-Type": "application/proto",
"Connect-Protocol-Version": "1",
"ideName": "Windsurf",
"extensionName": "codeium.windsurf",
"Content-Type": "application/grpc-web+proto",
"Accept": "application/grpc-web+proto",
"X-Grpc-Web": "1",
},
// Bearer of apiKey (sk-ws-... / Firebase-derived / Devin session) — Connect-Protocol scheme unverified.
auth: { combined: true, header: "Authorization" },
// apiKey (sk-ws-... or Firebase-derived) as Bearer + in protobuf Metadata.api_key.
auth: { combined: true, header: "Authorization", scheme: "Bearer" },
},
// Auth chain (4 terminal paths, all yield apiKey):
@@ -52,9 +50,8 @@ export default {
},
// Catalog verified against model_configs_v2.bin from Devin CLI (2026.5.x).
// Source: OmniRoute registry (guanxiaol/WindsurfPoolAPI). Dot-notation ids; the
// executor MODEL_ALIAS_MAP would map these to Windsurf modelUid once proto chat
// is implemented. contextLength dropped — 9router schema uses id+name only.
// Dot-notation ids; the executor MODEL_ALIAS_MAP maps these to Windsurf modelUid.
// contextLength dropped — 9router schema uses id+name only.
models: [
// Cognition / SWE
{ id: "swe-1.6-fast", name: "SWE-1.6 Fast" },

View File

@@ -1,73 +0,0 @@
export default {
id: "workbuddy",
// Short model prefix (wb/glm-5.2). WorkBuddy is a B2B/enterprise skin of
// CodeBuddy CN (same codebuddy.cn backend), so models mirror codebuddy-cn.
alias: "wb",
uiAlias: "wb",
hidden: false,
priority: 90,
display: {
name: "WorkBuddy",
icon: "smart_toy",
color: "#006EFF",
website: "https://www.codebuddy.cn",
notice: {
signupUrl: "https://www.codebuddy.cn",
},
},
category: "oauth",
authModes: ["oauth", "apikey"],
hasOAuth: true,
transport: {
// Same OpenAI-compatible gateway as codebuddy-cn; platform=workbuddy is
// distinguished at the OAuth layer, not the chat endpoint.
baseUrl: "https://www.codebuddy.cn/v2/chat/completions",
forceStream: true,
thinkingFormat: "openai",
headers: {
"User-Agent": "CLI/2.108.1 CodeBuddy/2.108.1",
"X-Product": "SaaS",
"X-IDE-Type": "CLI",
"X-IDE-Name": "CLI",
"x-requested-with": "XMLHttpRequest",
"x-codebuddy-request": "1",
},
auth: {
combined: true,
header: "Authorization",
scheme: "bearer",
},
},
models: [
{ id: "glm-5.2", name: "GLM-5.2" },
{ id: "glm-5.1", name: "GLM-5.1" },
{ id: "glm-5.0", name: "GLM-5.0" },
{ id: "glm-5.0-turbo", name: "GLM-5.0-Turbo" },
{ id: "glm-5v-turbo", name: "GLM-5v-Turbo" },
{ id: "glm-4.7", name: "GLM-4.7" },
{ id: "minimax-m3", name: "MiniMax-M3" },
{ id: "minimax-m2.7", name: "MiniMax-M2.7" },
{ id: "kimi-k2.7", name: "Kimi-K2.7-Code" },
{ id: "kimi-k2.6", name: "Kimi-K2.6" },
{ id: "kimi-k2.5", name: "Kimi-K2.5" },
{ id: "hy3-preview", name: "Hy3 Preview" },
{ id: "deepseek-v4-pro", name: "DeepSeek-V4-Pro" },
{ id: "deepseek-v4-flash", name: "DeepSeek-V4-Flash" },
{ id: "deepseek-v3-2-volc", name: "DeepSeek-V3.2" },
],
oauth: {
// Same codebuddy.cn host as codebuddy-cn; only platform param differs
// (workbuddy vs CLI). Prefix /v2/plugin matches cockpit-tools Rust.
baseUrl: "https://www.codebuddy.cn",
stateUrl: "https://www.codebuddy.cn/v2/plugin/auth/state",
tokenUrl: "https://www.codebuddy.cn/v2/plugin/auth/token",
refreshUrl: "https://www.codebuddy.cn/v2/plugin/auth/token/refresh",
userAgent: "CLI/2.63.2 CodeBuddy/2.63.2",
platform: "workbuddy",
pollInterval: 5000,
},
features: {
usage: true,
usageApikey: true,
},
};

View File

@@ -14,7 +14,6 @@ import {
refreshCopilotToken,
refreshCodebuddyToken,
refreshCodebuddyIntlToken,
refreshWorkbuddyToken,
refreshTraeToken,
refreshZedToken,
refreshWindsurfToken,
@@ -35,7 +34,6 @@ export {
refreshCopilotToken,
refreshCodebuddyToken,
refreshCodebuddyIntlToken,
refreshWorkbuddyToken,
refreshTraeToken,
refreshZedToken,
refreshWindsurfToken,
@@ -149,7 +147,6 @@ const REFRESH_HANDLERS = {
gcli: (c, log) => refreshXaiToken(c.refreshToken, log),
"codebuddy-cn": (c, log) => refreshCodebuddyToken(c.refreshToken, log),
"codebuddy-intl": (c, log) => refreshCodebuddyIntlToken(c.refreshToken, log),
workbuddy: (c, log) => refreshWorkbuddyToken(c.refreshToken, log),
trae: (c, log) => refreshTraeToken(c.refreshToken, c, log),
zed: () => refreshZedToken(),
windsurf: (c, log) => refreshWindsurfToken(c, log),

View File

@@ -31,10 +31,68 @@ export async function refreshXaiToken(refreshToken, log) {
}, log);
}
// Per-provider refresh variants for the generic path. Keys not listed fall back
// to the default form-encoded OAuth2 refresh with client_id + client_secret.
const REFRESH_PROFILES = {
claude: {
bodyFormat: "json",
includeClientSecret: false,
url: () => OAUTH_ENDPOINTS.anthropic.token,
dedupKey: "claude",
},
qwen: {
url: () => OAUTH_ENDPOINTS.qwen.token,
dedupKey: "qwen",
parse: (tokens) => tokens.resource_url ? { providerSpecificData: { resourceUrl: tokens.resource_url } } : {},
},
iflow: {
url: () => OAUTH_ENDPOINTS.iflow.token,
dedupKey: "iflow",
extraHeaders: (creds, cfg) => ({
Authorization: `Basic ${btoa(`${cfg.clientId}:${cfg.clientSecret}`)}`,
}),
},
github: {
url: () => OAUTH_ENDPOINTS.github.token,
dedupKey: "github",
includeClientSecret: (cfg) => !!cfg?.clientSecret,
},
kimi: {
dedupKey: "kimi",
extraHeaders: (creds) => buildKimiHeaders(creds?.providerSpecificData?.deviceId),
},
};
function resolveRefreshUrl(provider, config, profile) {
if (profile?.url) {
try { return profile.url(); } catch { /* fall through */ }
}
return config?.refreshUrl || PROVIDER_OAUTH[provider]?.tokenUrl || null;
}
function buildRefreshBody(profile, config, refreshToken) {
const fmt = profile?.bodyFormat === "json" ? "json" : "form";
const includeSecret = profile?.includeClientSecret === undefined
? true
: typeof profile.includeClientSecret === "function"
? profile.includeClientSecret(config)
: profile.includeClientSecret;
const payload = {
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: config.clientId,
};
if (includeSecret && config.clientSecret) payload.client_secret = config.clientSecret;
if (fmt === "json") return { format: "json", body: JSON.stringify(payload) };
return { format: "form", body: new URLSearchParams(payload) };
}
export async function refreshAccessToken(provider, refreshToken, credentials, log) {
const config = PROVIDERS[provider];
const profile = REFRESH_PROFILES[provider] || {};
const url = resolveRefreshUrl(provider, config, profile);
if (!config || !config.refreshUrl) {
if (!config || !url) {
log?.warn?.("TOKEN_REFRESH", `No refresh URL configured for provider: ${provider}`);
return null;
}
@@ -44,21 +102,17 @@ export async function refreshAccessToken(provider, refreshToken, credentials, lo
return null;
}
return dedupRefresh(provider, refreshToken, async () => {
const dedupKey = profile.dedupKey || provider;
return dedupRefresh(dedupKey, refreshToken, async () => {
try {
const response = await fetch(config.refreshUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: config.clientId,
client_secret: config.clientSecret,
}),
});
const { format: bodyFormat, body } = buildRefreshBody(profile, config, refreshToken);
const headers = {
"Content-Type": bodyFormat === "json" ? "application/json" : "application/x-www-form-urlencoded",
Accept: "application/json",
...(profile.extraHeaders ? (profile.extraHeaders(credentials, config) || {}) : {}),
};
const response = await fetch(url, { method: "POST", headers, body });
if (!response.ok) {
const errorText = await response.text();
@@ -81,6 +135,7 @@ export async function refreshAccessToken(provider, refreshToken, credentials, lo
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
...(profile.parse ? (profile.parse(tokens) || {}) : {}),
};
} catch (error) {
log?.error?.("TOKEN_REFRESH", `Error refreshing token for ${provider}`, {
@@ -92,82 +147,14 @@ export async function refreshAccessToken(provider, refreshToken, credentials, lo
}
// CLIProxyAPI DeviceFlowClient.RefreshToken: form body (no client_secret) + X-Msh-* headers
// Delegate to refreshAccessToken("kimi", ...) — profile carries the X-Msh headers.
export async function refreshKimiToken(refreshToken, credentials, log) {
const config = PROVIDERS.kimi;
if (!config?.refreshUrl || !config?.clientId) {
log?.warn?.("TOKEN_REFRESH", "No Kimi refresh URL/clientId configured");
return null;
}
if (!refreshToken) return null;
return dedupRefresh("kimi", refreshToken, async () => {
try {
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
...buildKimiHeaders(credentials?.providerSpecificData?.deviceId),
};
const response = await fetch(config.refreshUrl, {
method: "POST",
headers,
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: config.clientId,
}),
});
if (!response.ok) {
const errorText = await response.text();
log?.error?.("TOKEN_REFRESH", `Failed to refresh token for kimi`, {
status: response.status,
error: errorText,
});
return null;
}
const tokens = await response.json();
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
};
} catch (error) {
log?.error?.("TOKEN_REFRESH", `Error refreshing token for kimi`, { error: error.message });
return null;
}
}, log);
return refreshAccessToken("kimi", refreshToken, credentials, log);
}
// Claude OAuth: JSON body, client_id only. Delegate to refreshAccessToken("claude", ...).
export async function refreshClaudeOAuthToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("claude", refreshToken, async () => {
try {
const response = await fetch(OAUTH_ENDPOINTS.anthropic.token, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: PROVIDERS.claude.clientId,
}),
});
if (!response.ok) {
const errorText = await response.text();
log?.error?.("TOKEN_REFRESH", "Failed to refresh Claude OAuth token", { status: response.status, error: errorText });
return null;
}
const tokens = await response.json();
log?.info?.("TOKEN_REFRESH", "Successfully refreshed Claude OAuth token", { hasNewAccessToken: !!tokens.access_token, expiresIn: tokens.expires_in });
return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || refreshToken, expiresIn: tokens.expires_in };
} catch (error) {
log?.error?.("TOKEN_REFRESH", `Network error refreshing Claude token: ${error.message}`);
return null;
}
}, log);
return refreshAccessToken("claude", refreshToken, {}, log);
}
export async function refreshGoogleToken(refreshToken, clientId, clientSecret, log) {
@@ -204,58 +191,9 @@ export async function refreshGoogleToken(refreshToken, clientId, clientSecret, l
}, log);
}
// Qwen: form body + clientId, surfaces resource_url. Delegate to refreshAccessToken("qwen", ...).
export async function refreshQwenToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("qwen", refreshToken, async () => {
const endpoint = OAUTH_ENDPOINTS.qwen.token;
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: PROVIDERS.qwen.clientId,
}),
});
if (response.status === 200) {
const tokens = await response.json();
log?.info?.("TOKEN_REFRESH", "Successfully refreshed Qwen token", {
hasNewAccessToken: !!tokens.access_token,
hasNewRefreshToken: !!tokens.refresh_token,
expiresIn: tokens.expires_in,
});
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
providerSpecificData: tokens.resource_url
? { resourceUrl: tokens.resource_url }
: undefined,
};
} else {
const errorText = await response.text().catch(() => "");
log?.warn?.("TOKEN_REFRESH", `Error with Qwen endpoint`, {
status: response.status,
error: errorText,
});
}
} catch (error) {
log?.warn?.("TOKEN_REFRESH", `Network error trying Qwen endpoint`, {
error: error.message,
});
}
log?.error?.("TOKEN_REFRESH", "Failed to refresh Qwen token");
return null;
}, log);
return refreshAccessToken("qwen", refreshToken, {}, log);
}
export function classifyOAuthRefreshError(errorText = "", status = 0) {
@@ -480,95 +418,14 @@ export async function refreshKiroToken(refreshToken, providerSpecificData, log,
}, log);
}
// iFlow: Basic Auth + client_id+client_secret in body. Delegate to refreshAccessToken("iflow", ...).
export async function refreshIflowToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("iflow", refreshToken, async () => {
const basicAuth = btoa(`${PROVIDERS.iflow.clientId}:${PROVIDERS.iflow.clientSecret}`);
const response = await fetch(OAUTH_ENDPOINTS.iflow.token, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
Authorization: `Basic ${basicAuth}`,
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: PROVIDERS.iflow.clientId,
client_secret: PROVIDERS.iflow.clientSecret,
}),
});
if (!response.ok) {
const errorText = await response.text();
log?.error?.("TOKEN_REFRESH", "Failed to refresh iFlow token", {
status: response.status,
error: errorText,
});
return null;
}
const tokens = await response.json();
log?.info?.("TOKEN_REFRESH", "Successfully refreshed iFlow token", {
hasNewAccessToken: !!tokens.access_token,
hasNewRefreshToken: !!tokens.refresh_token,
expiresIn: tokens.expires_in,
});
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
};
}, log);
return refreshAccessToken("iflow", refreshToken, {}, log);
}
// GitHub: optional client_secret. Delegate to refreshAccessToken("github", ...).
export async function refreshGitHubToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("github", refreshToken, async () => {
const params = {
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: PROVIDERS.github.clientId,
};
if (PROVIDERS.github.clientSecret) {
params.client_secret = PROVIDERS.github.clientSecret;
}
const response = await fetch(OAUTH_ENDPOINTS.github.token, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams(params),
});
if (!response.ok) {
const errorText = await response.text();
log?.error?.("TOKEN_REFRESH", "Failed to refresh GitHub token", {
status: response.status,
error: errorText,
});
return null;
}
const tokens = await response.json();
log?.info?.("TOKEN_REFRESH", "Successfully refreshed GitHub token", {
hasNewAccessToken: !!tokens.access_token,
hasNewRefreshToken: !!tokens.refresh_token,
expiresIn: tokens.expires_in,
});
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
};
}, log);
return refreshAccessToken("github", refreshToken, {}, log);
}
export async function refreshCopilotToken(githubAccessToken, log) {
@@ -720,60 +577,8 @@ export async function refreshCodebuddyIntlToken(refreshToken, log) {
}, log);
}
export async function refreshWorkbuddyToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("workbuddy", refreshToken, async () => {
const oauth = PROVIDER_OAUTH["workbuddy"] || {};
const response = await fetch(oauth.refreshUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": oauth.userAgent,
"X-Requested-With": "XMLHttpRequest",
"X-Domain": "www.codebuddy.cn",
"X-Refresh-Token": refreshToken,
"X-Auth-Refresh-Source": "plugin",
"X-Product": "SaaS",
},
body: "{}",
});
if (!response.ok) {
const errorText = await response.text();
log?.error?.("TOKEN_REFRESH", "Failed to refresh WorkBuddy token", {
status: response.status,
error: errorText,
});
return null;
}
const data = await response.json();
if (data.code !== 0 || !data.data?.accessToken) {
log?.error?.("TOKEN_REFRESH", "WorkBuddy token refresh returned no token", {
code: data.code,
msg: data.msg,
});
return null;
}
log?.info?.("TOKEN_REFRESH", "Successfully refreshed WorkBuddy token", {
hasNewAccessToken: !!data.data.accessToken,
hasNewRefreshToken: !!data.data.refreshToken,
expiresIn: data.data.expiresIn,
});
return {
accessToken: data.data.accessToken,
refreshToken: data.data.refreshToken || refreshToken,
expiresIn: data.data.expiresIn,
};
}, log);
}
// Trae refresh — POST ExchangeToken with JSON body {ClientID, RefreshToken, ClientSecret, UserID}.
// Response: {Result: {AccessToken, RefreshToken, TokenType, ExpiresAt}}.
// Source: cockpit-tools/src-tauri/src/modules/trae_oauth.rs (TRAE_EXCHANGE_TOKEN_PATH).
export async function refreshTraeToken(refreshToken, credentials, log) {
if (!refreshToken) return null;
const oauth = PROVIDER_OAUTH.trae || {};

View File

@@ -198,6 +198,21 @@ export function parseGrokCliBilling(billing, user = null) {
};
}
// SuperGrok weekly shared-pool usage (subscription tier). creditUsagePercent is
// the single total used %; productUsage is a breakdown legend, NOT independent
// quotas — never split it into separate bars.
const usedPct = unwrapVal(
config.creditUsagePercent ?? config.credit_usage_percent ?? root.creditUsagePercent,
NaN,
);
if (Number.isFinite(usedPct) && usedPct >= 0) {
quotas["Weekly SuperGrok"] = makeQuota({
used: Math.max(0, Math.min(100, usedPct)),
total: 100,
resetAt: periodEnd,
});
}
// Opportunistic richer credit envelopes (future / other account types)
const creditBags = [
root.credits,

View File

@@ -1,10 +1,10 @@
import { NextResponse } from "next/server";
import {
getProvider,
generateAuthData,
exchangeTokens,
requestDeviceCode,
pollForToken
import {
getProvider,
generateAuthData,
exchangeTokens,
requestDeviceCode,
pollForToken
} from "@/lib/oauth/providers";
import { createProviderConnection } from "@/models";
import {
@@ -18,7 +18,24 @@ import {
registerXaiSession,
getXaiSessionStatus,
clearXaiSession,
startTraeProxy,
stopTraeProxy,
registerTraeSession,
getTraeSessionStatus,
clearTraeSession,
startWindsurfProxy,
stopWindsurfProxy,
registerWindsurfSession,
getWindsurfSessionStatus,
clearWindsurfSession,
startZedProxy,
stopZedProxy,
registerZedSession,
getZedSessionStatus,
clearZedSession,
} from "@/lib/oauth/utils/server";
import { detectIdeInstalled } from "@/lib/oauth/utils/ideDetect";
import { ZED_HOSTED_CONFIG } from "@/lib/oauth/constants/oauth";
async function completeXaiManualCode(code, state) {
const session = state ? getXaiSessionStatus(state) : null;
@@ -77,13 +94,34 @@ export async function GET(request, { params }) {
const reservedParams = new Set(["redirect_uri"]);
const meta = {};
searchParams.forEach((value, key) => { if (!reservedParams.has(key)) meta[key] = value; });
// Zed: derive native_app_port from the local callback URL so the RSA keypair
// is bound to the port the proxy is actually listening on.
if (provider === "zed") {
try { const p = new URL(redirectUri).port; if (p) meta.nativeAppPort = p; } catch { /* ignore */ }
}
const authData = await generateAuthData(provider, redirectUri, Object.keys(meta).length ? meta : undefined);
return NextResponse.json(authData);
}
if (action === "start-proxy") {
// Trae/Windsurf/Zed use a dynamic-port local callback server (singleton session,
// state is registered separately via /register-session after /authorize).
if (provider === "trae") {
const result = await startTraeProxy();
return NextResponse.json(result);
}
if (provider === "windsurf") {
const result = await startWindsurfProxy();
return NextResponse.json(result);
}
if (provider === "zed") {
// Prefer ZED_HOSTED_CONFIG.defaultNativeAppPort (58443) so the browser redirect
// matches what Zed expects; falls back to a random port if it's busy.
const result = await startZedProxy(searchParams.get("native_app_port") || ZED_HOSTED_CONFIG.defaultNativeAppPort);
return NextResponse.json(result);
}
if (!["codex", "xai"].includes(provider)) {
return NextResponse.json({ error: "Proxy only supported for codex/xai" }, { status: 400 });
return NextResponse.json({ error: "Proxy only supported for codex/xai/trae/windsurf/zed" }, { status: 400 });
}
const appPort = searchParams.get("app_port");
if (!appPort) {
@@ -105,18 +143,24 @@ export async function GET(request, { params }) {
}
if (action === "poll-status") {
if (!["codex", "xai"].includes(provider)) {
return NextResponse.json({ error: "Poll only supported for codex/xai" }, { status: 400 });
}
const state = searchParams.get("state");
if (!state) {
return NextResponse.json({ error: "Missing state" }, { status: 400 });
}
const session = provider === "xai" ? getXaiSessionStatus(state) : getCodexSessionStatus(state);
let session;
if (provider === "trae") session = getTraeSessionStatus(state);
else if (provider === "windsurf") session = getWindsurfSessionStatus(state);
else if (provider === "zed") session = getZedSessionStatus(state);
else if (provider === "xai") session = getXaiSessionStatus(state);
else if (provider === "codex") session = getCodexSessionStatus(state);
else return NextResponse.json({ error: "Poll only supported for codex/xai/trae/windsurf/zed" }, { status: 400 });
if (!session) return NextResponse.json({ status: "unknown" });
if (session.status === "done" || session.status === "error") {
const payload = { ...session };
if (provider === "xai") clearXaiSession(state);
if (provider === "trae") clearTraeSession(state);
else if (provider === "windsurf") clearWindsurfSession(state);
else if (provider === "zed") clearZedSession(state);
else if (provider === "xai") clearXaiSession(state);
else clearCodexSession(state);
return NextResponse.json(payload);
}
@@ -124,14 +168,24 @@ export async function GET(request, { params }) {
}
if (action === "stop-proxy") {
if (!["codex", "xai"].includes(provider)) {
return NextResponse.json({ error: "Proxy only supported for codex/xai" }, { status: 400 });
}
if (provider === "xai") stopXaiProxy();
else stopCodexProxy();
if (provider === "trae") stopTraeProxy();
else if (provider === "windsurf") stopWindsurfProxy();
else if (provider === "zed") stopZedProxy();
else if (provider === "xai") stopXaiProxy();
else if (provider === "codex") stopCodexProxy();
else return NextResponse.json({ error: "Proxy only supported for codex/xai/trae/windsurf/zed" }, { status: 400 });
return NextResponse.json({ success: true });
}
if (action === "ide-status") {
// Detect whether the IDE is installed locally (used by import-token UX).
if (provider !== "trae" && provider !== "windsurf") {
return NextResponse.json({ error: "ide-status only supported for trae/windsurf" }, { status: 400 });
}
const status = await detectIdeInstalled(provider);
return NextResponse.json(status);
}
if (action === "device-code") {
const providerData = getProvider(provider);
if (providerData.flowType !== "device_code") {
@@ -158,6 +212,7 @@ export async function GET(request, { params }) {
"kimi-coding",
"kilocode",
"codebuddy-cn",
"codebuddy-intl",
"qoder",
"grok-cli",
];
@@ -196,9 +251,54 @@ export async function POST(request, { params }) {
return NextResponse.json({ error: "Invalid or empty request body" }, { status: 400 });
}
if (action === "register-session") {
// Register proxy session out of URL query (state) + body (codeVerifier).
// Zed's codeVerifier encodes the RSA private key — must stay out of URL/logs.
const state = searchParams.get("state") || body?.state;
if (!state) return NextResponse.json({ error: "Missing state" }, { status: 400 });
let ok = false;
if (provider === "trae") ok = registerTraeSession({ state });
else if (provider === "windsurf") ok = registerWindsurfSession({ state });
else if (provider === "zed") ok = registerZedSession({ state, codeVerifier: body?.codeVerifier });
else return NextResponse.json({ error: "register-session only supported for trae/windsurf/zed" }, { status: 400 });
return NextResponse.json({ success: ok });
}
if (action === "exchange") {
const { code, redirectUri, codeVerifier, state, meta } = body;
// Trae/Windsurf: code is either a raw callback URL or a pasted token.
// exchangeTokens() handles both paths; no PKCE, skip codex JWT extraction.
if (provider === "trae" || provider === "windsurf") {
const token = typeof code === "string" ? code.trim() : "";
if (!token) {
return NextResponse.json({ error: "Missing token or callback URL" }, { status: 400 });
}
try {
const tokenData = await exchangeTokens(provider, token, null, null, state);
const connection = await createProviderConnection({
provider,
authType: provider === "windsurf" ? "api_key" : "oauth",
...tokenData,
expiresAt: tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
: null,
testStatus: "active",
});
return NextResponse.json({
success: true,
connection: {
id: connection.id,
provider: connection.provider,
email: connection.email,
displayName: connection.displayName,
}
});
} catch (err) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
// Detect if "code" is actually a raw JWT access token (starts with eyJ)
if (code && code.startsWith("eyJ") && code.includes(".")) {
const { extractCodexAccountInfo } = await import("@/lib/oauth/providers");
@@ -280,7 +380,7 @@ export async function POST(request, { params }) {
}
// Providers that don't use PKCE for device code
const noPkceProviders = ["github", "kimi", "kimi-coding", "kilocode", "codebuddy-cn"];
const noPkceProviders = ["github", "kimi", "kimi-coding", "kilocode", "codebuddy-cn", "codebuddy-intl"];
let result;
if (noPkceProviders.includes(provider)) {
// kimi needs extraData._kimiDeviceId for stable X-Msh-Device-Id (CLIProxyAPI parity)

View File

@@ -14,6 +14,7 @@ import { resolveCopilotModels } from "open-sse/services/copilotModels.js";
import { resolveClinepassModels } from "open-sse/services/clinepassModels.js";
import { resolveGrokCliModels } from "open-sse/services/grokCliModels.js";
import { resolveCursorModels } from "open-sse/services/cursorModels.js";
import { resolveZedModels } from "open-sse/shared/zedAuth.js";
import { updateProviderCredentials } from "@/sse/services/tokenRefresh";
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
import { capabilitiesFromServiceKind, getCapabilitiesForModel } from "open-sse/providers/capabilities.js";
@@ -104,7 +105,23 @@ const LIVE_MODEL_RESOLVERS = {
providerSpecificData: conn.providerSpecificData || {},
}, { log: console });
return result?.models?.length ? { models: result.models } : null;
}
},
zed: async (conn) => {
const result = await resolveZedModels({
accessToken: conn.accessToken,
providerSpecificData: conn.providerSpecificData || {},
});
if (!result?.models?.length) return null;
return {
models: result.models
.filter((m) => !m.isDisabled)
.map((m) => ({
id: m.id,
name: m.name,
capabilities: m.supportsTools ? { tools: true } : undefined,
})),
};
},
};
const parseOpenAIStyleModels = (data) => {

View File

@@ -117,6 +117,9 @@ export const GITLAB_CONFIG = { ...PROVIDER_OAUTH["gitlab"] };
// CodeBuddy (Tencent) OAuth Configuration (Browser OAuth Polling Flow)
export const CODEBUDDY_CONFIG = { ...PROVIDER_OAUTH["codebuddy-cn"] };
// CodeBuddy International — same shape as CN, .ai domain (mirror of codebuddy-cn).
export const CODEBUDDY_INTL_CONFIG = { ...PROVIDER_OAUTH["codebuddy-intl"] };
// Kimchi OAuth Configuration (Browser token callback flow)
export const KIMCHI_CONFIG = { ...PROVIDER_OAUTH["kimchi"] };
@@ -124,6 +127,77 @@ export const KIMCHI_CONFIG = { ...PROVIDER_OAUTH["kimchi"] };
// Endpoint: cli-chat-proxy.grok.com — same client_id as xai, different flow + scopes
export const GROK_CLI_CONFIG = { ...PROVIDER_OAUTH["grok-cli"] };
// Trae (ByteDance marscode) OAuth — authorization_code flow with local callback.
// 1) POST GetLoginGuidance {loginTraceID} → {Result.LoginHost}
// 2) Browser opens ${loginHost}/authorization?client_id=...&login_trace_id=...&auth_callback_url=${cb}
// 3) Redirect → ${cb}?refreshToken=...&loginHost=...&isRedirect=true
// 4) POST ExchangeToken {ClientID, RefreshToken, ClientSecret:"-"} → {Result.AccessToken, ExpiresAt}
// 5) POST GetUserInfo (x-cloudide-token) → email/name
export const TRAE_CONFIG = {
clientId: "ono9krqynydwx5",
clientSecret: "-",
loginGuidanceUrls: [
"https://api.marscode.com/cloudide/api/v3/trae/GetLoginGuidance",
"https://api.trae.ai/cloudide/api/v3/trae/GetLoginGuidance",
"https://www.trae.ai/cloudide/api/v3/trae/GetLoginGuidance",
],
apiOrigins: [
"https://api.marscode.com",
"https://api.trae.ai",
"https://www.trae.ai",
"https://www.marscode.com",
],
exchangeTokenPath: "/cloudide/api/v3/trae/oauth/ExchangeToken",
getUserInfoPath: "/cloudide/api/v3/trae/GetUserInfo",
authorizationPath: "/authorization",
callbackPath: "/callback",
minAppVersion: "3.5.54",
defaultAppVersion: "3.5.54",
defaultAppType: "stable",
defaultPluginVersion: "local",
// service machine id is derived at runtime; device_id "0" is the stable default
defaultDeviceId: "0",
userAgent: "Trae/1.0.0 antigravity-cockpit-tools",
webUrl: "https://www.trae.ai",
authScheme: "Cloud-IDE-JWT",
tokenLifetimeDays: 14,
oauthTimeoutMs: 600_000,
};
// Windsurf / Devin CLI OAuth — authorization_code (implicit) flow with local callback.
// 1) Browser opens windsurf.com/windsurf/signin?response_type=token&client_id=...&redirect_uri=${cb}
// 2) Redirect → ${cb}?access_token=${firebaseJWT}&state=...
// 3) POST RegisterUser {firebase_id_token} → {apiKey, apiServerUrl, name}
// 4) POST GetOneTimeAuthToken → GetCurrentUser (best-effort email/plan)
export const WINDSURF_CONFIG = {
clientId: "3GUryQ7ldAeKEuD2obYnppsnmj58eP5u",
authBaseUrl: "https://www.windsurf.com",
signInPath: "/windsurf/signin",
registerApiBaseUrl: "https://register.windsurf.com",
registerPath: "/exa.seat_management_pb.SeatManagementService/RegisterUser",
oneTimeAuthPath: "/exa.seat_management_pb.SeatManagementService/GetOneTimeAuthToken",
currentUserPath: "/exa.seat_management_pb.SeatManagementService/GetCurrentUser",
planStatusPath: "/exa.seat_management_pb.SeatManagementService/GetPlanStatus",
userStatusPath: "/exa.seat_management_pb.SeatManagementService/GetUserStatus",
defaultApiServerUrl: "https://server.codeium.com",
firebaseApiKey: "AIzaSyDsOl-1XpT5err0Tcn0TFFod1H8gVGIycY",
callbackPath: "/windsurf-auth-callback",
userAgent: "antigravity-cockpit-tools",
oauthTimeoutMs: 600_000,
};
// Zed hosted LLM aggregator — RSA keypair native-app auth (NOT OAuth).
// Client generates ephemeral RSA-2048 keypair; user signs in at zed.dev/native_app_signin;
// Zed redirects to local callback with access_token RSA-encrypted against our public key.
// See open-sse/shared/zedAuth.js for the keypair/decrypt helpers.
export const ZED_HOSTED_CONFIG = {
webBaseUrl: "https://zed.dev",
cloudBaseUrl: "https://cloud.zed.dev",
llmBaseUrl: "https://cloud.zed.dev",
defaultNativeAppPort: 58443,
oauthTimeoutMs: 600_000,
};
// OAuth timeout (5 minutes)
export const OAUTH_TIMEOUT = 300000;
@@ -147,6 +221,10 @@ export const PROVIDERS = {
CLINEPASS: "clinepass",
GITLAB: "gitlab",
CODEBUDDY: "codebuddy-cn",
CODEBUDDY_INTL: "codebuddy-intl",
KIMCHI: "kimchi",
GROK_CLI: "grok-cli",
TRAE: "trae",
WINDSURF: "windsurf",
ZED: "zed",
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
// Shared helpers used across provider entry files (currently trae + windsurf).
export function extractJsonPath(root, paths) {
for (const path of paths) {
let cur = root;
for (const key of path) {
if (cur == null || typeof cur !== "object") { cur = undefined; break; }
cur = cur[key];
}
if (typeof cur === "string" && cur.trim()) return cur.trim();
if (typeof cur === "number") return String(cur);
}
return null;
}

View File

@@ -0,0 +1,122 @@
import { ANTIGRAVITY_CONFIG, getOAuthClientMetadata } from "../constants/oauth.js";
const antigravity = {
config: ANTIGRAVITY_CONFIG,
flowType: "authorization_code",
buildAuthUrl: (config, redirectUri, state) => {
const params = new URLSearchParams({
client_id: config.clientId,
response_type: "code",
redirect_uri: redirectUri,
scope: config.scopes.join(" "),
state: state,
access_type: "offline",
prompt: "consent",
});
return `${config.authorizeUrl}?${params.toString()}`;
},
exchangeToken: async (config, code, redirectUri) => {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: config.clientId,
client_secret: config.clientSecret,
code: code,
redirect_uri: redirectUri,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
return await response.json();
},
postExchange: async (tokens) => {
// Numeric enums matching Antigravity binary ClientMetadata
const loadHeaders = {
"Authorization": `Bearer ${tokens.access_token}`,
"Content-Type": "application/json",
"User-Agent": ANTIGRAVITY_CONFIG.loadCodeAssistUserAgent,
"X-Goog-Api-Client": ANTIGRAVITY_CONFIG.loadCodeAssistApiClient,
"Client-Metadata": ANTIGRAVITY_CONFIG.loadCodeAssistClientMetadata,
"x-request-source": "local",
};
const metadata = getOAuthClientMetadata();
// Fetch user info
const userInfoRes = await fetch(`${ANTIGRAVITY_CONFIG.userInfoUrl}?alt=json`, {
headers: {
Authorization: `Bearer ${tokens.access_token}`,
"x-request-source": "local",
},
});
const userInfo = userInfoRes.ok ? await userInfoRes.json() : {};
// Load Code Assist to get project ID and tier
let projectId = "";
let tierId = "legacy-tier";
try {
const loadRes = await fetch(ANTIGRAVITY_CONFIG.loadCodeAssistEndpoint, {
method: "POST",
headers: loadHeaders,
body: JSON.stringify({ metadata }),
});
if (loadRes.ok) {
const data = await loadRes.json();
projectId = data.cloudaicompanionProject?.id || data.cloudaicompanionProject || "";
if (Array.isArray(data.allowedTiers)) {
for (const tier of data.allowedTiers) {
if (tier.isDefault && tier.id) {
tierId = tier.id.trim();
break;
}
}
}
}
} catch (e) {
console.log("Failed to load code assist:", e);
}
// Fire-and-forget onboarding — does not block DB save
if (projectId) {
const doOnboard = async () => {
for (let i = 0; i < 10; i++) {
try {
const onboardRes = await fetch(ANTIGRAVITY_CONFIG.onboardUserEndpoint, {
method: "POST",
headers: loadHeaders,
body: JSON.stringify({ tierId, metadata }),
});
if (onboardRes.ok) {
const result = await onboardRes.json();
if (result.done === true) break;
}
} catch (e) {
break;
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
};
doOnboard().catch(() => {});
}
return { userInfo, projectId };
},
mapTokens: (tokens, extra) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
scope: tokens.scope,
email: extra?.userInfo?.email,
projectId: extra?.projectId,
}),
};
export default antigravity;

View File

@@ -0,0 +1,60 @@
import { CLAUDE_CONFIG } from "../constants/oauth.js";
const claude = {
config: CLAUDE_CONFIG,
flowType: "authorization_code_pkce",
buildAuthUrl: (config, redirectUri, state, codeChallenge) => {
const params = new URLSearchParams({
code: "true",
client_id: config.clientId,
response_type: "code",
redirect_uri: redirectUri,
scope: config.scopes.join(" "),
code_challenge: codeChallenge,
code_challenge_method: config.codeChallengeMethod,
state: state,
});
return `${config.authorizeUrl}?${params.toString()}`;
},
exchangeToken: async (config, code, redirectUri, codeVerifier, state) => {
// Parse code - may contain state after #
let authCode = code;
let codeState = "";
if (authCode.includes("#")) {
const parts = authCode.split("#");
authCode = parts[0];
codeState = parts[1] || "";
}
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
code: authCode,
state: codeState || state,
grant_type: "authorization_code",
client_id: config.clientId,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
return await response.json();
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
scope: tokens.scope,
}),
};
export default claude;

View File

@@ -0,0 +1,62 @@
import { CLINE_CONFIG } from "../constants/oauth.js";
const cline = {
config: CLINE_CONFIG,
flowType: "authorization_code",
buildAuthUrl: (config, redirectUri) => {
const params = new URLSearchParams({
client_type: "extension",
callback_url: redirectUri,
redirect_uri: redirectUri,
});
return `${config.authorizeUrl}?${params.toString()}`;
},
exchangeToken: async (config, code, redirectUri) => {
try {
// Cline encodes token data as base64 in the code param
let base64 = code;
const padding = 4 - (base64.length % 4);
if (padding !== 4) base64 += "=".repeat(padding);
const decoded = Buffer.from(base64, "base64").toString("utf-8");
const lastBrace = decoded.lastIndexOf("}");
if (lastBrace === -1) throw new Error("No JSON found in decoded code");
const tokenData = JSON.parse(decoded.substring(0, lastBrace + 1));
return {
access_token: tokenData.accessToken,
refresh_token: tokenData.refreshToken,
email: tokenData.email,
firstName: tokenData.firstName,
lastName: tokenData.lastName,
expires_at: tokenData.expiresAt,
};
} catch (e) {
const response = await fetch(config.tokenExchangeUrl, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ grant_type: "authorization_code", code, client_type: "extension", redirect_uri: redirectUri }),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Cline token exchange failed: ${error}`);
}
const data = await response.json();
return {
access_token: data.data?.accessToken || data.accessToken,
refresh_token: data.data?.refreshToken || data.refreshToken,
email: data.data?.userInfo?.email || "",
expires_at: data.data?.expiresAt || data.expiresAt,
};
}
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_at
? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)
: 3600,
email: tokens.email,
providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName },
}),
};
export default cline;

View File

@@ -0,0 +1,62 @@
import { CLINEPASS_CONFIG } from "../constants/oauth.js";
const clinepass = {
config: CLINEPASS_CONFIG,
flowType: "authorization_code",
buildAuthUrl: (config, redirectUri) => {
const params = new URLSearchParams({
client_type: "extension",
callback_url: redirectUri,
redirect_uri: redirectUri,
});
return `${config.authorizeUrl}?${params.toString()}`;
},
exchangeToken: async (config, code, redirectUri) => {
try {
// Cline encodes token data as base64 in the code param
let base64 = code;
const padding = 4 - (base64.length % 4);
if (padding !== 4) base64 += "=".repeat(padding);
const decoded = Buffer.from(base64, "base64").toString("utf-8");
const lastBrace = decoded.lastIndexOf("}");
if (lastBrace === -1) throw new Error("No JSON found in decoded code");
const tokenData = JSON.parse(decoded.substring(0, lastBrace + 1));
return {
access_token: tokenData.accessToken,
refresh_token: tokenData.refreshToken,
email: tokenData.email,
firstName: tokenData.firstName,
lastName: tokenData.lastName,
expires_at: tokenData.expiresAt,
};
} catch (e) {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ grant_type: "authorization_code", code, client_type: "extension", redirect_uri: redirectUri }),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`ClinePass token exchange failed: ${error}`);
}
const data = await response.json();
return {
access_token: data.data?.accessToken || data.accessToken,
refresh_token: data.data?.refreshToken || data.refreshToken,
email: data.data?.userInfo?.email || "",
expires_at: data.data?.expiresAt || data.expiresAt,
};
}
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_at
? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)
: 3600,
email: tokens.email,
providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName },
}),
};
export default clinepass;

View File

@@ -0,0 +1,80 @@
import { CODEBUDDY_CONFIG } from "../constants/oauth.js";
// CodeBuddy (Tencent) - Browser OAuth Polling Flow
// 1. POST stateUrl → get { state, authUrl }
// 2. Open authUrl in browser
// 3. Poll tokenUrl with state until success (code 0) or timeout
const codebuddyCn = {
config: CODEBUDDY_CONFIG,
flowType: "device_code",
requestDeviceCode: async (config) => {
const response = await fetch(`${config.stateUrl}?platform=${config.platform}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": config.userAgent,
"X-Requested-With": "XMLHttpRequest",
"X-Domain": "copilot.tencent.com",
"X-No-Authorization": "true",
"X-No-User-Id": "true",
"X-Product": "SaaS",
},
body: "{}",
});
if (!response.ok) throw new Error(`CodeBuddy state request failed: ${await response.text()}`);
const data = await response.json();
if (data.code !== 0 || !data.data?.state || !data.data?.authUrl) {
throw new Error(`CodeBuddy state error: ${data.msg || "missing state/authUrl"}`);
}
return {
device_code: data.data.state,
verification_uri: data.data.authUrl,
user_code: "",
interval: config.pollInterval / 1000,
_isCodeBuddy: true,
};
},
pollToken: async (config, deviceCode) => {
// CodeBuddy polls the token endpoint via GET with the state as a query
// param (not POST/body) — matches the official CLI's /v2/plugin/auth/token?state=...
const response = await fetch(`${config.tokenUrl}?state=${encodeURIComponent(deviceCode)}`, {
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": config.userAgent,
"X-Requested-With": "XMLHttpRequest",
"X-Domain": "copilot.tencent.com",
"X-No-Authorization": "true",
"X-No-User-Id": "true",
"X-No-Enterprise-Id": "true",
"X-No-Department-Info": "true",
"X-Product": "SaaS",
},
});
if (!response.ok) return { ok: false, data: { error: "request_failed" } };
const data = await response.json();
// code 11217 = pending (RetryFetchToken), code 0 = success
if (data.code === 0 && data.data?.accessToken) {
return {
ok: true,
data: {
access_token: data.data.accessToken,
refresh_token: data.data.refreshToken || "",
token_type: data.data.tokenType || "Bearer",
expires_in: data.data.expiresIn,
},
};
}
if (data.code === 11217) return { ok: true, data: { error: "authorization_pending" } };
return { ok: false, data: { error: data.msg || "unknown_error" } };
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in || 86400,
providerSpecificData: {},
}),
};
export default codebuddyCn;

View File

@@ -0,0 +1,74 @@
import { CODEBUDDY_INTL_CONFIG } from "../constants/oauth.js";
// CodeBuddy International — mirrors codebuddy-cn flow against the .ai domain.
const codebuddyIntl = {
config: CODEBUDDY_INTL_CONFIG,
flowType: "device_code",
requestDeviceCode: async (config) => {
const response = await fetch(`${config.stateUrl}?platform=${config.platform}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": config.userAgent,
"X-Requested-With": "XMLHttpRequest",
"X-Domain": "www.codebuddy.ai",
"X-No-Authorization": "true",
"X-No-User-Id": "true",
"X-Product": "SaaS",
},
body: "{}",
});
if (!response.ok) throw new Error(`CodeBuddy Intl state request failed: ${await response.text()}`);
const data = await response.json();
if (data.code !== 0 || !data.data?.state || !data.data?.authUrl) {
throw new Error(`CodeBuddy Intl state error: ${data.msg || "missing state/authUrl"}`);
}
return {
device_code: data.data.state,
verification_uri: data.data.authUrl,
user_code: "",
interval: config.pollInterval / 1000,
_isCodeBuddy: true,
};
},
pollToken: async (config, deviceCode) => {
const response = await fetch(`${config.tokenUrl}?state=${encodeURIComponent(deviceCode)}`, {
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": config.userAgent,
"X-Requested-With": "XMLHttpRequest",
"X-Domain": "www.codebuddy.ai",
"X-No-Authorization": "true",
"X-No-User-Id": "true",
"X-No-Enterprise-Id": "true",
"X-No-Department-Info": "true",
"X-Product": "SaaS",
},
});
if (!response.ok) return { ok: false, data: { error: "request_failed" } };
const data = await response.json();
if (data.code === 0 && data.data?.accessToken) {
return {
ok: true,
data: {
access_token: data.data.accessToken,
refresh_token: data.data.refreshToken || "",
token_type: data.data.tokenType || "Bearer",
expires_in: data.data.expiresIn,
},
};
}
if (data.code === 11217) return { ok: true, data: { error: "authorization_pending" } };
return { ok: false, data: { error: data.msg || "unknown_error" } };
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in || 86400,
providerSpecificData: {},
}),
};
export default codebuddyIntl;

View File

@@ -0,0 +1,69 @@
import { CODEX_CONFIG } from "../constants/oauth.js";
import { extractCodexAccountInfo, extractEmailFromAccessToken } from "../providerHelpers.js";
const codex = {
config: CODEX_CONFIG,
flowType: "authorization_code_pkce",
fixedPort: CODEX_CONFIG.fixedPort,
callbackPath: CODEX_CONFIG.callbackPath,
buildAuthUrl: (config, redirectUri, state, codeChallenge) => {
const params = {
response_type: "code",
client_id: config.clientId,
redirect_uri: redirectUri,
scope: config.scope,
code_challenge: codeChallenge,
code_challenge_method: config.codeChallengeMethod,
...config.extraParams,
state: state,
};
const queryString = Object.entries(params)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join("&");
return `${config.authorizeUrl}?${queryString}`;
},
exchangeToken: async (config, code, redirectUri, codeVerifier) => {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: config.clientId,
code: code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
return await response.json();
},
mapTokens: (tokens) => {
const info = extractCodexAccountInfo(tokens.id_token);
const mapped = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
idToken: tokens.id_token,
expiresIn: tokens.expires_in,
lastRefreshAt: new Date().toISOString(),
};
const email = info.email || extractEmailFromAccessToken(tokens.access_token);
if (email) mapped.email = email;
if (info.chatgptAccountId || info.chatgptPlanType) {
mapped.providerSpecificData = {
chatgptAccountId: info.chatgptAccountId,
chatgptPlanType: info.chatgptPlanType,
};
}
return mapped;
},
};
export default codex;

View File

@@ -0,0 +1,19 @@
import { CURSOR_CONFIG } from "../constants/oauth.js";
const cursor = {
config: CURSOR_CONFIG,
flowType: "import_token",
// Cursor uses import token flow - tokens are extracted from local SQLite database
// No OAuth flow needed, handled by /api/oauth/cursor/import route
mapTokens: (tokens) => ({
accessToken: tokens.accessToken,
refreshToken: null, // Cursor doesn't have public refresh endpoint
expiresIn: tokens.expiresIn || 86400,
providerSpecificData: {
machineId: tokens.machineId,
authMethod: "imported",
},
}),
};
export default cursor;

View File

@@ -0,0 +1,85 @@
import { GEMINI_CONFIG, getOAuthClientMetadata } from "../constants/oauth.js";
const geminiCli = {
config: GEMINI_CONFIG,
flowType: "authorization_code",
buildAuthUrl: (config, redirectUri, state) => {
const params = new URLSearchParams({
client_id: config.clientId,
response_type: "code",
redirect_uri: redirectUri,
scope: config.scopes.join(" "),
state: state,
access_type: "offline",
prompt: "consent",
});
return `${config.authorizeUrl}?${params.toString()}`;
},
exchangeToken: async (config, code, redirectUri) => {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: config.clientId,
client_secret: config.clientSecret,
code: code,
redirect_uri: redirectUri,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
return await response.json();
},
postExchange: async (tokens) => {
// Fetch user info
const userInfoRes = await fetch(`${GEMINI_CONFIG.userInfoUrl}?alt=json`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const userInfo = userInfoRes.ok ? await userInfoRes.json() : {};
// Fetch project ID
let projectId = "";
try {
const projectRes = await fetch(
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
{
method: "POST",
headers: {
Authorization: `Bearer ${tokens.access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
metadata: getOAuthClientMetadata(),
mode: 1,
}),
}
);
if (projectRes.ok) {
const data = await projectRes.json();
projectId = data.cloudaicompanionProject?.id || data.cloudaicompanionProject || "";
}
} catch (e) {
console.log("Failed to fetch project ID:", e);
}
return { userInfo, projectId };
},
mapTokens: (tokens, extra) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
scope: tokens.scope,
email: extra?.userInfo?.email,
projectId: extra?.projectId,
}),
};
export default geminiCli;

View File

@@ -0,0 +1,98 @@
import { GITHUB_CONFIG } from "../constants/oauth.js";
const github = {
config: GITHUB_CONFIG,
flowType: "device_code",
requestDeviceCode: async (config) => {
const response = await fetch(config.deviceCodeUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
client_id: config.clientId,
scope: config.scopes,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Device code request failed: ${error}`);
}
return await response.json();
},
pollToken: async (config, deviceCode) => {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
client_id: config.clientId,
device_code: deviceCode,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
});
// Handle response properly - if not ok, try to get error as text first
let data;
try {
data = await response.json();
} catch (e) {
// If response is not JSON, get as text
const text = await response.text();
data = { error: "invalid_response", error_description: text };
}
return {
ok: response.ok,
data: data,
};
},
postExchange: async (tokens) => {
// Get Copilot token using GitHub access token
const copilotRes = await fetch(GITHUB_CONFIG.copilotTokenUrl, {
headers: {
Authorization: `Bearer ${tokens.access_token}`,
Accept: "application/json",
"X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion,
"User-Agent": GITHUB_CONFIG.userAgent,
},
});
const copilotToken = copilotRes.ok ? await copilotRes.json() : {};
// Get user info from GitHub
const userRes = await fetch(GITHUB_CONFIG.userInfoUrl, {
headers: {
Authorization: `Bearer ${tokens.access_token}`,
Accept: "application/json",
"X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion,
"User-Agent": GITHUB_CONFIG.userAgent,
},
});
const userInfo = userRes.ok ? await userRes.json() : {};
return { copilotToken, userInfo };
},
mapTokens: (tokens, extra) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
name: extra?.userInfo?.login || extra?.userInfo?.name,
displayName: extra?.userInfo?.name || extra?.userInfo?.login,
email: extra?.userInfo?.email || null,
providerSpecificData: {
copilotToken: extra?.copilotToken?.token,
copilotTokenExpiresAt: extra?.copilotToken?.expires_at,
githubUserId: extra?.userInfo?.id,
githubLogin: extra?.userInfo?.login,
githubName: extra?.userInfo?.name,
githubEmail: extra?.userInfo?.email,
},
}),
};
export default github;

View File

@@ -0,0 +1,64 @@
import { GITLAB_CONFIG } from "../constants/oauth.js";
// GitLab Duo - Authorization Code Flow with PKCE
// Supports two login modes via loginMode metadata: "oauth" (default) or "pat"
const gitlab = {
config: GITLAB_CONFIG,
flowType: "authorization_code_pkce",
buildAuthUrl: (config, redirectUri, state, codeChallenge, meta = {}) => {
const baseUrl = meta.baseUrl || config.defaultBaseUrl;
const clientId = meta.clientId || "";
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
response_type: "code",
state,
scope: config.scope,
code_challenge: codeChallenge,
code_challenge_method: config.codeChallengeMethod,
});
return `${baseUrl}${config.authorizeUrlPath}?${params.toString()}`;
},
exchangeToken: async (config, code, redirectUri, codeVerifier, state, meta = {}) => {
const baseUrl = meta.baseUrl || config.defaultBaseUrl;
const clientId = meta.clientId || "";
const clientSecret = meta.clientSecret || "";
const body = new URLSearchParams({
client_id: clientId,
grant_type: "authorization_code",
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
});
if (clientSecret) body.set("client_secret", clientSecret);
const response = await fetch(`${baseUrl}${config.tokenUrlPath}`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
body: body.toString(),
});
if (!response.ok) throw new Error(`GitLab token exchange failed: ${await response.text()}`);
const tokens = await response.json();
// Fetch user info
const userRes = await fetch(`${baseUrl}${config.userInfoUrlPath}`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const user = userRes.ok ? await userRes.json() : {};
return { ...tokens, _user: user, _baseUrl: baseUrl, _clientId: clientId };
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
scope: tokens.scope,
providerSpecificData: {
username: tokens._user?.username || "",
email: tokens._user?.email || tokens._user?.public_email || "",
name: tokens._user?.name || "",
baseUrl: tokens._baseUrl,
clientId: tokens._clientId,
authKind: "oauth",
},
}),
};
export default gitlab;

View File

@@ -0,0 +1,130 @@
import { GROK_CLI_CONFIG } from "../constants/oauth.js";
import { decodeXaiIdTokenEmail, extractEmailFromAccessToken } from "../providerHelpers.js";
// Grok CLI / Grok Build — device code flow to auth.x.ai, inference on cli-chat-proxy.grok.com
const grokCli = {
config: GROK_CLI_CONFIG,
flowType: "device_code",
requestDeviceCode: async (config) => {
const body = new URLSearchParams({
client_id: config.clientId,
scope: config.scope,
});
// Official CLI sends referrer=grok-build
if (config.referrer) body.set("referrer", config.referrer);
const response = await fetch(config.deviceCodeUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
"User-Agent": "grok-pager/0.2.93 grok-shell/0.2.93 (linux; x86_64)",
},
body,
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Grok CLI device code request failed: ${error}`);
}
return await response.json();
},
pollToken: async (config, deviceCode) => {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
"User-Agent": "grok-pager/0.2.93 grok-shell/0.2.93 (linux; x86_64)",
},
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: deviceCode,
client_id: config.clientId,
}),
});
let data;
try {
data = await response.json();
} catch {
const text = await response.text();
data = { error: "invalid_response", error_description: text };
}
// Device flow: 400 + authorization_pending is expected while user authorizes
const pending =
data?.error === "authorization_pending" ||
data?.error === "slow_down";
return {
ok: response.ok || pending,
data,
};
},
postExchange: async (tokens) => {
// Best-effort user profile from cli-chat-proxy (non-fatal)
try {
const res = await fetch("https://cli-chat-proxy.grok.com/v1/user", {
headers: {
Authorization: `Bearer ${tokens.access_token}`,
Accept: "application/json",
"User-Agent": "grok-pager/0.2.93 grok-shell/0.2.93 (linux; x86_64)",
"x-xai-token-auth": "xai-grok-cli",
"x-grok-client-version": "0.2.93",
},
});
if (res.ok) return { user: await res.json() };
} catch {
/* ignore */
}
return { user: null };
},
mapTokens: (tokens, extra) => {
const email =
decodeXaiIdTokenEmail(tokens.id_token) ||
extractEmailFromAccessToken(tokens.access_token) ||
extra?.user?.email ||
null;
const userId =
extra?.user?.userId ||
extra?.user?.principalId ||
null;
const displayName = [extra?.user?.firstName, extra?.user?.lastName]
.filter(Boolean)
.join(" ")
.trim() || null;
const expiresAt = tokens.expires_in
? new Date(Date.now() + tokens.expires_in * 1000).toISOString()
: null;
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || null,
expiresIn: tokens.expires_in,
// Surface an absolute expiry so the proactive refresh path
// (shouldRefreshCredentials / checkAndRefreshToken) can refresh the
// xAI token before it silently expires ~40-45 min after login.
// Without this, only the reactive 401 path in chatCore would refresh,
// causing intermittent "token expired" failures for Grok CLI.
expiresAt,
scope: tokens.scope,
// Top-level for dashboard connection cards
email: email || undefined,
displayName: displayName || undefined,
// Mirror identity into providerSpecificData so GrokCliExecutor can set
// x-email / x-userid without depending on top-level credential shape.
providerSpecificData: {
authMethod: "device_code",
idToken: tokens.id_token || null,
email: email || null,
userId,
hasGrokCodeAccess: extra?.user?.hasGrokCodeAccess ?? null,
subscriptionTier: extra?.user?.subscriptionTier ?? null,
},
};
},
};
export default grokCli;

View File

@@ -0,0 +1,91 @@
import { IFLOW_CONFIG } from "../constants/oauth.js";
const iflow = {
config: IFLOW_CONFIG,
flowType: "authorization_code",
buildAuthUrl: (config, redirectUri, state) => {
const params = new URLSearchParams({
loginMethod: config.extraParams.loginMethod,
type: config.extraParams.type,
redirect: redirectUri,
state: state,
client_id: config.clientId,
});
return `${config.authorizeUrl}?${params.toString()}`;
},
exchangeToken: async (config, code, redirectUri) => {
// Create Basic Auth header
const basicAuth = Buffer.from(
`${config.clientId}:${config.clientSecret}`
).toString("base64");
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
Authorization: `Basic ${basicAuth}`,
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
redirect_uri: redirectUri,
client_id: config.clientId,
client_secret: config.clientSecret,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Token exchange failed: ${error}`);
}
return await response.json();
},
postExchange: async (tokens) => {
// Fetch user info (MUST succeed to get API key)
const userInfoRes = await fetch(
`${IFLOW_CONFIG.userInfoUrl}?accessToken=${encodeURIComponent(tokens.access_token)}`,
{
headers: {
Accept: "application/json",
},
}
);
if (!userInfoRes.ok) {
const errorText = await userInfoRes.text();
throw new Error(`Failed to fetch user info: ${errorText}`);
}
const result = await userInfoRes.json();
if (!result.success) {
throw new Error(`User info request failed: ${result.message || 'Unknown error'}`);
}
const userInfo = result.data || {};
// Validate API key (critical for iFlow)
if (!userInfo.apiKey || userInfo.apiKey.trim() === "") {
throw new Error("Empty API key returned from iFlow");
}
// Validate email/phone
const email = userInfo.email?.trim() || userInfo.phone?.trim();
if (!email) {
throw new Error("Missing account email/phone in user info");
}
return { userInfo };
},
mapTokens: (tokens, extra) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
apiKey: extra?.userInfo?.apiKey,
email: extra?.userInfo?.email || extra?.userInfo?.phone,
displayName: extra?.userInfo?.nickname || extra?.userInfo?.name,
}),
};
export default iflow;

View File

@@ -0,0 +1,241 @@
// Ensure outbound fetch respects HTTP(S)_PROXY/ALL_PROXY in Node runtime
import "open-sse/index.js";
import { generatePKCE } from "../utils/pkce.js";
import { extractCodexAccountInfo, fetchKiroProfileArn } from "../providerHelpers.js";
import claude from "./claude.js";
import codex from "./codex.js";
import xai from "./xai.js";
import grokCli from "./grok-cli.js";
import geminiCli from "./gemini-cli.js";
import antigravity from "./antigravity.js";
import iflow from "./iflow.js";
import qoder from "./qoder.js";
import qwen from "./qwen.js";
import github from "./github.js";
import kiro from "./kiro.js";
import cursor from "./cursor.js";
import kimi from "./kimi.js";
import kilocode from "./kilocode.js";
import cline from "./cline.js";
import clinepass from "./clinepass.js";
import gitlab from "./gitlab.js";
import codebuddyCn from "./codebuddy-cn.js";
import codebuddyIntl from "./codebuddy-intl.js";
import kimchi from "./kimchi.js";
import trae from "./trae.js";
import windsurf from "./windsurf.js";
import zed from "./zed.js";
// Provider configurations
const PROVIDERS = {
claude,
codex,
xai,
"grok-cli": grokCli,
"gemini-cli": geminiCli,
antigravity,
iflow,
qoder,
qwen,
github,
kiro,
cursor,
kimi,
kilocode,
cline,
clinepass,
gitlab,
"codebuddy-cn": codebuddyCn,
"codebuddy-intl": codebuddyIntl,
kimchi,
trae,
windsurf,
zed,
};
export { PROVIDERS };
// Re-export helpers that other files import from this path
export { extractCodexAccountInfo, fetchKiroProfileArn };
/**
* Get provider handler
*/
export function getProvider(name) {
// Legacy kimi-coding → kimi (dual-auth merge)
const key = name === "kimi-coding" ? "kimi" : name;
const provider = PROVIDERS[key];
if (!provider) {
throw new Error(`Unknown provider: ${name}`);
}
return provider;
}
/**
* Get all provider names
*/
export function getProviderNames() {
return Object.keys(PROVIDERS);
}
/**
* Generate auth data for a provider
* @param {object} [meta] - Provider-specific metadata (e.g. gitlab clientId/baseUrl)
*/
export async function generateAuthData(providerName, redirectUri, meta) {
const provider = getProvider(providerName);
const config = provider.prepareConfig
? await provider.prepareConfig(provider.config, meta || {})
: provider.config;
const { codeVerifier: pkceVerifier, codeChallenge, state: pkceState } = generatePKCE(provider.pkceVerifierBytes);
// Trae uses loginTraceID (set by prepareConfig) as the callback matcher, not PKCE state.
const state = config.loginTraceID || pkceState;
// Zed: codeVerifier carries the encoded RSA private key (from prepareConfig), not a PKCE verifier.
const codeVerifier = config.privateKeyVerifier || pkceVerifier;
let authUrl;
if (provider.flowType === "device_code") {
// Device code flow doesn't have auth URL upfront
authUrl = null;
} else if (provider.flowType === "authorization_code_pkce") {
authUrl = provider.buildAuthUrl(config, redirectUri, state, codeChallenge, meta || {});
} else {
authUrl = provider.buildAuthUrl(config, redirectUri, state, undefined, meta || {});
}
return {
authUrl,
state,
codeVerifier,
codeChallenge,
redirectUri,
flowType: provider.flowType,
fixedPort: provider.fixedPort,
callbackPath: provider.callbackPath || "/callback",
};
}
/**
* Exchange code for tokens
* @param {object} [meta] - Provider-specific metadata (e.g. gitlab clientId/baseUrl)
*/
export async function exchangeTokens(providerName, code, redirectUri, codeVerifier, state, meta) {
const provider = getProvider(providerName);
const config = provider.prepareConfig
? await provider.prepareConfig(provider.config, meta || {})
: provider.config;
const tokens = await provider.exchangeToken(config, code, redirectUri, codeVerifier, state, meta || {});
let extra = null;
if (provider.postExchange) {
extra = await provider.postExchange(tokens);
}
return provider.mapTokens(tokens, extra);
}
/**
* Request device code (for device_code flow)
*/
export async function requestDeviceCode(providerName, codeChallenge, options) {
const provider = getProvider(providerName);
if (provider.flowType !== "device_code") {
throw new Error(`Provider ${providerName} does not support device code flow`);
}
return await provider.requestDeviceCode(provider.config, codeChallenge, options || {});
}
/**
* Poll for token (for device_code flow)
* @param {string} providerName - Provider name
* @param {string} deviceCode - Device code from requestDeviceCode
* @param {string} codeVerifier - PKCE code verifier (optional for some providers)
* @param {object} extraData - Extra data from device code response (e.g. clientId/clientSecret for Kiro)
*/
export async function pollForToken(providerName, deviceCode, codeVerifier, extraData) {
const provider = getProvider(providerName);
if (provider.flowType !== "device_code") {
throw new Error(`Provider ${providerName} does not support device code flow`);
}
const result = await provider.pollToken(provider.config, deviceCode, codeVerifier, extraData);
if (result.ok) {
// For device code flows, success is only when we have an access token
if (result.data.access_token) {
// Call postExchange to get additional data (copilotToken, userInfo, etc.)
let extra = null;
if (provider.postExchange) {
extra = await provider.postExchange(result.data);
}
const tokens = provider.mapTokens(result.data, extra);
// Kiro IDC/Builder-ID tokens lack profileArn; resolve it to avoid 403
if (providerName === "kiro" && !tokens.providerSpecificData?.profileArn) {
const profileArn = await fetchKiroProfileArn(tokens.accessToken);
if (profileArn) tokens.providerSpecificData.profileArn = profileArn;
}
return { success: true, tokens };
} else {
// Check if it's still pending authorization
if (result.data.error === 'authorization_pending' || result.data.error === 'slow_down') {
// This is not a failure, just still waiting
return {
success: false,
error: result.data.error,
errorDescription: result.data.error_description || result.data.message,
pending: result.data.error === 'authorization_pending'
};
} else {
// Actual error
return {
success: false,
error: result.data.error || 'no_access_token',
errorDescription: result.data.error_description || result.data.message || 'No access token received'
};
}
}
}
return { success: false, error: result.data.error, errorDescription: result.data.error_description };
}
// Run-once guard across the process lifetime
let codexBackfillDone = false;
// Backfill email + chatgpt account info for existing codex OAuth connections missing them
export async function backfillCodexEmails() {
if (codexBackfillDone) return;
codexBackfillDone = true;
try {
const { getProviderConnections, updateProviderConnection } = await import("@/lib/localDb");
const connections = await getProviderConnections();
const targets = connections.filter((c) => {
if (c.provider !== "codex" || c.authType !== "oauth" || !c.idToken) return false;
const hasEmail = !!c.email;
const hasAccountInfo = !!c.providerSpecificData?.chatgptAccountId;
return !hasEmail || !hasAccountInfo;
});
for (const conn of targets) {
const info = extractCodexAccountInfo(conn.idToken);
if (!info.email && !info.chatgptAccountId) continue;
const patch = {};
if (!conn.email && info.email) patch.email = info.email;
if (info.chatgptAccountId || info.chatgptPlanType) {
patch.providerSpecificData = {
...(conn.providerSpecificData || {}),
chatgptAccountId: info.chatgptAccountId,
chatgptPlanType: info.chatgptPlanType,
};
}
if (Object.keys(patch).length) {
await updateProviderConnection(conn.id, patch);
}
}
} catch (err) {
codexBackfillDone = false;
console.log("backfillCodexEmails failed:", err?.message || err);
}
}

View File

@@ -0,0 +1,60 @@
import { KILOCODE_CONFIG } from "../constants/oauth.js";
const kilocode = {
config: KILOCODE_CONFIG,
flowType: "device_code",
requestDeviceCode: async (config) => {
const response = await fetch(config.initiateUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
if (response.status === 429) {
throw new Error("Too many pending authorization requests. Please try again later.");
}
const error = await response.text();
throw new Error(`Device auth initiation failed: ${error}`);
}
const data = await response.json();
return {
device_code: data.code,
user_code: data.code,
verification_uri: data.verificationUrl,
verification_uri_complete: data.verificationUrl,
expires_in: data.expiresIn || 300,
interval: 3,
};
},
pollToken: async (config, deviceCode) => {
const response = await fetch(`${config.pollUrlBase}/${deviceCode}`);
if (response.status === 202) return { ok: false, data: { error: "authorization_pending" } };
if (response.status === 403) return { ok: false, data: { error: "access_denied", error_description: "Authorization denied by user" } };
if (response.status === 410) return { ok: false, data: { error: "expired_token", error_description: "Authorization code expired" } };
if (!response.ok) return { ok: false, data: { error: "poll_failed", error_description: `Poll failed: ${response.status}` } };
const data = await response.json();
if (data.status === "approved" && data.token) {
// Fetch profile to get orgId for X-Kilocode-OrganizationID header
let orgId = null;
try {
const profileRes = await fetch(`${config.apiBaseUrl}/api/profile`, {
headers: { "Authorization": `Bearer ${data.token}` }
});
if (profileRes.ok) {
const profile = await profileRes.json();
orgId = profile.organizations?.[0]?.id || null;
}
} catch {}
return { ok: true, data: { access_token: data.token, _userEmail: data.userEmail, _orgId: orgId } };
}
return { ok: false, data: { error: "authorization_pending" } };
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: null,
expiresIn: null,
email: tokens._userEmail,
...(tokens._orgId ? { providerSpecificData: { orgId: tokens._orgId } } : {}),
}),
};
export default kilocode;

View File

@@ -0,0 +1,75 @@
import { KIMCHI_CONFIG } from "../constants/oauth.js";
const kimchi = {
config: KIMCHI_CONFIG,
flowType: "browser_token",
buildAuthUrl: (config, redirectUri, state) => {
const baseUrl = (config.webAppUrl || "https://app.kimchi.dev").replace(/\/+$/, "");
const params = new URLSearchParams({
callback: redirectUri,
state,
});
return `${baseUrl}/cli-auth?${params.toString()}`;
},
exchangeToken: async (config, token) => {
const accessToken = String(token || "").trim();
if (!accessToken) {
throw new Error("Missing Kimchi token");
}
const validationUrl = config.validationUrl || "https://api.cast.ai/v1/llm/openai/supported-providers";
const validationRes = await fetch(validationUrl, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${accessToken}`,
},
});
if (!validationRes.ok) {
throw new Error(`Kimchi token validation failed: ${validationRes.status}`);
}
let userInfo = {};
if (config.userInfoUrl) {
try {
const userRes = await fetch(config.userInfoUrl, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${accessToken}`,
},
});
if (userRes.ok) {
userInfo = await userRes.json();
}
} catch {
userInfo = {};
}
}
return {
access_token: accessToken,
token_type: "Bearer",
_kimchiUser: userInfo,
};
},
mapTokens: (tokens) => {
const user = tokens._kimchiUser || {};
const userId = user.id ? String(user.id) : "";
const username = user.username || "";
const email = user.email || (userId ? `kimchi-user-${userId}` : null);
return {
accessToken: tokens.access_token,
refreshToken: null,
email,
displayName: user.name || username || null,
providerSpecificData: {
authMethod: "browser_token",
userId,
username,
},
};
},
};
export default kimchi;

View File

@@ -0,0 +1,81 @@
import crypto from "crypto";
import { KIMI_CONFIG } from "../constants/oauth.js";
// Kimi Code device flow (CLIProxyAPI internal/auth/kimi). Id is `kimi`;
// `kimi-coding` remains an alias key so old UI/API routes still resolve.
const kimi = {
config: KIMI_CONFIG,
flowType: "device_code",
requestDeviceCode: async (config) => {
const { buildKimiHeaders } = await import("open-sse/config/appConstants.js");
const deviceId = crypto.randomUUID();
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
...buildKimiHeaders(deviceId),
};
const response = await fetch(config.deviceCodeUrl, {
method: "POST",
headers,
body: new URLSearchParams({ client_id: config.clientId }),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Device code request failed: ${error}`);
}
const data = await response.json();
const authorizeDeviceUrl = config.authorizeDeviceUrl || "https://www.kimi.com/code/authorize_device";
return {
device_code: data.device_code,
user_code: data.user_code,
verification_uri: data.verification_uri || authorizeDeviceUrl,
verification_uri_complete:
data.verification_uri_complete ||
`${authorizeDeviceUrl}?user_code=${data.user_code}`,
expires_in: data.expires_in,
interval: data.interval || 5,
_kimiDeviceId: deviceId,
};
},
pollToken: async (config, deviceCode, _codeVerifier, extraData) => {
const { buildKimiHeaders } = await import("open-sse/config/appConstants.js");
const deviceId = extraData?._kimiDeviceId;
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
...buildKimiHeaders(deviceId),
};
const response = await fetch(config.tokenUrl, {
method: "POST",
headers,
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: config.clientId,
device_code: deviceCode,
}),
});
let data;
try {
data = await response.json();
} catch {
data = { error: "invalid_response", error_description: "non-json token response" };
}
// CLIProxyAPI: Kimi returns 200 for pending states with error field
if (data.error === "authorization_pending" || data.error === "slow_down") {
return { ok: true, data };
}
if (data.access_token && deviceId) data._kimiDeviceId = deviceId;
return { ok: response.ok || !!data.access_token || !!data.error, data };
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
providerSpecificData: {
authMethod: "device_code",
...(tokens._kimiDeviceId ? { deviceId: tokens._kimiDeviceId } : {}),
},
}),
};
export default kimi;

View File

@@ -0,0 +1,151 @@
import { KIRO_CONFIG, assertValidAwsRegion } from "../constants/oauth.js";
import { extractEmailFromAccessToken } from "../providerHelpers.js";
const kiro = {
config: KIRO_CONFIG,
flowType: "device_code",
// Kiro uses AWS SSO OIDC - requires client registration first
requestDeviceCode: async (config, codeChallenge, options = {}) => {
const trimmedRegion = typeof options.region === "string" ? options.region.trim() : "";
const region = trimmedRegion || "us-east-1";
assertValidAwsRegion(region);
const trimmedStartUrl = typeof options.startUrl === "string" ? options.startUrl.trim() : "";
const startUrl = trimmedStartUrl || config.startUrl;
const authMethod = options.authMethod === "idc" ? "idc" : "builder-id";
const registerClientUrl = `https://oidc.${region}.amazonaws.com/client/register`;
const deviceAuthUrl = `https://oidc.${region}.amazonaws.com/device_authorization`;
// Step 1: Register client with AWS SSO OIDC
const registerRes = await fetch(registerClientUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
clientName: config.clientName,
clientType: config.clientType,
scopes: config.scopes,
grantTypes: config.grantTypes,
issuerUrl: config.issuerUrl,
}),
});
if (!registerRes.ok) {
const error = await registerRes.text();
throw new Error(`Client registration failed: ${error}`);
}
const clientInfo = await registerRes.json();
// Step 2: Request device authorization
const deviceRes = await fetch(deviceAuthUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
clientId: clientInfo.clientId,
clientSecret: clientInfo.clientSecret,
startUrl,
}),
});
if (!deviceRes.ok) {
const error = await deviceRes.text();
throw new Error(`Device authorization failed: ${error}`);
}
const deviceData = await deviceRes.json();
// Return combined data for polling
return {
device_code: deviceData.deviceCode,
user_code: deviceData.userCode,
verification_uri: deviceData.verificationUri,
verification_uri_complete: deviceData.verificationUriComplete,
expires_in: deviceData.expiresIn,
interval: deviceData.interval || 5,
// Store client credentials for token exchange
_clientId: clientInfo.clientId,
_clientSecret: clientInfo.clientSecret,
_region: region,
_authMethod: authMethod,
_startUrl: startUrl,
};
},
pollToken: async (config, deviceCode, codeVerifier, extraData) => {
const region = extraData?._region || "us-east-1";
assertValidAwsRegion(region);
const tokenUrl = `https://oidc.${region}.amazonaws.com/token`;
const response = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
clientId: extraData?._clientId,
clientSecret: extraData?._clientSecret,
deviceCode: deviceCode,
grantType: "urn:ietf:params:oauth:grant-type:device_code",
}),
});
let data;
try {
data = await response.json();
} catch (e) {
const text = await response.text();
data = { error: "invalid_response", error_description: text };
}
// AWS SSO OIDC returns camelCase
if (data.accessToken) {
return {
ok: true,
data: {
access_token: data.accessToken,
refresh_token: data.refreshToken,
expires_in: data.expiresIn,
profile_arn: data?.profileArn || null,
// Store client credentials for refresh
_clientId: extraData?._clientId,
_clientSecret: extraData?._clientSecret,
_region: extraData?._region,
_authMethod: extraData?._authMethod,
_startUrl: extraData?._startUrl,
},
};
}
return {
ok: false,
data: {
error: data.error || "authorization_pending",
error_description: data.error_description || data.message,
},
};
},
mapTokens: (tokens) => {
const email = extractEmailFromAccessToken(tokens.access_token);
const mapped = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
email,
providerSpecificData: {
profileArn: tokens?.profile_arn || null,
clientId: tokens._clientId,
clientSecret: tokens._clientSecret,
region: tokens._region || "us-east-1",
authMethod: tokens._authMethod || "builder-id",
startUrl: tokens._startUrl || KIRO_CONFIG.startUrl,
},
};
return mapped;
},
};
export default kiro;

View File

@@ -0,0 +1,102 @@
import { QODER_CONFIG } from "../constants/oauth.js";
const qoder = {
config: QODER_CONFIG,
flowType: "device_code",
// Qoder uses a custom device flow: PKCE + nonce + machine_id are generated
// locally, the user lands on qoder.com/device/selectAccounts in the
// browser, and we poll openapi.qoder.sh until a `dt-...` token appears.
requestDeviceCode: async (config) => {
const { QoderService } = await import("@/lib/oauth/services/qoder");
const flow = new QoderService().initiateDeviceFlow();
// Match the device_code shape the rest of the OAuthModal expects
// (device_code, user_code, verification_uri[_complete], interval).
// The poll endpoint identifies us by nonce+verifier, not by a
// server-issued device_code, so we plumb our own values through:
// device_code = nonce (modal forwards as deviceCode on poll)
// codeVerifier = our PKCE verifier (route forwards as codeVerifier)
return {
device_code: flow.nonce,
user_code: flow.nonce.slice(0, 8).toUpperCase(),
verification_uri: config.loginUrl,
verification_uri_complete: flow.verificationUriComplete,
expires_in: 300,
interval: 2,
codeVerifier: flow.codeVerifier,
_qoderNonce: flow.nonce,
_qoderMachineId: flow.machineId,
};
},
pollToken: async (config, deviceCode, codeVerifier, extraData) => {
const { QoderService } = await import("@/lib/oauth/services/qoder");
const svc = new QoderService();
const nonce = deviceCode || extraData?._qoderNonce;
const verifier = codeVerifier || extraData?._qoderVerifier;
if (!nonce || !verifier) {
return {
ok: false,
data: { error: "invalid_request", error_description: "Missing nonce/verifier" },
};
}
let result;
try {
result = await svc.pollDeviceToken({ nonce, codeVerifier: verifier });
} catch (err) {
return {
ok: false,
data: { error: "poll_failed", error_description: err.message },
};
}
if (result.status === "pending") {
return { ok: false, data: { error: "authorization_pending" } };
}
// Best-effort profile lookup so we have a name/email to display.
const userInfo = await svc.fetchUserInfo(result.accessToken);
// expireTime is a Unix-ms timestamp from QoderService.parseExpiry,
// which already falls back to "now + 30 days" when the upstream
// omits expiry. Floor to a sane minimum (1 day) so a stale or
// skewed upstream timestamp doesn't truncate the stored token below
// something useful.
const minSeconds = 24 * 60 * 60;
const remainingSeconds = Math.floor((result.expireTime - Date.now()) / 1000);
const expiresIn = Math.max(minSeconds, remainingSeconds);
return {
ok: true,
data: {
access_token: result.accessToken,
refresh_token: result.refreshToken,
expires_in: expiresIn,
_qoderUserId: result.userId,
_qoderMachineId: extraData?._qoderMachineId || "",
_qoderName: userInfo.name,
_qoderEmail: userInfo.email,
_qoderOrganizationId: userInfo.organizationId,
},
};
},
mapTokens: (tokens) => {
const rawEmail = (tokens._qoderEmail || "").trim();
const displayName = (tokens._qoderName || "").trim() || null;
const userId = tokens._qoderUserId || "";
// Dedup in createProviderConnection requires a non-empty email. When
// fetchUserInfo silently fails (returns ""), fall back to a stable
// synthetic identifier derived from userId so re-logins update the
// existing row instead of accumulating "Account N" duplicates.
const email = rawEmail || (userId ? `qoder-user-${userId}` : null);
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || null,
expiresIn: tokens.expires_in,
email,
displayName,
providerSpecificData: {
authMethod: "device",
userId,
machineId: tokens._qoderMachineId || "",
organizationId: tokens._qoderOrganizationId || "",
},
};
},
};
export default qoder;

View File

@@ -0,0 +1,56 @@
import { QWEN_CONFIG } from "../constants/oauth.js";
const qwen = {
config: QWEN_CONFIG,
flowType: "device_code",
requestDeviceCode: async (config, codeChallenge) => {
const response = await fetch(config.deviceCodeUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
client_id: config.clientId,
scope: config.scope,
code_challenge: codeChallenge,
code_challenge_method: config.codeChallengeMethod,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Device code request failed: ${error}`);
}
return await response.json();
},
pollToken: async (config, deviceCode, codeVerifier) => {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: config.clientId,
device_code: deviceCode,
code_verifier: codeVerifier,
}),
});
return {
ok: response.ok,
data: await response.json(),
};
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
providerSpecificData: { resourceUrl: tokens.resource_url },
}),
};
export default qwen;

View File

@@ -0,0 +1,263 @@
import crypto from "crypto";
import { TRAE_CONFIG } from "../constants/oauth.js";
import { extractJsonPath } from "./_shared.js";
// ───────────────────────────────────────────────────────────────────────────
// Trae (ByteDance marscode) OAuth helpers
// ───────────────────────────────────────────────────────────────────────────
// Per-login device context. No IDE access in 9router, so use stable defaults.
function buildTraeDeviceContext() {
return {
plugin_version: TRAE_CONFIG.defaultPluginVersion,
machine_id: crypto.randomUUID(),
device_id: TRAE_CONFIG.defaultDeviceId,
x_device_brand: "unknown",
x_device_type: "unknown",
x_os_version: "unknown",
x_env: "",
x_app_version: TRAE_CONFIG.defaultAppVersion,
x_app_type: TRAE_CONFIG.defaultAppType,
};
}
// POST GetLoginGuidance → { Result: { LoginHost } }
async function fetchTraeLoginGuidance(loginTraceId) {
const body = JSON.stringify({ loginTraceID: loginTraceId, login_trace_id: loginTraceId });
let lastErr = "no successful response";
for (const url of TRAE_CONFIG.loginGuidanceUrls) {
try {
const res = await fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": TRAE_CONFIG.userAgent,
},
body,
});
if (!res.ok) { lastErr = `${url} HTTP ${res.status}`; continue; }
const data = await res.json();
const loginHost = extractJsonPath(data, [
["Result", "LoginHost"], ["Result", "loginHost"], ["Result", "LoginURL"],
["result", "loginHost"], ["data", "Result", "LoginHost"], ["data", "loginHost"],
["LoginHost"], ["loginHost"],
]);
if (loginHost) return loginHost;
lastErr = `${url} missing LoginHost`;
} catch (e) { lastErr = `${url} ${e.message}`; }
}
throw new Error(`Trae GetLoginGuidance failed: ${lastErr}`);
}
// Build the browser verification URL the user opens to sign in.
function buildTraeVerificationUrl(loginHost, loginTraceId, callbackUrl, ctx) {
const url = new URL(loginHost.startsWith("http") ? loginHost : `https://${loginHost.replace(/^\/+/, "")}`);
url.pathname = TRAE_CONFIG.authorizationPath;
const p = new URLSearchParams();
p.set("login_version", "1");
p.set("auth_from", "trae");
p.set("login_channel", "native_ide");
p.set("plugin_version", ctx.plugin_version);
p.set("auth_type", "local");
p.set("client_id", TRAE_CONFIG.clientId);
p.set("redirect", "0");
p.set("login_trace_id", loginTraceId);
p.set("auth_callback_url", callbackUrl);
p.set("machine_id", ctx.machine_id);
p.set("device_id", ctx.device_id);
p.set("x_device_id", ctx.device_id);
p.set("x_machine_id", ctx.machine_id);
p.set("x_device_brand", ctx.x_device_brand);
p.set("x_device_type", ctx.x_device_type);
p.set("x_os_version", ctx.x_os_version);
p.set("x_env", ctx.x_env);
p.set("x_app_version", ctx.x_app_version);
p.set("x_app_type", ctx.x_app_type);
url.search = p.toString();
return url.toString();
}
// Parse the Trae OAuth callback (query string or full URL).
// Expected: ?isRedirect=true&refreshToken=...&loginHost=...[&x-cloudide-token=...]
function parseTraeCallback(raw) {
const text = String(raw || "").trim();
let queryStr = text;
if (text.includes("?")) queryStr = text.slice(text.indexOf("?") + 1);
if (text.startsWith("#")) queryStr = text.slice(1);
const params = Object.fromEntries(new URLSearchParams(queryStr));
const pick = (keys) => {
for (const k of keys) { const v = params[k]; if (v && String(v).trim()) return String(v).trim(); }
return null;
};
const err = pick(["error", "error_code", "errorCode"]);
if (err) {
const desc = pick(["error_description", "error_desc", "message"]);
throw new Error(desc ? `Trae auth failed: ${err} (${desc})` : `Trae auth failed: ${err}`);
}
const refreshToken = pick(["refreshToken", "refresh_token", "RefreshToken"]);
if (!refreshToken) throw new Error("Trae callback missing refreshToken");
const loginHost = pick(["loginHost", "login_host", "LoginHost", "host", "consoleHost"]);
if (!loginHost) throw new Error("Trae callback missing loginHost");
const cloudideToken = pick(["x-cloudide-token", "xCloudideToken", "accessToken", "access_token", "token"]);
return { refreshToken, loginHost, cloudideToken };
}
// Allowed API origins for ExchangeToken/GetUserInfo — hardcoded HTTPS allowlist only.
// loginHost from the callback is intentionally NOT honored (SSRF guard: a callback
// attacker could otherwise point this at internal hosts/cloud metadata).
function traeApiOrigins() {
return [...TRAE_CONFIG.apiOrigins];
}
// POST ExchangeToken {ClientID, RefreshToken, ClientSecret, UserID} → {Result:{AccessToken,RefreshToken,ExpiresAt}}
async function fetchTraeExchangeToken(refreshToken, cloudideToken) {
const body = JSON.stringify({
ClientID: TRAE_CONFIG.clientId,
RefreshToken: refreshToken,
ClientSecret: TRAE_CONFIG.clientSecret,
UserID: "",
});
let lastErr = "no successful response";
for (const origin of traeApiOrigins()) {
const url = `${origin.replace(/\/$/, "")}${TRAE_CONFIG.exchangeTokenPath}`;
try {
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": TRAE_CONFIG.userAgent,
};
if (cloudideToken) headers["x-cloudide-token"] = cloudideToken;
const res = await fetch(url, { method: "POST", headers, body });
const text = await res.text();
if (!res.ok) { lastErr = `${url} HTTP ${res.status}`; continue; }
let data; try { data = JSON.parse(text); } catch { lastErr = `${url} invalid JSON`; continue; }
const accessToken = extractJsonPath(data, [
["Result", "AccessToken"], ["Result", "accessToken"], ["result", "access_token"], ["accessToken"],
]);
if (!accessToken) {
const msg = extractJsonPath(data, [["message"], ["msg"], ["error"], ["Result", "Message"]]) || "missing AccessToken";
lastErr = `${url} ${msg}`;
continue;
}
return {
accessToken,
refreshToken: extractJsonPath(data, [["Result", "RefreshToken"], ["result", "refresh_token"], ["refreshToken"]]) || refreshToken,
expiresIn: null, // ExchangeToken returns ExpiresAt (absolute), converted below
expiresAt: extractJsonPath(data, [["Result", "ExpiresAt"], ["Result", "expiresAt"], ["result", "expires_at"], ["expiresAt"]]),
};
} catch (e) { lastErr = `${url} ${e.message}`; }
}
throw new Error(`Trae ExchangeToken failed: ${lastErr}`);
}
// POST GetUserInfo with x-cloudide-token → identity fields used by SOLO common_params.
async function fetchTraeUserInfo(accessToken) {
for (const origin of traeApiOrigins()) {
const url = `${origin.replace(/\/$/, "")}${TRAE_CONFIG.getUserInfoPath}`;
try {
const res = await fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": TRAE_CONFIG.userAgent,
"x-cloudide-token": accessToken,
},
body: JSON.stringify({}),
});
if (!res.ok) continue;
const data = await res.json();
return {
email: extractJsonPath(data, [
["Result", "NonPlainTextEmail"], ["Result", "Email"], ["Result", "email"],
["email"], ["data", "email"],
]),
name: extractJsonPath(data, [
["Result", "ScreenName"], ["Result", "Nickname"], ["Result", "Name"],
["result", "nickname"], ["nickname"], ["name"],
]),
aiRegion: extractJsonPath(data, [["Result", "AIRegion"], ["Result", "aiRegion"], ["aiRegion"]]),
region: extractJsonPath(data, [["Result", "Region"], ["Result", "region"], ["region"]]),
tenant: extractJsonPath(data, [["Result", "TenantID"], ["Result", "tenantId"], ["tenantId"]]),
userId: extractJsonPath(data, [["Result", "UserID"], ["Result", "userId"], ["userId"]]),
};
} catch { /* try next origin */ }
}
return { email: null, name: null };
}
// Map AIRegion (e.g. "SG", "US") → SOLO scope used in common_params.
function traeScopeForRegion(aiRegion) {
const r = (aiRegion || "").toLowerCase();
if (r === "sg" || r.includes("singapore")) return "marscode-sg";
if (r === "cn" || r.includes("cn") || r.includes("china")) return "marscode-cn";
return "marscode-us";
}
// Trae — browser OAuth: GetLoginGuidance → verification URL
// → local callback (refreshToken+loginHost) → ExchangeToken → GetUserInfo.
// state === config.loginTraceID so the proxy can match the callback.
const trae = {
config: TRAE_CONFIG,
flowType: "authorization_code",
callbackPath: TRAE_CONFIG.callbackPath,
prepareConfig: async (config) => {
const loginTraceID = crypto.randomUUID();
const loginHost = await fetchTraeLoginGuidance(loginTraceID);
return { ...config, loginTraceID, loginHost };
},
buildAuthUrl: (config, redirectUri, state) => {
const ctx = buildTraeDeviceContext();
const traceId = config.loginTraceID || state;
return buildTraeVerificationUrl(config.loginHost, traceId, redirectUri, ctx);
},
exchangeToken: async (config, code) => {
const trimmed = String(code || "").trim();
// Paste-token mode: raw Cloud-IDE-JWT (no refresh exchange)
const looksCallback = /[?=&]/.test(trimmed) && (trimmed.includes("refreshToken") || trimmed.includes("refresh_token"));
if (!looksCallback) {
// Strip "Cloud-IDE-JWT " / "Bearer " prefix users paste from the Authorization header
const clean = trimmed.replace(/^(Cloud-IDE-JWT|Bearer)\s+/i, "");
return { accessToken: clean, refreshToken: null, expiresIn: TRAE_CONFIG.tokenLifetimeDays * 24 * 60 * 60, _authMethod: "imported" };
}
const { refreshToken, cloudideToken } = parseTraeCallback(trimmed);
return { ...(await fetchTraeExchangeToken(refreshToken, cloudideToken)), _authMethod: "oauth" };
},
postExchange: async (tokens) => {
const userInfo = await fetchTraeUserInfo(tokens.accessToken);
return { userInfo };
},
mapTokens: (tokens, extra) => {
const expiresIn = tokens.expiresIn
|| (tokens.expiresAt ? Math.max(60, Number(tokens.expiresAt) - Math.floor(Date.now() / 1000)) : TRAE_CONFIG.tokenLifetimeDays * 24 * 60 * 60);
const ui = extra?.userInfo || {};
const aiRegion = ui.aiRegion || "US-East";
// SOLO common_params defaults — identity fields web_id/biz_user_id are not
// exposed by GetUserInfo; empty strings are accepted upstream (verified).
return {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresIn,
email: ui.email || undefined,
displayName: ui.name || undefined,
providerSpecificData: {
authMethod: tokens._authMethod || "oauth",
aiRegion,
region: ui.region || aiRegion,
tenant: ui.tenant || "marscode",
userId: ui.userId || "",
scope: traeScopeForRegion(aiRegion),
webId: "",
bizUserId: "",
userUniqueId: "",
appLanguage: "en",
appVersion: TRAE_CONFIG.defaultAppVersion,
userRegion: aiRegion === "SG" ? "SG" : "US",
userIdentity: "Free",
},
};
},
};
export default trae;

View File

@@ -0,0 +1,132 @@
import { WINDSURF_CONFIG } from "../constants/oauth.js";
import { extractJsonPath } from "./_shared.js";
// ───────────────────────────────────────────────────────────────────────────
// Windsurf OAuth helpers
// ───────────────────────────────────────────────────────────────────────────
async function windsurfSeatRequest(baseUrl, path, body) {
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
const res = await fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": WINDSURF_CONFIG.userAgent,
},
body: JSON.stringify(body),
});
const text = await res.text();
if (!res.ok) throw new Error(`Windsurf ${path} HTTP ${res.status}: ${text.slice(0, 200)}`);
try { return JSON.parse(text); } catch { throw new Error(`Windsurf ${path} invalid JSON`); }
}
// Parse Windsurf callback (query string or full URL): ?access_token=...&state=...
function parseWindsurfCallback(raw, expectedState) {
const text = String(raw || "").trim();
let queryStr = text;
if (text.includes("?")) queryStr = text.slice(text.indexOf("?") + 1);
if (text.startsWith("#")) queryStr = text.slice(1);
const params = Object.fromEntries(new URLSearchParams(queryStr));
const pick = (keys) => {
for (const k of keys) { const v = params[k]; if (v && String(v).trim()) return String(v).trim(); }
return null;
};
const err = pick(["error"]);
if (err) {
const desc = pick(["error_description"]);
throw new Error(desc ? `Windsurf auth failed: ${err} (${desc})` : `Windsurf auth failed: ${err}`);
}
const accessToken = pick(["access_token", "token"]);
if (!accessToken) throw new Error("Windsurf callback missing access_token");
const state = pick(["state"]);
if (expectedState && state && state !== expectedState) {
throw new Error("Windsurf callback state mismatch");
}
return { firebaseIdToken: accessToken };
}
// POST RegisterUser {firebase_id_token} → {apiKey, apiServerUrl, name}
async function fetchWindsurfRegisterUser(firebaseIdToken) {
const data = await windsurfSeatRequest(WINDSURF_CONFIG.registerApiBaseUrl, WINDSURF_CONFIG.registerPath, {
firebase_id_token: firebaseIdToken,
});
const apiKey = extractJsonPath(data, [["apiKey"], ["api_key"]]);
if (!apiKey) throw new Error("Windsurf RegisterUser missing apiKey");
const apiServerUrl = extractJsonPath(data, [["apiServerUrl"], ["api_server_url"]]) || WINDSURF_CONFIG.defaultApiServerUrl;
const name = extractJsonPath(data, [["name"]]);
return { apiKey, apiServerUrl, name };
}
// Best-effort: GetOneTimeAuthToken → GetCurrentUser → email/name.
async function fetchWindsurfUserInfo(apiServerUrl, firebaseIdToken) {
try {
const authRes = await windsurfSeatRequest(apiServerUrl, WINDSURF_CONFIG.oneTimeAuthPath, { firebaseIdToken });
const authToken = extractJsonPath(authRes, [["authToken"], ["auth_token"]]);
if (!authToken) return { email: null, name: null };
const userRes = await windsurfSeatRequest(apiServerUrl, WINDSURF_CONFIG.currentUserPath, {
authToken,
includeSubscription: true,
});
const user = userRes.user || userRes;
return {
email: extractJsonPath(user, [["email"]]),
name: extractJsonPath(user, [["name"]]),
};
} catch { return { email: null, name: null }; }
}
// Windsurf — browser OAuth: windsurf.com/signin →
// local callback (firebase JWT) → RegisterUser → apiKey (used as credential).
const windsurf = {
config: WINDSURF_CONFIG,
flowType: "authorization_code",
callbackPath: WINDSURF_CONFIG.callbackPath,
buildAuthUrl: (config, redirectUri, state) => {
const params = new URLSearchParams({
response_type: "token",
client_id: config.clientId,
redirect_uri: redirectUri,
state,
prompt: "login",
redirect_parameters_type: "query",
workflow: "onboarding",
});
return `${config.authBaseUrl}${config.signInPath}?${params.toString()}`;
},
exchangeToken: async (config, code, redirectUri, codeVerifier, state) => {
const trimmed = String(code || "").trim();
const looksCallback = trimmed.includes("?") || trimmed.includes("access_token=");
if (!looksCallback) {
// Paste-token mode: sk-ws-... apiKey OR firebase JWT (eyJ...). Strip "Bearer " if pasted.
const clean = trimmed.replace(/^Bearer\s+/i, "");
if (clean.startsWith("sk-ws-")) {
return { accessToken: clean, refreshToken: null, expiresIn: null, apiServerUrl: config.defaultApiServerUrl, firebaseIdToken: null, _authMethod: "imported" };
}
const reg = await fetchWindsurfRegisterUser(clean);
return { accessToken: reg.apiKey, refreshToken: null, expiresIn: null, apiServerUrl: reg.apiServerUrl, firebaseIdToken: clean, _authMethod: "imported" };
}
const { firebaseIdToken } = parseWindsurfCallback(trimmed, state);
const reg = await fetchWindsurfRegisterUser(firebaseIdToken);
return { accessToken: reg.apiKey, refreshToken: null, expiresIn: null, apiServerUrl: reg.apiServerUrl, firebaseIdToken, _authMethod: "oauth" };
},
postExchange: async (tokens) => {
if (!tokens.firebaseIdToken) return { userInfo: { email: null, name: null } };
const info = await fetchWindsurfUserInfo(tokens.apiServerUrl, tokens.firebaseIdToken);
return { userInfo: info };
},
mapTokens: (tokens, extra) => ({
accessToken: tokens.accessToken,
refreshToken: null,
expiresIn: null,
email: extra?.userInfo?.email || undefined,
displayName: extra?.userInfo?.name || undefined,
providerSpecificData: {
authMethod: tokens._authMethod || "oauth",
apiServerUrl: tokens.apiServerUrl,
firebaseIdToken: tokens.firebaseIdToken,
},
}),
};
export default windsurf;

View File

@@ -0,0 +1,96 @@
import crypto from "crypto";
import { XAI_CONFIG, XAI_PKCE_VERIFIER_BYTES } from "../constants/xai.js";
import { validateXaiOAuthEndpoint, decodeXaiIdTokenEmail } from "../providerHelpers.js";
// Inlined from services/xai.js to keep web route bundle free of `open` (CLI-only) package
let cachedXaiDiscovery = null;
async function discoverXaiEndpoints() {
if (cachedXaiDiscovery) return cachedXaiDiscovery;
try {
const res = await fetch(XAI_CONFIG.discoveryUrl, { headers: { Accept: "application/json" } });
if (res.ok) {
const data = await res.json();
cachedXaiDiscovery = {
authorizeUrl: validateXaiOAuthEndpoint(data.authorization_endpoint, "authorization_endpoint"),
tokenUrl: validateXaiOAuthEndpoint(data.token_endpoint, "token_endpoint"),
};
return cachedXaiDiscovery;
}
} catch { /* fall through to static fallback */ }
cachedXaiDiscovery = { authorizeUrl: XAI_CONFIG.authorizeUrl, tokenUrl: XAI_CONFIG.tokenUrl };
return cachedXaiDiscovery;
}
const xai = {
config: XAI_CONFIG,
flowType: "authorization_code_pkce",
fixedPort: XAI_CONFIG.loopbackPort,
callbackPath: XAI_CONFIG.callbackPath,
pkceVerifierBytes: XAI_PKCE_VERIFIER_BYTES,
prepareConfig: async (config) => {
const endpoints = await discoverXaiEndpoints();
return {
...config,
authorizeUrl: endpoints.authorizeUrl,
tokenUrl: endpoints.tokenUrl,
};
},
buildAuthUrl: (config, redirectUri, state, codeChallenge) => {
// Mirror CLIProxyAPI BuildAuthorizeURL: includes nonce, plan, referrer
const nonce = crypto.randomBytes(16).toString("hex");
const params = {
response_type: "code",
client_id: config.clientId,
redirect_uri: redirectUri,
scope: config.scope,
code_challenge: codeChallenge,
code_challenge_method: config.codeChallengeMethod,
state,
nonce,
plan: "generic",
referrer: "cli-proxy-api",
};
const qs = Object.entries(params)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join("&");
return `${config.authorizeUrl}?${qs}`;
},
exchangeToken: async (config, code, redirectUri, codeVerifier) => {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: config.clientId,
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`xAI token exchange failed: ${error}`);
}
return await response.json();
},
mapTokens: (tokens) => {
const mapped = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
scope: tokens.scope,
};
const email = decodeXaiIdTokenEmail(tokens.id_token);
if (email) mapped.email = email;
if (tokens.id_token) {
mapped.providerSpecificData = { idToken: tokens.id_token };
}
return mapped;
},
};
export default xai;

View File

@@ -0,0 +1,62 @@
import { ZED_HOSTED_CONFIG } from "../constants/oauth.js";
import {
createZedNativeAuthData,
parseZedCallbackPayload,
decryptZedAccessToken,
fetchZedAuthenticatedUser,
resolveZedOrganizationId,
} from "open-sse/shared/zedAuth.js";
// Zed — RSA keypair native-app flow (NOT OAuth). prepareConfig generates a fresh
// keypair; buildAuthUrl returns the native_app_signin URL; exchangeToken decrypts
// the RSA-encrypted access token from the local callback.
const zed = {
config: ZED_HOSTED_CONFIG,
flowType: "authorization_code",
callbackPath: "/",
prepareConfig: async (config, meta) => {
// native_app_port is the local callback port (passed via meta from start-proxy).
const nativeAppPort = Number(meta?.nativeAppPort) || ZED_HOSTED_CONFIG.defaultNativeAppPort;
const auth = createZedNativeAuthData(config, { nativeAppPort });
return { ...config, ...auth };
},
buildAuthUrl: (config, redirectUri, state) => config.authUrl,
exchangeToken: async (config, code, redirectUri, codeVerifier, state) => {
// code = raw callback URL/query; codeVerifier = encoded private key verifier.
const { userId, encryptedAccessToken } = parseZedCallbackPayload(code);
const accessToken = decryptZedAccessToken(encryptedAccessToken, codeVerifier);
return { accessToken, userId, systemId: config.systemId };
},
postExchange: async (tokens) => {
const credentials = {
accessToken: tokens.accessToken,
providerSpecificData: { userId: tokens.userId, systemId: tokens.systemId },
};
let userInfo = null;
try {
userInfo = await fetchZedAuthenticatedUser(credentials, { config: ZED_HOSTED_CONFIG });
} catch { /* best-effort */ }
const organizationId = resolveZedOrganizationId(credentials, userInfo);
return {
userInfo,
organizationId,
email: userInfo?.email || null,
name: userInfo?.name || userInfo?.display_name || null,
};
},
mapTokens: (tokens, extra) => ({
accessToken: tokens.accessToken,
refreshToken: null,
expiresIn: null,
email: extra?.email || undefined,
displayName: extra?.name || undefined,
providerSpecificData: {
authMethod: "oauth",
userId: tokens.userId,
systemId: tokens.systemId,
organizationId: extra?.organizationId || "",
},
}),
};
export default zed;

View File

@@ -0,0 +1,57 @@
import fs from "fs/promises";
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import os from "os";
const execAsync = promisify(exec);
// Install paths per provider per platform — Trae and standard Windsurf IDE locations.
const IDE_PATHS = {
trae: {
darwin: ["/Applications/Trae.app"],
win32: [
path.join(process.env.LOCALAPPDATA || "", "Programs", "Trae", "Trae.exe"),
path.join(process.env.ProgramFiles || "", "Trae", "Trae.exe"),
],
linux: ["/usr/bin/trae", "/usr/local/bin/trae", "/opt/trae", "/opt/Trae"],
},
windsurf: {
darwin: ["/Applications/Windsurf.app"],
win32: [
path.join(process.env.LOCALAPPDATA || "", "Programs", "Windsurf", "Windsurf.exe"),
path.join(process.env.ProgramFiles || "", "Windsurf", "Windsurf.exe"),
],
linux: ["/usr/bin/windsurf", "/usr/local/bin/windsurf", "/opt/windsurf", "/opt/Windsurf"],
},
};
const IDE_BINARIES = {
trae: "trae",
windsurf: "windsurf",
};
async function pathExists(p) {
try { await fs.access(p); return true; } catch { return false; }
}
async function checkBinary(bin) {
try {
const cmd = os.platform() === "win32" ? `where ${bin}` : `which ${bin}`;
await execAsync(cmd, { windowsHide: true });
return true;
} catch { return false; }
}
// Returns { installed: boolean, path: string|null } for the given provider's IDE.
export async function detectIdeInstalled(providerId) {
const platform = os.platform();
const paths = IDE_PATHS[providerId];
if (!paths) return { installed: false, path: null };
for (const p of paths[platform] || []) {
if (p && await pathExists(p)) return { installed: true, path: p };
}
const bin = IDE_BINARIES[providerId];
if (bin && await checkBinary(bin)) return { installed: true, path: bin };
return { installed: false, path: null };
}

View File

@@ -1,6 +1,16 @@
import http from "http";
import { URL } from "url";
import { CODEX_CONFIG } from "../constants/oauth.js";
import { CODEX_CONFIG, TRAE_CONFIG, WINDSURF_CONFIG, ZED_HOSTED_CONFIG } from "../constants/oauth.js";
// Loopback origin guard for local callback proxies.
// Legit OAuth redirects are top-level navigations (no `Origin` header); a cross-site
// page issuing `fetch(..., {mode:"no-cors"})` to scan + hit 127.0.0.1 always sends
// `Origin: https://attacker`. Reject any non-loopback Origin to block login-CSRF.
function isLoopbackOrigin(origin) {
if (!origin) return true; // navigation redirect — allow
return /^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin);
}
/**
* Start a local HTTP server to receive OAuth callback
@@ -424,3 +434,324 @@ export function stopXaiProxy() {
}
}
// ───────────────────────────────────────────────────────────────────────────
// Trae dynamic-port proxy. Singleton session (one connect at a time per provider).
// Callback path = /callback with params refreshToken + loginHost.
// ───────────────────────────────────────────────────────────────────────────
let traeProxyServer = null;
let traeProxyTimeout = null;
let traeProxyPort = null;
let traeSession = null;
export function registerTraeSession({ state }) {
if (!state) return false;
traeSession = { state, status: "pending", createdAt: Date.now() };
return true;
}
export function getTraeSessionStatus(state) {
if (!traeSession) return null;
if (state && traeSession.state !== state) return null;
return traeSession;
}
export function clearTraeSession(state) {
if (!state || (traeSession && traeSession.state === state)) traeSession = null;
}
export function startTraeProxy() {
return new Promise((resolve) => {
if (traeProxyServer) {
resolve({ success: true, port: traeProxyPort, callbackUrl: `http://127.0.0.1:${traeProxyPort}${TRAE_CONFIG.callbackPath}` });
return;
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
if (url.pathname !== TRAE_CONFIG.callbackPath && url.pathname !== "/auth/callback") {
res.writeHead(404);
res.end("Not found");
return;
}
const session = traeSession;
if (!session) {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, "No active Trae login session"));
return;
}
// Anti-CSRF: reject cross-origin fetches (legit redirects send no Origin),
// and reject state mismatch when state is present.
if (!isLoopbackOrigin(req.headers.origin)) {
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, "Cross-origin callback rejected"));
return;
}
const cbState = url.searchParams.get("state");
if (cbState && session.state && cbState !== session.state) {
session.status = "error";
session.error = "Trae callback state mismatch";
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, session.error));
stopTraeProxy();
return;
}
// Pass the raw callback query to exchangeTokens → parseTraeCallback
const rawCallback = `${url.pathname}?${url.searchParams.toString()}`;
try {
const { exchangeTokens } = await import("../providers.js");
const { createProviderConnection } = await import("@/models");
const tokenData = await exchangeTokens("trae", rawCallback);
const connection = await createProviderConnection({
provider: "trae",
authType: "oauth",
...tokenData,
expiresAt: tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
: null,
testStatus: "active",
});
session.status = "done";
session.connectionId = connection.id;
session.email = connection.email;
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(true, "You can close this window."));
} catch (err) {
session.status = "error";
session.error = err.message;
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, err.message));
} finally {
stopTraeProxy();
}
});
server.listen(0, "127.0.0.1", () => {
traeProxyServer = server;
traeProxyPort = server.address().port;
traeProxyTimeout = setTimeout(() => stopTraeProxy(), TRAE_CONFIG.oauthTimeoutMs);
resolve({ success: true, port: traeProxyPort, callbackUrl: `http://127.0.0.1:${traeProxyPort}${TRAE_CONFIG.callbackPath}` });
});
server.on("error", (err) => resolve({ success: false, reason: err.message }));
});
}
export function stopTraeProxy() {
if (traeProxyTimeout) { clearTimeout(traeProxyTimeout); traeProxyTimeout = null; }
if (traeProxyServer) { traeProxyServer.close(); traeProxyServer = null; }
traeProxyPort = null;
}
// ───────────────────────────────────────────────────────────────────────────
// Windsurf dynamic-port proxy. Singleton session.
// Callback path = /windsurf-auth-callback with params access_token (firebase JWT) + state.
// ───────────────────────────────────────────────────────────────────────────
let windsurfProxyServer = null;
let windsurfProxyTimeout = null;
let windsurfProxyPort = null;
let windsurfSession = null;
export function registerWindsurfSession({ state }) {
if (!state) return false;
windsurfSession = { state, status: "pending", createdAt: Date.now() };
return true;
}
export function getWindsurfSessionStatus(state) {
if (!windsurfSession) return null;
if (state && windsurfSession.state !== state) return null;
return windsurfSession;
}
export function clearWindsurfSession(state) {
if (!state || (windsurfSession && windsurfSession.state === state)) windsurfSession = null;
}
export function startWindsurfProxy() {
return new Promise((resolve) => {
if (windsurfProxyServer) {
resolve({ success: true, port: windsurfProxyPort, callbackUrl: `http://127.0.0.1:${windsurfProxyPort}${WINDSURF_CONFIG.callbackPath}` });
return;
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
if (url.pathname !== WINDSURF_CONFIG.callbackPath) {
res.writeHead(404);
res.end("Not found");
return;
}
const session = windsurfSession;
if (!session) {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, "No active Windsurf login session"));
return;
}
// Anti-CSRF: reject cross-origin fetches, and require state present + matching.
if (!isLoopbackOrigin(req.headers.origin)) {
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, "Cross-origin callback rejected"));
return;
}
const cbState = url.searchParams.get("state");
if (!cbState || !session.state || cbState !== session.state) {
session.status = "error";
session.error = "Windsurf callback state mismatch";
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, session.error));
stopWindsurfProxy();
return;
}
const rawCallback = `${url.pathname}?${url.searchParams.toString()}`;
try {
const { exchangeTokens } = await import("../providers.js");
const { createProviderConnection } = await import("@/models");
const tokenData = await exchangeTokens("windsurf", rawCallback, null, null, session.state);
const connection = await createProviderConnection({
provider: "windsurf",
authType: "api_key",
...tokenData,
testStatus: "active",
});
session.status = "done";
session.connectionId = connection.id;
session.email = connection.email;
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(true, "You can close this window."));
} catch (err) {
session.status = "error";
session.error = err.message;
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, err.message));
} finally {
stopWindsurfProxy();
}
});
server.listen(0, "127.0.0.1", () => {
windsurfProxyServer = server;
windsurfProxyPort = server.address().port;
windsurfProxyTimeout = setTimeout(() => stopWindsurfProxy(), WINDSURF_CONFIG.oauthTimeoutMs);
resolve({ success: true, port: windsurfProxyPort, callbackUrl: `http://127.0.0.1:${windsurfProxyPort}${WINDSURF_CONFIG.callbackPath}` });
});
server.on("error", (err) => resolve({ success: false, reason: err.message }));
});
}
export function stopWindsurfProxy() {
if (windsurfProxyTimeout) { clearTimeout(windsurfProxyTimeout); windsurfProxyTimeout = null; }
if (windsurfProxyServer) { windsurfProxyServer.close(); windsurfProxyServer = null; }
windsurfProxyPort = null;
}
// ───────────────────────────────────────────────────────────────────────────
// Zed RSA native-app proxy. Singleton session.
// Callback: GET http://127.0.0.1:<port>/?user_id=...&access_token=<RSA-encrypted>
// The proxy decrypts the access token using the private key stored in session.codeVerifier.
// ───────────────────────────────────────────────────────────────────────────
let zedProxyServer = null;
let zedProxyTimeout = null;
let zedProxyPort = null;
let zedSession = null;
export function registerZedSession({ state, codeVerifier }) {
if (!state || !codeVerifier) return false;
zedSession = { state, codeVerifier, status: "pending", createdAt: Date.now() };
return true;
}
export function getZedSessionStatus(state) {
if (!zedSession) return null;
if (state && zedSession.state !== state) return null;
return zedSession;
}
export function clearZedSession(state) {
if (!state || (zedSession && zedSession.state === state)) zedSession = null;
}
export function startZedProxy(preferredPort = 0) {
return new Promise((resolve) => {
if (zedProxyServer) {
resolve({ success: true, port: zedProxyPort, callbackUrl: `http://127.0.0.1:${zedProxyPort}/` });
return;
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
// Log path + redacted params (access_token is the RSA-encrypted credential).
const redacted = Object.fromEntries(url.searchParams);
for (const k of ["access_token", "user_id", "code_verifier", "state"]) {
if (redacted[k]) redacted[k] = "<redacted>";
}
console.log("[Zed proxy]", req.method, url.pathname, JSON.stringify(redacted));
if (url.pathname !== "/" && url.pathname !== "/callback") {
res.writeHead(404);
res.end("Not found");
return;
}
const session = zedSession;
if (!session) {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, "No active Zed login session"));
return;
}
// Anti-CSRF: Zed tokens are RSA-encrypted to our keypair so they can't be
// forged cross-site, but still reject cross-origin fetches for defense-in-depth.
if (!isLoopbackOrigin(req.headers.origin)) {
res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, "Cross-origin callback rejected"));
return;
}
// Pass raw callback path+query to exchangeTokens → parseZedCallbackPayload.
// codeVerifier carries the encoded RSA private key for decryption.
const rawCallback = url.search ? `${url.pathname}?${url.searchParams.toString()}` : url.pathname;
try {
const { exchangeTokens } = await import("../providers.js");
const { createProviderConnection } = await import("@/models");
const tokenData = await exchangeTokens("zed", rawCallback, null, session.codeVerifier, session.state);
const connection = await createProviderConnection({
provider: "zed",
authType: "oauth",
...tokenData,
testStatus: "active",
});
session.status = "done";
session.connectionId = connection.id;
session.email = connection.email;
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(true, "You can close this window."));
} catch (err) {
session.status = "error";
session.error = err.message;
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, err.message));
} finally {
stopZedProxy();
}
});
const tryPort = Number(preferredPort) || 0;
server.on("error", (err) => {
// If the preferred port (e.g. 58443) is busy, fall back to a random port.
if (err.code === "EADDRINUSE" && tryPort !== 0) {
console.log(`[Zed proxy] port ${tryPort} busy, falling back to random`);
server.listen(0, "127.0.0.1", () => {
zedProxyServer = server;
zedProxyPort = server.address().port;
zedProxyTimeout = setTimeout(() => stopZedProxy(), ZED_HOSTED_CONFIG.oauthTimeoutMs);
console.log(`[Zed proxy] listening on random port ${zedProxyPort}`);
resolve({ success: true, port: zedProxyPort, callbackUrl: `http://127.0.0.1:${zedProxyPort}/` });
});
} else {
console.log(`[Zed proxy] listen error: ${err.message}`);
resolve({ success: false, reason: err.message });
}
});
server.listen(tryPort, "127.0.0.1", () => {
zedProxyServer = server;
zedProxyPort = server.address().port;
zedProxyTimeout = setTimeout(() => { console.log("[Zed proxy] timeout, stopping"); stopZedProxy(); }, ZED_HOSTED_CONFIG.oauthTimeoutMs);
console.log(`[Zed proxy] listening on port ${zedProxyPort}`);
resolve({ success: true, port: zedProxyPort, callbackUrl: `http://127.0.0.1:${zedProxyPort}/` });
});
});
}
export function stopZedProxy() {
console.log(`[Zed proxy] stopping (port ${zedProxyPort || "-"})`);
if (zedProxyTimeout) { clearTimeout(zedProxyTimeout); zedProxyTimeout = null; }
if (zedProxyServer) { zedProxyServer.close(); zedProxyServer = null; }
zedProxyPort = null;
}

View File

@@ -5,6 +5,31 @@ import PropTypes from "prop-types";
import { Modal, Button, Input } from "@/shared/components";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
// Providers using the dynamic-port local callback proxy.
// Browser OAuth: popup → auto callback → auto exchange → poll-status.
const PROXY_OAUTH_PROVIDERS = new Set(["trae", "windsurf", "zed"]);
// Providers offering a paste-token fallback (import-token flow).
// UX warns if the IDE (which issues the token) is not installed.
const PASTE_TOKEN_PROVIDERS = {
trae: {
label: "Cloud-IDE-JWT",
instructions:
"Sign in at trae.ai (or solo.trae.ai), open DevTools → Network, copy the Cloud-IDE-JWT token from any request's Authorization header (~14-day lifetime).",
placeholder: "Paste Cloud-IDE-JWT here...",
ideName: "Trae",
ideOptional: true, // token can be grabbed from DevTools without the IDE
},
windsurf: {
label: "Windsurf API key",
instructions:
"In the Windsurf/VS Code IDE, run the \"Windsurf: Provide Auth Token\" command, then copy the displayed sk-ws-... key.",
placeholder: "Paste sk-ws-... key here...",
ideName: "Windsurf",
ideOptional: false,
},
};
/**
* OAuth Modal Component
* - Localhost: Auto callback via popup message
@@ -18,6 +43,10 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
const [isDeviceCode, setIsDeviceCode] = useState(false);
const [deviceData, setDeviceData] = useState(null);
const [polling, setPolling] = useState(false);
// trae/windsurf: choose between browser OAuth (proxy) and paste-token (import)
const [authMode, setAuthMode] = useState("browser"); // "browser" | "paste-token"
const [pasteToken, setPasteToken] = useState("");
const [ideStatus, setIdeStatus] = useState(null);
const popupRef = useRef(null);
const pollingAbortRef = useRef(false);
const openedRef = useRef(false);
@@ -150,12 +179,50 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
setPolling(false);
}, [provider, onSuccess]);
// Trae/Windsurf proxy OAuth flow: dynamic-port local callback → auto exchange.
const startProxyFlow = useCallback(async (providerId) => {
// 1. Start the local callback server (returns a dynamic port + callback URL).
const startRes = await fetch(`/api/oauth/${providerId}/start-proxy`);
const startData = await startRes.json();
if (!startRes.ok || !startData.success || !startData.callbackUrl) {
throw new Error(startData.reason || startData.error || `Failed to start ${providerId} callback server`);
}
// 2. Build the authorize URL with redirect_uri = proxy callback URL.
const authorizeUrl = new URL(`/api/oauth/${providerId}/authorize`, window.location.origin);
authorizeUrl.searchParams.set("redirect_uri", startData.callbackUrl);
const authRes = await fetch(authorizeUrl);
const authData = await authRes.json();
if (!authRes.ok) throw new Error(authData.error);
// 3. Register the session so the proxy can match the incoming callback.
// Zed also passes code_verifier (encodes the RSA private key for decrypt);
// sent via POST body so the private key never lands in URL/query logs.
const regBody = { state: authData.state };
if (authData.codeVerifier) regBody.codeVerifier = authData.codeVerifier;
await fetch(`/api/oauth/${providerId}/register-session`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(regBody),
});
// 4. Open popup; proxy auto-exchanges on callback, modal polls poll-status.
setAuthData({ ...authData, proxyProvider: providerId });
setStep("waiting");
popupRef.current = window.open(authData.authUrl, "oauth_popup", "width=600,height=700");
if (!popupRef.current) setStep("input"); // popup blocked → fall back to manual paste
}, []);
// Start OAuth flow
const startOAuthFlow = useCallback(async () => {
if (!provider) return;
try {
setError(null);
// Trae/Windsurf: proxy OAuth (browser mode) — handled by dedicated flow.
// Paste-token mode is handled by handleManualSubmit (no /authorize call).
if (PROXY_OAUTH_PROVIDERS.has(provider) && authMode === "browser") {
await startProxyFlow(provider);
return;
}
// Device code flow providers (must match oauth providers with flowType: "device_code")
const deviceCodeProviders = [
"github",
@@ -165,6 +232,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
"kimi-coding",
"kilocode",
"codebuddy-cn",
"codebuddy-intl",
"qoder",
"grok-cli",
];
@@ -329,7 +397,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
setError(err.message);
setStep("error");
}
}, [provider, isLocalhost, startPolling, oauthMeta, idcConfig]);
}, [provider, isLocalhost, startPolling, oauthMeta, idcConfig, authMode, startProxyFlow]);
// Reset state and start OAuth when modal opens
useEffect(() => {
@@ -343,7 +411,17 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
setIsDeviceCode(false);
setDeviceData(null);
setPolling(false);
setAuthMode("browser");
setPasteToken("");
setIdeStatus(null);
pollingAbortRef.current = false;
// Best-effort IDE detection for paste-token providers (Trae/Windsurf)
if (PASTE_TOKEN_PROVIDERS[provider]) {
fetch(`/api/oauth/${provider}/ide-status`)
.then((r) => r.json())
.then((data) => setIdeStatus(data))
.catch(() => setIdeStatus({ installed: false, path: null }));
}
startOAuthFlow();
} else if (!isOpen) {
// Abort polling and cleanup proxy when modal closes
@@ -353,13 +431,26 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
fetch("/api/oauth/codex/stop-proxy").catch(() => {});
} else if (provider === "xai") {
fetch("/api/oauth/xai/stop-proxy").catch(() => {});
} else if (provider === "trae") {
fetch("/api/oauth/trae/stop-proxy").catch(() => {});
} else if (provider === "windsurf") {
fetch("/api/oauth/windsurf/stop-proxy").catch(() => {});
} else if (provider === "zed") {
fetch("/api/oauth/zed/stop-proxy").catch(() => {});
}
}
}, [isOpen, provider, startOAuthFlow]);
// Fixed-port server-side mode: poll status (proxy auto-exchanges + saves DB)
// Server-side proxy mode (codex/xai fixed-port + trae/windsurf dynamic-port):
// poll status until the proxy auto-exchanges and saves the connection.
useEffect(() => {
const pollProvider = authData?.codexServerSide ? "codex" : authData?.xaiServerSide ? "xai" : null;
const pollProvider = authData?.codexServerSide
? "codex"
: authData?.xaiServerSide
? "xai"
: authData?.proxyProvider
? authData.proxyProvider
: null;
if (!pollProvider || !authData?.state) return;
if (callbackProcessedRef.current) return;
let cancelled = false;
@@ -487,8 +578,38 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
try {
setError(null);
// Paste-token mode (Trae/Windsurf): token goes straight to /exchange
if (authMode === "paste-token" && PASTE_TOKEN_PROVIDERS[provider]) {
const token = pasteToken.trim();
if (!token) throw new Error("Missing token");
const res = await fetch(`/api/oauth/${provider}/exchange`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: token }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
setStep("success");
onSuccess?.();
return;
}
const input = callbackUrl.trim();
// Trae/Windsurf proxy flow fallback (popup blocked): paste the full callback URL
if (PROXY_OAUTH_PROVIDERS.has(provider) && input) {
const res = await fetch(`/api/oauth/${provider}/exchange`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: input, state: authData?.state }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
setStep("success");
onSuccess?.();
return;
}
// Detect raw JWT access token (starts with eyJ) — skip URL parsing
if (input.startsWith("eyJ") && input.includes(".")) {
await exchangeTokens(input, null);
@@ -538,6 +659,12 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
fetch("/api/oauth/codex/stop-proxy").catch(() => {});
} else if (provider === "xai") {
fetch("/api/oauth/xai/stop-proxy").catch(() => {});
} else if (provider === "trae") {
fetch("/api/oauth/trae/stop-proxy").catch(() => {});
} else if (provider === "windsurf") {
fetch("/api/oauth/windsurf/stop-proxy").catch(() => {});
} else if (provider === "zed") {
fetch("/api/oauth/zed/stop-proxy").catch(() => {});
}
onClose();
}, [onClose, provider]);
@@ -556,8 +683,82 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
return (
<Modal isOpen={isOpen} title={modalTitle} onClose={handleClose} size="lg">
<div className="flex flex-col gap-4">
{/* Waiting + Manual Input combined (non-device-code) */}
{(step === "waiting" || step === "input") && !isDeviceCode && (
{/* Trae/Windsurf: browser OAuth (proxy) + paste-token fallback */}
{PROXY_OAUTH_PROVIDERS.has(provider) && (step === "waiting" || step === "input" || step === "error") && (
<>
<div className="flex gap-2">
<button
type="button"
onClick={() => { setAuthMode("browser"); setError(null); setStep("waiting"); startOAuthFlow(); }}
className={`flex-1 rounded-lg border px-3 py-2 text-sm transition-colors ${authMode === "browser" ? "border-primary bg-primary/10 text-primary" : "border-border text-text-muted hover:text-primary"}`}
>
🌐 Sign in with browser
</button>
<button
type="button"
onClick={() => { setAuthMode("paste-token"); setError(null); setStep("input"); }}
className={`flex-1 rounded-lg border px-3 py-2 text-sm transition-colors ${authMode === "paste-token" ? "border-primary bg-primary/10 text-primary" : "border-border text-text-muted hover:text-primary"}`}
>
🔑 Paste token
</button>
</div>
{authMode === "browser" && (
<>
{step === "waiting" && (
<div className="flex items-center gap-2 px-3 py-2 border border-border rounded-lg bg-sidebar/50">
<span className="material-symbols-outlined text-base text-primary animate-spin">progress_activity</span>
<span className="text-sm">Waiting for browser authorization</span>
</div>
)}
{step === "input" && (
<div className="space-y-3">
<p className="text-sm text-text-muted">
Popup was blocked. After authorizing in the browser, paste the full callback URL here:
</p>
<Input
value={callbackUrl}
onChange={(e) => setCallbackUrl(e.target.value)}
placeholder="http://127.0.0.1:.../callback?..."
className="font-mono text-xs"
/>
<div className="flex gap-2">
<Button onClick={handleManualSubmit} fullWidth disabled={!callbackUrl}>Connect</Button>
<Button onClick={handleClose} variant="ghost" fullWidth>Cancel</Button>
</div>
</div>
)}
</>
)}
{authMode === "paste-token" && (
<div className="space-y-3">
{ideStatus && !ideStatus.installed && (
<div className={`px-3 py-2 rounded-lg text-sm ${PASTE_TOKEN_PROVIDERS[provider].ideOptional ? "bg-blue-500/10 text-blue-700 dark:text-blue-300" : "bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"}`}>
{PASTE_TOKEN_PROVIDERS[provider].ideName} IDE not detected.
{PASTE_TOKEN_PROVIDERS[provider].ideOptional
? " You can still grab the token from DevTools."
: ` Install ${PASTE_TOKEN_PROVIDERS[provider].ideName} IDE to get the token, or use "Sign in with browser".`}
</div>
)}
<p className="text-sm text-text-muted">{PASTE_TOKEN_PROVIDERS[provider].instructions}</p>
<Input
value={pasteToken}
onChange={(e) => setPasteToken(e.target.value)}
placeholder={PASTE_TOKEN_PROVIDERS[provider].placeholder}
className="font-mono text-xs"
/>
<div className="flex gap-2">
<Button onClick={handleManualSubmit} fullWidth disabled={!pasteToken}>Connect</Button>
<Button onClick={handleClose} variant="ghost" fullWidth>Cancel</Button>
</div>
</div>
)}
</>
)}
{/* Waiting + Manual Input combined (non-device-code, non-proxy) */}
{(step === "waiting" || step === "input") && !isDeviceCode && !PROXY_OAUTH_PROVIDERS.has(provider) && (
<>
{/* Option A: Auto via popup */}
<div className="flex items-center gap-2 px-3 py-2 border border-border rounded-lg bg-sidebar/50">

View File

@@ -113,6 +113,43 @@ describe("parseGrokCliBilling", () => {
expect(parsed.exhausted).toBe(false);
});
it("maps creditUsagePercent to a single Weekly SuperGrok bar (not productUsage)", () => {
const parsed = parseGrokCliBilling(
{
config: {
currentPeriod: {
type: "USAGE_PERIOD_TYPE_WEEKLY",
start: "2026-07-17T12:42:26.494595+00:00",
end: "2026-07-24T12:42:26.494595+00:00",
},
creditUsagePercent: 99.0,
onDemandCap: { val: 0 },
onDemandUsed: { val: 0 },
productUsage: [
{ product: "GrokBuild", usagePercent: 97.0 },
{ product: "GrokImagine", usagePercent: 2.0 },
],
isUnifiedBillingUser: true,
prepaidBalance: { val: 0 },
billingPeriodStart: "2026-07-17T12:42:26.494595+00:00",
billingPeriodEnd: "2026-07-24T12:42:26.494595+00:00",
},
},
{ subscriptionTier: "XPremiumPlus", hasGrokCodeAccess: true },
);
// Single shared-pool bar from creditUsagePercent
expect(parsed.quotas["Weekly SuperGrok"]).toMatchObject({
used: 99,
total: 100,
remainingPercentage: 1,
resetAt: "2026-07-24T12:42:26.494Z",
unlimited: false,
});
// productUsage must NOT become independent quota bars
expect(Object.keys(parsed.quotas)).toEqual(["Weekly SuperGrok"]);
expect(parsed.exhausted).toBe(false);
});
it("maps current monthly fields and snake-case subscription tier", () => {
const parsed = parseGrokCliBilling({
monthlyLimit: { val: 1000 },

View File

@@ -0,0 +1,146 @@
/**
* Generic OAuth2 token refresh — config-driven profiles.
*
* Verifies refreshAccessToken() handles the 5 foldable providers
* (qwen, iflow, github, kimi, claude) via a REFRESH_PROFILES table,
* while preserving the legacy generic path for unknown providers.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
const originalFetch = global.fetch;
function mockFetchOnce(payload, { ok = true, status = 200 } = {}) {
const fn = vi.fn().mockResolvedValue({
ok,
status,
json: () => Promise.resolve(payload),
text: () => Promise.resolve(JSON.stringify(payload)),
});
global.fetch = fn;
return fn;
}
describe("refreshAccessToken — config-driven profiles", () => {
beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); global.fetch = originalFetch; });
afterEach(() => { global.fetch = originalFetch; });
it("qwen: form body + clientId, surfaces resource_url as providerSpecificData", async () => {
const fm = mockFetchOnce({
access_token: "qw-acc",
refresh_token: "qw-refresh-rotated",
expires_in: 7200,
resource_url: "https://dashscope.aliyuncs.com",
});
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
const out = await refreshAccessToken("qwen", "qw-old-refresh", {}, console);
expect(out).toEqual({
accessToken: "qw-acc",
refreshToken: "qw-refresh-rotated",
expiresIn: 7200,
providerSpecificData: { resourceUrl: "https://dashscope.aliyuncs.com" },
});
const [url, init] = fm.mock.calls[0];
expect(init.method).toBe("POST");
expect(init.headers["Content-Type"]).toBe("application/x-www-form-urlencoded");
const body = new URLSearchParams(init.body);
expect(body.get("grant_type")).toBe("refresh_token");
expect(body.get("refresh_token")).toBe("qw-old-refresh");
expect(body.get("client_id")).toBeTruthy();
});
it("iflow: Basic Auth header from clientId:clientSecret, form body keeps client_secret", async () => {
const fm = mockFetchOnce({ access_token: "if-acc", refresh_token: "if-rot", expires_in: 3600 });
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
await refreshAccessToken("iflow", "if-old", {}, console);
const [, init] = fm.mock.calls[0];
expect(init.headers["Authorization"]).toMatch(/^Basic /);
const body = new URLSearchParams(init.body);
expect(body.get("client_id")).toBeTruthy();
expect(body.get("client_secret")).toBeTruthy();
});
it("github: omits client_secret when config has none", async () => {
const fm = mockFetchOnce({ access_token: "gh-acc", expires_in: 28800 });
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
const out = await refreshAccessToken("github", "gh-old", {}, console);
const body = new URLSearchParams(fm.mock.calls[0][1].body);
expect(body.get("client_secret")).toBeNull();
expect(out.accessToken).toBe("gh-acc");
expect(out.refreshToken).toBe("gh-old");
});
it("kimi: merges X-Msh-* headers from credentials.providerSpecificData.deviceId", async () => {
const fm = mockFetchOnce({ access_token: "km-acc", expires_in: 86400 });
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
await refreshAccessToken("kimi", "km-old", {
providerSpecificData: { deviceId: "dev-xyz" },
}, console);
const headers = fm.mock.calls[0][1].headers;
// Kimi's buildKimiHeaders must contribute at least one X-Msh- header
const mshKeys = Object.keys(headers).filter((k) => k.toLowerCase().startsWith("x-msh-"));
expect(mshKeys.length).toBeGreaterThan(0);
});
it("claude: JSON body, client_id only (no client_secret)", async () => {
const fm = mockFetchOnce({ access_token: "cl-acc", refresh_token: "cl-rot", expires_in: 3600 });
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
await refreshAccessToken("claude", "cl-old", {}, console);
const [, init] = fm.mock.calls[0];
expect(init.headers["Content-Type"]).toBe("application/json");
const parsed = JSON.parse(init.body);
expect(parsed.grant_type).toBe("refresh_token");
expect(parsed.client_id).toBeTruthy();
expect(parsed).not.toHaveProperty("client_secret");
});
it("returns null on non-ok response", async () => {
mockFetchOnce({ error: "invalid_grant" }, { ok: false, status: 400 });
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
const out = await refreshAccessToken("qwen", "dead", {}, console);
expect(out).toBeNull();
});
it("returns null when refreshToken missing", async () => {
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
const out = await refreshAccessToken("qwen", "", {}, console);
expect(out).toBeNull();
});
it("dedupes concurrent calls with same refresh token (same dedupKey)", async () => {
const fm = mockFetchOnce({ access_token: "dd-acc", expires_in: 3600 });
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
const creds = { providerSpecificData: { deviceId: "d" } };
await Promise.all([
refreshAccessToken("kimi", "dup-refresh", creds, console),
refreshAccessToken("kimi", "dup-refresh", creds, console),
]);
expect(fm).toHaveBeenCalledTimes(1);
});
});
describe("refreshAccessToken — legacy generic path (no profile)", () => {
beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); global.fetch = originalFetch; });
afterEach(() => { global.fetch = originalFetch; });
it("still works for an unprofiled provider via config.refreshUrl/clientId/clientSecret", async () => {
const fm = mockFetchOnce({ access_token: "gen-acc", expires_in: 3600 });
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
await refreshAccessToken("cline", "gen-old", {}, console);
const body = new URLSearchParams(fm.mock.calls[0][1].body);
expect(body.get("grant_type")).toBe("refresh_token");
expect(body.get("client_id")).toBeTruthy();
});
});

View File

@@ -0,0 +1,198 @@
import { describe, it, expect } from "vitest";
import {
resolveWsModelId,
buildGetChatMessageRequest,
grpcWebFrame,
decodeCompletionChunk,
default as WindsurfExecutor,
} from "open-sse/executors/windsurf.js";
import { PROVIDERS } from "open-sse/config/providers.js";
// ─── Protobuf helpers for building expected wire bytes in tests ──────────────
function encodeVarint(value) {
const bytes = [];
let v = value >>> 0;
while (v > 0x7f) { bytes.push((v & 0x7f) | 0x80); v >>>= 7; }
bytes.push(v & 0x7f);
return new Uint8Array(bytes);
}
function encodeLenField(fieldNum, payload) {
const tag = encodeVarint((fieldNum << 3) | 2);
const len = encodeVarint(payload.length);
const out = new Uint8Array(tag.length + len.length + payload.length);
out.set(tag, 0); out.set(len, tag.length); out.set(payload, tag.length + len.length);
return out;
}
function encodeStringField(fieldNum, str) {
return encodeLenField(fieldNum, new TextEncoder().encode(str));
}
describe("windsurf MODEL_ALIAS_MAP", () => {
it("maps SWE models to snake-case wire names", () => {
expect(resolveWsModelId("swe-1.6-fast")).toBe("swe-1-6-fast");
expect(resolveWsModelId("swe-1.5")).toBe("swe-1-5");
});
it("maps Claude 4.5 to MODEL_PRIVATE_* aliases", () => {
expect(resolveWsModelId("claude-sonnet-4.5")).toBe("MODEL_PRIVATE_2");
expect(resolveWsModelId("claude-opus-4.5")).toBe("MODEL_CLAUDE_4_5_OPUS");
});
it("applies default effort level for bare gpt-5.x ids", () => {
expect(resolveWsModelId("gpt-5.5")).toBe("gpt-5-5-medium");
expect(resolveWsModelId("gpt-5.4")).toBe("gpt-5-4-medium");
});
it("passes through unknown ids as-is", () => {
expect(resolveWsModelId("custom-model")).toBe("custom-model");
});
});
describe("grpcWebFrame", () => {
it("prepends a 5-byte header: 0x00 flag + big-endian length", () => {
const payload = new Uint8Array([1, 2, 3, 4, 5]);
const frame = grpcWebFrame(payload);
expect(frame[0]).toBe(0x00);
const view = new DataView(frame.buffer);
expect(view.getUint32(1, false)).toBe(5); // big-endian length
expect(Array.from(frame.slice(5))).toEqual([1, 2, 3, 4, 5]);
});
it("encodes empty payload as a 5-byte frame", () => {
const frame = grpcWebFrame(new Uint8Array(0));
expect(frame.length).toBe(5);
expect(frame[0]).toBe(0x00);
});
});
describe("buildGetChatMessageRequest", () => {
it("emits metadata (field 1), cascade_id (2), model (3), messages (4+)", () => {
const payload = buildGetChatMessageRequest("sk-ws-test", "swe-1.6", [
{ role: "user", content: "hello" },
]);
expect(payload.length).toBeGreaterThan(10);
// First byte 0x0a = field 1, wire type 2 (length-delimited) → metadata present
expect(payload[0]).toBe(0x0a);
});
it("embeds the apiKey inside the metadata sub-message", () => {
const payload = buildGetChatMessageRequest("sk-ws-secret", "gpt-5", []);
// The metadata bytes are the first length-delimited field — should contain the key.
const asString = new TextDecoder().decode(payload);
expect(asString).toContain("sk-ws-secret");
// And the IDE identification fields.
expect(asString).toContain("windsurf");
expect(asString).toContain("3.14.0");
});
it("appends one field-4 message per chat message", () => {
// Proper top-level protobuf field counter (byte 0x22 collides with content bytes).
const countField = (buf, target) => {
let offset = 0;
let count = 0;
while (offset < buf.length) {
let result = 0, shift = 0;
while (offset < buf.length) {
const b = buf[offset++];
result |= (b & 0x7f) << shift;
if ((b & 0x80) === 0) break;
shift += 7;
}
const fieldNum = result >>> 3;
const wireType = result & 0x07;
if (wireType === 2) {
let len = 0, ls = 0;
while (offset < buf.length) {
const b = buf[offset++];
len |= (b & 0x7f) << ls;
if ((b & 0x80) === 0) break;
ls += 7;
}
if (fieldNum === target) count++;
offset += len;
} else if (wireType === 0) {
while (offset < buf.length) {
const b = buf[offset++];
if ((b & 0x80) === 0) break;
}
} else if (wireType === 1) {
offset += 8;
} else if (wireType === 5) {
offset += 4;
} else {
break;
}
}
return count;
};
const one = buildGetChatMessageRequest("k", "m", [{ role: "user", content: "a" }]);
const two = buildGetChatMessageRequest("k", "m", [
{ role: "user", content: "a" },
{ role: "assistant", content: "b" },
]);
expect(countField(one, 4)).toBe(1);
expect(countField(two, 4)).toBe(2);
});
});
describe("decodeCompletionChunk", () => {
it("decodes a ContentChunk (field 1 → text)", () => {
const chunk = encodeLenField(1, encodeStringField(1, "hello world"));
const decoded = decodeCompletionChunk(chunk);
expect(decoded).toEqual({ kind: "content", text: "hello world" });
});
it("decodes an ErrorChunk (field 4 → message)", () => {
const chunk = encodeLenField(4, encodeStringField(1, "quota exhausted"));
const decoded = decodeCompletionChunk(chunk);
expect(decoded).toEqual({ kind: "error", message: "quota exhausted" });
});
it("decodes a DoneChunk (field 3 → UsageStats with prompt/completion tokens)", () => {
// UsageStats: field 1 = prompt_tokens (varint), field 2 = completion_tokens (varint)
const usage = new Uint8Array([...encodeVarint((1 << 3) | 0), ...encodeVarint(42), ...encodeVarint((2 << 3) | 0), ...encodeVarint(99)]);
const doneChunk = encodeLenField(3, encodeLenField(1, usage));
const decoded = decodeCompletionChunk(doneChunk);
expect(decoded.kind).toBe("done");
expect(decoded.promptTokens).toBe(42);
expect(decoded.completionTokens).toBe(99);
});
it("returns { kind: 'unknown' } for empty buffer", () => {
expect(decodeCompletionChunk(new Uint8Array(0))).toEqual({ kind: "unknown" });
});
});
describe("WindsurfExecutor class", () => {
it("constructor wires config from PROVIDERS.windsurf", () => {
const ex = new WindsurfExecutor();
expect(ex.provider).toBe("windsurf");
expect(ex.config).toBeDefined();
expect(ex.config.baseUrl).toContain("server.self-serve.windsurf.com");
expect(typeof ex.execute).toBe("function");
});
it("buildHeaders emits grpc-web+proto + Bearer token", () => {
const ex = new WindsurfExecutor();
const h = ex.buildHeaders({ accessToken: "sk-ws-abc" });
expect(h["Content-Type"]).toBe("application/grpc-web+proto");
expect(h.Accept).toBe("application/grpc-web+proto");
expect(h["X-Grpc-Web"]).toBe("1");
expect(h.Authorization).toBe("Bearer sk-ws-abc");
expect(h["User-Agent"]).toMatch(/^windsurf\//);
});
it("buildHeaders omits Authorization when no token", () => {
const ex = new WindsurfExecutor();
const h = ex.buildHeaders({});
expect(h.Authorization).toBeUndefined();
});
it("buildUrl returns the GetChatMessage endpoint", () => {
const ex = new WindsurfExecutor();
expect(ex.buildUrl()).toBe("https://server.self-serve.windsurf.com/exa.language_server_pb.LanguageServerService/GetChatMessage");
});
it("PROVIDERS.windsurf baseUrl is the chat endpoint (registry in sync)", () => {
expect(PROVIDERS.windsurf.baseUrl).toBe(
"https://server.self-serve.windsurf.com/exa.language_server_pb.LanguageServerService/GetChatMessage"
);
});
});