feat(clinepass): add ClinePass provider support

Register clinepass provider (OAuth + API-key) using Cline's
OpenAI-compatible API with 10 curated models, live /v1/models
resolver, refreshCline-based token refresh with workos: prefix,
and dashboard OAuth login handler.

Reference: https://github.com/jellydn/pi-clinepass-provider
Closes #2261

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
sternelee
2026-07-03 10:50:32 +07:00
committed by decolua
parent 76752a4396
commit b08751c4ea
8 changed files with 282 additions and 84 deletions

View File

@@ -232,8 +232,8 @@ export async function POST(request, { params }) {
});
}
// Cline uses authorization_code without PKCE. Kimchi returns a browser token.
const noPkceExchangeProviders = ["cline", "kimchi"];
// Cline and ClinePass use authorization_code without PKCE. Kimchi returns a browser token.
const noPkceExchangeProviders = ["cline", "clinepass", "kimchi"];
if (!code || !redirectUri || (!codeVerifier && !noPkceExchangeProviders.includes(provider))) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}

View File

@@ -11,6 +11,7 @@ import { resolveKiroModels } from "open-sse/services/kiroModels.js";
import { resolveKimchiModels } from "open-sse/services/kimchiModels.js";
import { resolveQoderModels } from "open-sse/services/qoderModels.js";
import { resolveCopilotModels } from "open-sse/services/copilotModels.js";
import { resolveClinepassModels } from "open-sse/services/clinepassModels.js";
import { updateProviderCredentials } from "@/sse/services/tokenRefresh";
import { capabilitiesFromServiceKind } from "open-sse/providers/capabilities.js";
@@ -63,6 +64,13 @@ const LIVE_MODEL_RESOLVERS = {
},
});
return result?.models?.length ? { models: result.models } : null;
},
clinepass: async (conn) => {
const result = await resolveClinepassModels({
accessToken: conn.accessToken,
apiKey: conn.apiKey,
});
return result?.models?.length ? { models: result.models } : null;
}
};

View File

@@ -102,6 +102,9 @@ export const KILOCODE_CONFIG = { ...PROVIDER_OAUTH["kilocode"] };
// Cline OAuth Configuration (Local Callback Flow via app.cline.bot)
export const CLINE_CONFIG = { ...PROVIDER_OAUTH["cline"] };
// ClinePass OAuth Configuration (shares Cline's OAuth endpoints)
export const CLINEPASS_CONFIG = { ...PROVIDER_OAUTH["clinepass"] };
// GitLab Duo OAuth Configuration (Authorization Code Flow with PKCE)
export const GITLAB_CONFIG = { ...PROVIDER_OAUTH["gitlab"] };
@@ -130,6 +133,7 @@ export const PROVIDERS = {
KIMI_CODING: "kimi-coding",
KILOCODE: "kilocode",
CLINE: "cline",
CLINEPASS: "clinepass",
GITLAB: "gitlab",
CODEBUDDY: "codebuddy-cn",
KIMCHI: "kimchi",

View File

@@ -23,6 +23,7 @@ import {
KIMI_CODING_CONFIG,
KILOCODE_CONFIG,
CLINE_CONFIG,
CLINEPASS_CONFIG,
GITLAB_CONFIG,
CODEBUDDY_CONFIG,
KIMCHI_CONFIG,
@@ -1116,6 +1117,64 @@ const PROVIDERS = {
providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName },
}),
},
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 },
}),
},
// GitLab Duo - Authorization Code Flow with PKCE
// Supports two login modes via loginMode metadata: "oauth" (default) or "pat"
gitlab: {