170 lines
4.5 KiB
JavaScript
170 lines
4.5 KiB
JavaScript
import open from "open";
|
|
import { QWEN_CONFIG } from "../constants/oauth.js";
|
|
import { getServerCredentials } from "../config/index.js";
|
|
import { generatePKCE } from "../utils/pkce.js";
|
|
import { spinner as createSpinner } from "../utils/ui.js";
|
|
|
|
/**
|
|
* Qwen OAuth Service
|
|
* Uses Device Code Flow with PKCE
|
|
*/
|
|
export class QwenService {
|
|
constructor() {
|
|
this.config = QWEN_CONFIG;
|
|
}
|
|
|
|
/**
|
|
* Request device code
|
|
*/
|
|
async requestDeviceCode(codeChallenge) {
|
|
const response = await fetch(this.config.deviceCodeUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
Accept: "application/json",
|
|
},
|
|
body: new URLSearchParams({
|
|
client_id: this.config.clientId,
|
|
scope: this.config.scope,
|
|
code_challenge: codeChallenge,
|
|
code_challenge_method: this.config.codeChallengeMethod,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
throw new Error(`Device code request failed: ${error}`);
|
|
}
|
|
|
|
return await response.json();
|
|
}
|
|
|
|
/**
|
|
* Poll for token
|
|
*/
|
|
async pollForToken(deviceCode, codeVerifier, interval = 5) {
|
|
const maxAttempts = 60; // 5 minutes
|
|
const pollInterval = interval * 1000;
|
|
|
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
await new Promise((r) => setTimeout(r, pollInterval));
|
|
|
|
const response = await fetch(this.config.tokenUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
Accept: "application/json",
|
|
},
|
|
body: new URLSearchParams({
|
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
client_id: this.config.clientId,
|
|
device_code: deviceCode,
|
|
code_verifier: codeVerifier,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
return await response.json();
|
|
}
|
|
|
|
const error = await response.json();
|
|
|
|
if (error.error === "authorization_pending") {
|
|
continue;
|
|
} else if (error.error === "slow_down") {
|
|
await new Promise((r) => setTimeout(r, 5000));
|
|
continue;
|
|
} else if (error.error === "expired_token") {
|
|
throw new Error("Device code expired");
|
|
} else if (error.error === "access_denied") {
|
|
throw new Error("Access denied");
|
|
} else {
|
|
throw new Error(error.error_description || error.error);
|
|
}
|
|
}
|
|
|
|
throw new Error("Authorization timeout");
|
|
}
|
|
|
|
/**
|
|
* Save Qwen tokens to server
|
|
*/
|
|
async saveTokens(tokens) {
|
|
const { server, token, userId } = getServerCredentials();
|
|
|
|
const response = await fetch(`${server}/api/cli/providers/qwen`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
"X-User-Id": userId,
|
|
},
|
|
body: JSON.stringify({
|
|
accessToken: tokens.access_token,
|
|
refreshToken: tokens.refresh_token,
|
|
expiresIn: tokens.expires_in,
|
|
resourceUrl: tokens.resource_url,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Failed to save tokens");
|
|
}
|
|
|
|
return await response.json();
|
|
}
|
|
|
|
/**
|
|
* Complete Qwen OAuth flow
|
|
*/
|
|
async connect() {
|
|
const spinner = createSpinner("Starting Qwen OAuth...").start();
|
|
|
|
try {
|
|
spinner.text = "Generating PKCE...";
|
|
|
|
// Generate PKCE
|
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
|
|
spinner.text = "Requesting device code...";
|
|
|
|
// Request device code
|
|
const deviceData = await this.requestDeviceCode(codeChallenge);
|
|
|
|
spinner.stop();
|
|
|
|
console.log("\n📋 Please visit the following URL and enter the code:\n");
|
|
console.log(` ${deviceData.verification_uri}\n`);
|
|
console.log(` Code: ${deviceData.user_code}\n`);
|
|
|
|
// Open browser
|
|
if (deviceData.verification_uri_complete) {
|
|
await open(deviceData.verification_uri_complete);
|
|
} else {
|
|
await open(deviceData.verification_uri);
|
|
}
|
|
|
|
spinner.start("Waiting for authorization...");
|
|
|
|
// Poll for token
|
|
const tokens = await this.pollForToken(
|
|
deviceData.device_code,
|
|
codeVerifier,
|
|
deviceData.interval || 5
|
|
);
|
|
|
|
spinner.text = "Saving tokens to server...";
|
|
|
|
// Save tokens to server
|
|
await this.saveTokens(tokens);
|
|
|
|
spinner.succeed("Qwen connected successfully!");
|
|
return true;
|
|
} catch (error) {
|
|
spinner.fail(`Failed: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|