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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -8,6 +8,8 @@ import {
KIRO_CONFIG, KIRO_CONFIG,
QWEN_CONFIG, QWEN_CONFIG,
CLAUDE_CONFIG, CLAUDE_CONFIG,
CLINE_CONFIG,
KILOCODE_CONFIG,
} from "@/lib/oauth/constants/oauth"; } from "@/lib/oauth/constants/oauth";
// OAuth provider test endpoints // OAuth provider test endpoints
@ -44,8 +46,35 @@ const OAUTH_TEST_CONFIG = {
qwen: { checkExpiry: true, refreshable: true }, qwen: { checkExpiry: true, refreshable: true },
kiro: { checkExpiry: true, refreshable: true }, kiro: { checkExpiry: true, refreshable: true },
cursor: { tokenExists: 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) { async function refreshOAuthToken(connection) {
const provider = connection.provider; const provider = connection.provider;
const refreshToken = connection.refreshToken; 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 }; 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; return null;
} catch (err) { } catch (err) {
console.log(`Error refreshing ${provider} token:`, err.message); 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 }; 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 { try {
const testUrl = config.buildUrl ? config.buildUrl(accessToken) : config.url; const testUrl = config.buildUrl ? config.buildUrl(accessToken) : config.url;
const headers = config.noAuth const headers = config.noAuth