fix(test): support cline and kilocode oauth checks (#258)

This commit is contained in:
Andrew Peltekci
2026-03-06 20:10:53 -08:00
committed by GitHub
parent 758224749d
commit 4c469291a1

View File

@@ -8,6 +8,8 @@ import {
KIRO_CONFIG,
QWEN_CONFIG,
CLAUDE_CONFIG,
CLINE_CONFIG,
KILOCODE_CONFIG,
} from "@/lib/oauth/constants/oauth";
// OAuth provider test endpoints
@@ -44,8 +46,35 @@ const OAUTH_TEST_CONFIG = {
qwen: { checkExpiry: true, refreshable: true },
kiro: { checkExpiry: true, refreshable: true },
cursor: { tokenExists: true },
kilocode: {
url: `${KILOCODE_CONFIG.apiBaseUrl}/api/profile`,
method: "GET",
authHeader: "Authorization",
authPrefix: "Bearer ",
},
cline: { refreshable: true },
};
async function probeClineAccessToken(accessToken) {
const res = await fetch("https://api.cline.bot/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
body: JSON.stringify({
model: "cl/anthropic/claude-sonnet-4-20250514",
messages: [{ role: "user", content: "test" }],
max_tokens: 1,
stream: false,
}),
});
return res;
}
async function refreshOAuthToken(connection) {
const provider = connection.provider;
const refreshToken = connection.refreshToken;
@@ -140,6 +169,29 @@ async function refreshOAuthToken(connection) {
return { accessToken: data.access_token, expiresIn: data.expires_in, refreshToken: data.refresh_token || refreshToken };
}
if (provider === "cline") {
const response = await fetch(CLINE_CONFIG.refreshUrl, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
refreshToken,
grantType: "refresh_token",
clientType: "extension",
}),
});
if (!response.ok) return null;
const payload = await response.json();
const data = payload?.data || payload;
const expiresIn = data?.expiresAt
? Math.max(1, Math.floor((new Date(data.expiresAt).getTime() - Date.now()) / 1000))
: 3600;
return {
accessToken: data?.accessToken,
expiresIn,
refreshToken: data?.refreshToken || refreshToken,
};
}
return null;
} catch (err) {
console.log(`Error refreshing ${provider} token:`, err.message);
@@ -186,6 +238,31 @@ async function testOAuthConnection(connection) {
return { valid: true, error: null, refreshed: false, newTokens: null };
}
if (connection.provider === "cline") {
const tryProbe = async (token) => {
const res = await probeClineAccessToken(token);
if (res.ok) return { valid: true, error: null, refreshed, newTokens };
if (res.status === 401) return { valid: false, error: "Token invalid or revoked", refreshed };
if (res.status === 403) return { valid: false, error: "Access denied", refreshed };
return { valid: false, error: `API returned ${res.status}`, refreshed };
};
const initial = await tryProbe(accessToken);
if (initial.valid || initial.error !== "Token invalid or revoked" || !connection.refreshToken) {
return initial;
}
const tokens = await refreshOAuthToken(connection);
if (!tokens?.accessToken) {
return { valid: false, error: "Token invalid or revoked", refreshed: false };
}
refreshed = true;
newTokens = tokens;
accessToken = tokens.accessToken;
return await tryProbe(accessToken);
}
try {
const testUrl = config.buildUrl ? config.buildUrl(accessToken) : config.url;
const headers = config.noAuth