import type { OAuthCredentials, OAuthLoginCallbacks, } from "@earendil-works/pi-ai/oauth"; const API_BASE_URL = "https://api.cline.bot"; const WORKOS_API_BASE_URL = "https://api.workos.com"; const WORKOS_CLIENT_ID = "client_01K3A541FN8TA3EPPHTD2325AR"; const REFRESH_BUFFER_MS = 5 * 60 * 1000; const WORKOS_TOKEN_PREFIX = "workos:"; type ClineAuthResponse = { success?: boolean; data?: { accessToken?: string; refreshToken?: string; expiresAt?: string }; }; type WorkOSAuthResponse = { device_code?: string; user_code?: string; verification_uri?: string; verification_uri_complete?: string; expires_in?: number; interval?: number; error?: string; error_description?: string; }; type WorkOSTokenResponse = { access_token?: string; refresh_token?: string; error?: string; error_description?: string; }; function clineUrl(path: string): string { return new URL(path, API_BASE_URL).toString(); } function authHeaders(contentType = "application/json"): Record { return { Accept: "application/json", "Content-Type": contentType, "User-Agent": "cline-pi/0.1.2", "X-CLIENT-TYPE": "pi", }; } async function readError(response: Response): Promise { const text = await response.text().catch(() => ""); if (!text) return `${response.status} ${response.statusText}`; try { const json = JSON.parse(text) as { error?: string; error_description?: string; message?: string; }; return json.error_description ?? json.message ?? json.error ?? text; } catch { return text; } } function toCredentials( payload: ClineAuthResponse, fallback?: OAuthCredentials, ): OAuthCredentials { const data = payload.data; if (!payload.success || !data?.accessToken || !data.expiresAt) { throw new Error("Invalid token response from Cline"); } const refreshToken = data.refreshToken ?? fallback?.refresh; if (!refreshToken) { throw new Error("Token response did not include a refresh token"); } const expires = Date.parse(data.expiresAt); if (Number.isNaN(expires)) { throw new Error(`Invalid token expiration: ${data.expiresAt}`); } return { access: data.accessToken, refresh: refreshToken, expires: expires - REFRESH_BUFFER_MS, }; } async function startDeviceAuthorization(): Promise<{ deviceCode: string; userCode: string; verificationUri: string; verificationUriComplete?: string; expiresInSeconds: number; intervalSeconds: number; }> { const response = await fetch( `${WORKOS_API_BASE_URL}/user_management/authorize/device`, { method: "POST", headers: authHeaders("application/x-www-form-urlencoded"), body: new URLSearchParams({ client_id: WORKOS_CLIENT_ID }), }, ); const data = (await response.json().catch(() => ({}))) as WorkOSAuthResponse; if ( !response.ok || !data.device_code || !data.user_code || !data.verification_uri ) { throw new Error( `Device authorization failed: ${data.error_description ?? data.error ?? response.statusText}`, ); } return { deviceCode: data.device_code, userCode: data.user_code, verificationUri: data.verification_uri, verificationUriComplete: data.verification_uri_complete, expiresInSeconds: data.expires_in ?? 300, intervalSeconds: data.interval ?? 5, }; } async function pollDeviceAuthorization(params: { deviceCode: string; expiresInSeconds: number; intervalSeconds: number; }): Promise<{ accessToken: string; refreshToken: string }> { const deadline = Date.now() + params.expiresInSeconds * 1000; let intervalMs = Math.max(1, params.intervalSeconds) * 1000; while (Date.now() <= deadline) { const response = await fetch( `${WORKOS_API_BASE_URL}/user_management/authenticate`, { method: "POST", headers: authHeaders("application/x-www-form-urlencoded"), body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: params.deviceCode, client_id: WORKOS_CLIENT_ID, }), }, ); const data = (await response .json() .catch(() => ({}))) as WorkOSTokenResponse; if (response.ok && data.access_token && data.refresh_token) { return { accessToken: data.access_token, refreshToken: data.refresh_token }; } if (data.error === "authorization_pending") { await new Promise((r) => setTimeout(r, intervalMs)); continue; } if (data.error === "slow_down") { intervalMs += 1000; await new Promise((r) => setTimeout(r, intervalMs)); continue; } throw new Error( `Device authorization failed: ${data.error_description ?? data.error ?? response.statusText}`, ); } throw new Error("Device authorization timed out"); } async function registerWorkOSTokens(tokens: { accessToken: string; refreshToken: string; }): Promise { const response = await fetch(clineUrl("/api/v1/auth/register"), { method: "POST", headers: authHeaders(), body: JSON.stringify(tokens), }); if (!response.ok) { throw new Error(`Token registration failed: ${await readError(response)}`); } return toCredentials((await response.json()) as ClineAuthResponse); } export async function login( callbacks: OAuthLoginCallbacks, ): Promise { callbacks.onProgress?.("Starting Cline authentication..."); const device = await startDeviceAuthorization(); callbacks.onDeviceCode({ userCode: device.userCode, verificationUri: device.verificationUri, intervalSeconds: device.intervalSeconds, expiresInSeconds: device.expiresInSeconds, }); callbacks.onAuth({ url: device.verificationUriComplete ?? device.verificationUri }); callbacks.onProgress?.("Waiting for browser authentication..."); const workosTokens = await pollDeviceAuthorization({ deviceCode: device.deviceCode, expiresInSeconds: device.expiresInSeconds, intervalSeconds: device.intervalSeconds, }); callbacks.onProgress?.("Completing Cline authentication..."); const credentials = await registerWorkOSTokens(workosTokens); callbacks.onProgress?.("Authentication complete."); return credentials; } export async function refreshToken( credentials: OAuthCredentials, ): Promise { const response = await fetch(clineUrl("/api/v1/auth/refresh"), { method: "POST", headers: authHeaders(), body: JSON.stringify({ refreshToken: credentials.refresh, grantType: "refresh_token", }), }); if (!response.ok) { throw new Error(`Token refresh failed: ${await readError(response)}`); } return toCredentials( (await response.json()) as ClineAuthResponse, credentials, ); } export function getApiKey(credentials: OAuthCredentials): string { const token = credentials.access; return token.startsWith(WORKOS_TOKEN_PREFIX) ? token : `${WORKOS_TOKEN_PREFIX}${token}`; }