fix(oauth): refresh Cline tokens with extension JSON contract
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
|||||||
refreshXaiToken,
|
refreshXaiToken,
|
||||||
refreshAccessToken,
|
refreshAccessToken,
|
||||||
refreshKimiToken,
|
refreshKimiToken,
|
||||||
|
refreshClineToken,
|
||||||
refreshClaudeOAuthToken,
|
refreshClaudeOAuthToken,
|
||||||
refreshGoogleToken,
|
refreshGoogleToken,
|
||||||
refreshCodexToken,
|
refreshCodexToken,
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
export {
|
export {
|
||||||
refreshAccessToken,
|
refreshAccessToken,
|
||||||
refreshKimiToken,
|
refreshKimiToken,
|
||||||
|
refreshClineToken,
|
||||||
refreshClaudeOAuthToken,
|
refreshClaudeOAuthToken,
|
||||||
refreshGoogleToken,
|
refreshGoogleToken,
|
||||||
refreshCodexToken,
|
refreshCodexToken,
|
||||||
@@ -145,6 +147,7 @@ const REFRESH_HANDLERS = {
|
|||||||
"codebuddy-cn": (c, log) => refreshCodebuddyToken(c.refreshToken, log),
|
"codebuddy-cn": (c, log) => refreshCodebuddyToken(c.refreshToken, log),
|
||||||
"codebuddy-intl": (c, log) => refreshCodebuddyIntlToken(c.refreshToken, log),
|
"codebuddy-intl": (c, log) => refreshCodebuddyIntlToken(c.refreshToken, log),
|
||||||
trae: (c, log) => refreshTraeToken(c.refreshToken, c, log),
|
trae: (c, log) => refreshTraeToken(c.refreshToken, c, log),
|
||||||
|
cline: (c, log) => refreshClineToken(c.refreshToken, log),
|
||||||
zed: () => refreshZedToken(),
|
zed: () => refreshZedToken(),
|
||||||
windsurf: (c, log) => refreshWindsurfToken(c, log),
|
windsurf: (c, log) => refreshWindsurfToken(c, log),
|
||||||
// Kimi Code OAuth (merged into id `kimi`); legacy id still routes here
|
// Kimi Code OAuth (merged into id `kimi`); legacy id still routes here
|
||||||
|
|||||||
@@ -147,6 +147,53 @@ export async function refreshKimiToken(refreshToken, credentials, log) {
|
|||||||
return refreshAccessToken("kimi", refreshToken, credentials, log);
|
return refreshAccessToken("kimi", refreshToken, credentials, log);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function refreshClineToken(refreshToken, log) {
|
||||||
|
if (!refreshToken) return null;
|
||||||
|
|
||||||
|
return dedupRefresh("cline", refreshToken, async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(PROVIDERS.cline?.refreshUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
refreshToken,
|
||||||
|
grantType: "refresh_token",
|
||||||
|
clientType: "extension",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
log?.error?.("TOKEN_REFRESH", "Failed to refresh Cline token", {
|
||||||
|
status: response.status,
|
||||||
|
error: errorText,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
const tokens = body?.data || body;
|
||||||
|
if (!tokens?.accessToken) return null;
|
||||||
|
|
||||||
|
const expiresIn = tokens.expiresAt
|
||||||
|
? Math.max(1, Math.floor((new Date(tokens.expiresAt).getTime() - Date.now()) / 1000))
|
||||||
|
: (tokens.expiresIn || tokens.expires_in || 3600);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: tokens.accessToken,
|
||||||
|
refreshToken: tokens.refreshToken || refreshToken,
|
||||||
|
expiresIn,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log?.error?.("TOKEN_REFRESH", `Error refreshing Cline token: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, log);
|
||||||
|
}
|
||||||
|
|
||||||
// Claude OAuth: JSON body, client_id only. Delegate to refreshAccessToken("claude", ...).
|
// Claude OAuth: JSON body, client_id only. Delegate to refreshAccessToken("claude", ...).
|
||||||
export async function refreshClaudeOAuthToken(refreshToken, log) {
|
export async function refreshClaudeOAuthToken(refreshToken, log) {
|
||||||
return refreshAccessToken("claude", refreshToken, {}, log);
|
return refreshAccessToken("claude", refreshToken, {}, log);
|
||||||
|
|||||||
@@ -102,19 +102,38 @@ describe("refreshAccessToken — config-driven profiles", () => {
|
|||||||
expect(fm).toHaveBeenCalledTimes(1);
|
expect(fm).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("Cline refresh", () => {
|
||||||
describe("refreshAccessToken — legacy generic path (no profile)", () => {
|
|
||||||
beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); global.fetch = originalFetch; });
|
beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); global.fetch = originalFetch; });
|
||||||
afterEach(() => { global.fetch = originalFetch; });
|
afterEach(() => { global.fetch = originalFetch; });
|
||||||
|
|
||||||
it("still works for an unprofiled provider via config.refreshUrl/clientId/clientSecret", async () => {
|
it("uses the extension JSON refresh contract", async () => {
|
||||||
const fm = mockFetchOnce({ access_token: "gen-acc", expires_in: 3600 });
|
const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
|
||||||
const { refreshAccessToken } = await import("open-sse/services/tokenRefresh/providers.js");
|
const fm = mockFetchOnce({
|
||||||
|
data: {
|
||||||
|
accessToken: "cline-acc",
|
||||||
|
refreshToken: "cline-rot",
|
||||||
|
expiresAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { refreshTokenByProvider } = await import(
|
||||||
|
"open-sse/services/tokenRefresh.js"
|
||||||
|
);
|
||||||
|
|
||||||
await refreshAccessToken("cline", "gen-old", {}, console);
|
const out = await refreshTokenByProvider(
|
||||||
|
"cline",
|
||||||
|
{ refreshToken: "cline-old" },
|
||||||
|
console
|
||||||
|
);
|
||||||
|
|
||||||
const body = new URLSearchParams(fm.mock.calls[0][1].body);
|
const [, init] = fm.mock.calls[0];
|
||||||
expect(body.get("grant_type")).toBe("refresh_token");
|
expect(init.headers["Content-Type"]).toBe("application/json");
|
||||||
expect(body.get("client_id")).toBeTruthy();
|
expect(JSON.parse(init.body)).toEqual({
|
||||||
|
refreshToken: "cline-old",
|
||||||
|
grantType: "refresh_token",
|
||||||
|
clientType: "extension",
|
||||||
|
});
|
||||||
|
expect(out.accessToken).toBe("cline-acc");
|
||||||
|
expect(out.refreshToken).toBe("cline-rot");
|
||||||
|
expect(out.expiresIn).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user