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:
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user