620 lines
19 KiB
JavaScript
620 lines
19 KiB
JavaScript
|
|
/**
|
||
|
|
* OAuth Provider Configurations and Handlers
|
||
|
|
* Centralized DRY approach for all OAuth providers
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { generatePKCE, generateState } from "./utils/pkce";
|
||
|
|
import {
|
||
|
|
CLAUDE_CONFIG,
|
||
|
|
CODEX_CONFIG,
|
||
|
|
GEMINI_CONFIG,
|
||
|
|
QWEN_CONFIG,
|
||
|
|
IFLOW_CONFIG,
|
||
|
|
ANTIGRAVITY_CONFIG,
|
||
|
|
GITHUB_CONFIG,
|
||
|
|
} from "./constants/oauth";
|
||
|
|
|
||
|
|
// Provider configurations
|
||
|
|
const PROVIDERS = {
|
||
|
|
claude: {
|
||
|
|
config: CLAUDE_CONFIG,
|
||
|
|
flowType: "authorization_code_pkce",
|
||
|
|
buildAuthUrl: (config, redirectUri, state, codeChallenge) => {
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
code: "true",
|
||
|
|
client_id: config.clientId,
|
||
|
|
response_type: "code",
|
||
|
|
redirect_uri: redirectUri,
|
||
|
|
scope: config.scopes.join(" "),
|
||
|
|
code_challenge: codeChallenge,
|
||
|
|
code_challenge_method: config.codeChallengeMethod,
|
||
|
|
state: state,
|
||
|
|
});
|
||
|
|
return `${config.authorizeUrl}?${params.toString()}`;
|
||
|
|
},
|
||
|
|
exchangeToken: async (config, code, redirectUri, codeVerifier, state) => {
|
||
|
|
// Parse code - may contain state after #
|
||
|
|
let authCode = code;
|
||
|
|
let codeState = "";
|
||
|
|
if (authCode.includes("#")) {
|
||
|
|
const parts = authCode.split("#");
|
||
|
|
authCode = parts[0];
|
||
|
|
codeState = parts[1] || "";
|
||
|
|
}
|
||
|
|
|
||
|
|
const response = await fetch(config.tokenUrl, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
Accept: "application/json",
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
code: authCode,
|
||
|
|
state: codeState || state,
|
||
|
|
grant_type: "authorization_code",
|
||
|
|
client_id: config.clientId,
|
||
|
|
redirect_uri: redirectUri,
|
||
|
|
code_verifier: codeVerifier,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const error = await response.text();
|
||
|
|
throw new Error(`Token exchange failed: ${error}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return await response.json();
|
||
|
|
},
|
||
|
|
mapTokens: (tokens) => ({
|
||
|
|
accessToken: tokens.access_token,
|
||
|
|
refreshToken: tokens.refresh_token,
|
||
|
|
expiresIn: tokens.expires_in,
|
||
|
|
scope: tokens.scope,
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
|
||
|
|
codex: {
|
||
|
|
config: CODEX_CONFIG,
|
||
|
|
flowType: "authorization_code_pkce",
|
||
|
|
fixedPort: 1455,
|
||
|
|
callbackPath: "/auth/callback",
|
||
|
|
buildAuthUrl: (config, redirectUri, state, codeChallenge) => {
|
||
|
|
const params = {
|
||
|
|
response_type: "code",
|
||
|
|
client_id: config.clientId,
|
||
|
|
redirect_uri: redirectUri,
|
||
|
|
scope: config.scope,
|
||
|
|
code_challenge: codeChallenge,
|
||
|
|
code_challenge_method: config.codeChallengeMethod,
|
||
|
|
...config.extraParams,
|
||
|
|
state: state,
|
||
|
|
};
|
||
|
|
const queryString = Object.entries(params)
|
||
|
|
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||
|
|
.join("&");
|
||
|
|
return `${config.authorizeUrl}?${queryString}`;
|
||
|
|
},
|
||
|
|
exchangeToken: async (config, code, redirectUri, codeVerifier) => {
|
||
|
|
const response = await fetch(config.tokenUrl, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
||
|
|
Accept: "application/json",
|
||
|
|
},
|
||
|
|
body: new URLSearchParams({
|
||
|
|
grant_type: "authorization_code",
|
||
|
|
client_id: config.clientId,
|
||
|
|
code: code,
|
||
|
|
redirect_uri: redirectUri,
|
||
|
|
code_verifier: codeVerifier,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const error = await response.text();
|
||
|
|
throw new Error(`Token exchange failed: ${error}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return await response.json();
|
||
|
|
},
|
||
|
|
mapTokens: (tokens) => ({
|
||
|
|
accessToken: tokens.access_token,
|
||
|
|
refreshToken: tokens.refresh_token,
|
||
|
|
idToken: tokens.id_token,
|
||
|
|
expiresIn: tokens.expires_in,
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
|
||
|
|
"gemini-cli": {
|
||
|
|
config: GEMINI_CONFIG,
|
||
|
|
flowType: "authorization_code",
|
||
|
|
buildAuthUrl: (config, redirectUri, state) => {
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
client_id: config.clientId,
|
||
|
|
response_type: "code",
|
||
|
|
redirect_uri: redirectUri,
|
||
|
|
scope: config.scopes.join(" "),
|
||
|
|
state: state,
|
||
|
|
access_type: "offline",
|
||
|
|
prompt: "consent",
|
||
|
|
});
|
||
|
|
return `${config.authorizeUrl}?${params.toString()}`;
|
||
|
|
},
|
||
|
|
exchangeToken: async (config, code, redirectUri) => {
|
||
|
|
const response = await fetch(config.tokenUrl, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
||
|
|
Accept: "application/json",
|
||
|
|
},
|
||
|
|
body: new URLSearchParams({
|
||
|
|
grant_type: "authorization_code",
|
||
|
|
client_id: config.clientId,
|
||
|
|
client_secret: config.clientSecret,
|
||
|
|
code: code,
|
||
|
|
redirect_uri: redirectUri,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const error = await response.text();
|
||
|
|
throw new Error(`Token exchange failed: ${error}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return await response.json();
|
||
|
|
},
|
||
|
|
postExchange: async (tokens) => {
|
||
|
|
// Fetch user info
|
||
|
|
const userInfoRes = await fetch(`${GEMINI_CONFIG.userInfoUrl}?alt=json`, {
|
||
|
|
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
||
|
|
});
|
||
|
|
const userInfo = userInfoRes.ok ? await userInfoRes.json() : {};
|
||
|
|
|
||
|
|
// Fetch project ID
|
||
|
|
let projectId = "";
|
||
|
|
try {
|
||
|
|
const projectRes = await fetch(
|
||
|
|
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
||
|
|
{
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${tokens.access_token}`,
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
metadata: { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" },
|
||
|
|
}),
|
||
|
|
}
|
||
|
|
);
|
||
|
|
if (projectRes.ok) {
|
||
|
|
const data = await projectRes.json();
|
||
|
|
projectId = data.cloudaicompanionProject?.id || data.cloudaicompanionProject || "";
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.log("Failed to fetch project ID:", e);
|
||
|
|
}
|
||
|
|
|
||
|
|
return { userInfo, projectId };
|
||
|
|
},
|
||
|
|
mapTokens: (tokens, extra) => ({
|
||
|
|
accessToken: tokens.access_token,
|
||
|
|
refreshToken: tokens.refresh_token,
|
||
|
|
expiresIn: tokens.expires_in,
|
||
|
|
scope: tokens.scope,
|
||
|
|
email: extra?.userInfo?.email,
|
||
|
|
projectId: extra?.projectId,
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
|
||
|
|
antigravity: {
|
||
|
|
config: ANTIGRAVITY_CONFIG,
|
||
|
|
flowType: "authorization_code",
|
||
|
|
buildAuthUrl: (config, redirectUri, state) => {
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
client_id: config.clientId,
|
||
|
|
response_type: "code",
|
||
|
|
redirect_uri: redirectUri,
|
||
|
|
scope: config.scopes.join(" "),
|
||
|
|
state: state,
|
||
|
|
access_type: "offline",
|
||
|
|
prompt: "consent",
|
||
|
|
});
|
||
|
|
return `${config.authorizeUrl}?${params.toString()}`;
|
||
|
|
},
|
||
|
|
exchangeToken: async (config, code, redirectUri) => {
|
||
|
|
const response = await fetch(config.tokenUrl, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
||
|
|
Accept: "application/json",
|
||
|
|
},
|
||
|
|
body: new URLSearchParams({
|
||
|
|
grant_type: "authorization_code",
|
||
|
|
client_id: config.clientId,
|
||
|
|
client_secret: config.clientSecret,
|
||
|
|
code: code,
|
||
|
|
redirect_uri: redirectUri,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const error = await response.text();
|
||
|
|
throw new Error(`Token exchange failed: ${error}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return await response.json();
|
||
|
|
},
|
||
|
|
postExchange: async (tokens) => {
|
||
|
|
// Fetch user info
|
||
|
|
const userInfoRes = await fetch(`${ANTIGRAVITY_CONFIG.userInfoUrl}?alt=json`, {
|
||
|
|
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
||
|
|
});
|
||
|
|
const userInfo = userInfoRes.ok ? await userInfoRes.json() : {};
|
||
|
|
|
||
|
|
// Fetch project ID from loadCodeAssist
|
||
|
|
let projectId = "";
|
||
|
|
try {
|
||
|
|
const projectRes = await fetch(ANTIGRAVITY_CONFIG.loadCodeAssistEndpoint, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${tokens.access_token}`,
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
"User-Agent": ANTIGRAVITY_CONFIG.loadCodeAssistUserAgent,
|
||
|
|
"X-Goog-Api-Client": ANTIGRAVITY_CONFIG.loadCodeAssistApiClient,
|
||
|
|
"Client-Metadata": ANTIGRAVITY_CONFIG.loadCodeAssistClientMetadata,
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
metadata: { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" },
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
if (projectRes.ok) {
|
||
|
|
const data = await projectRes.json();
|
||
|
|
projectId = data.cloudaicompanionProject?.id || data.cloudaicompanionProject || "";
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.log("Failed to fetch project ID:", e);
|
||
|
|
}
|
||
|
|
|
||
|
|
return { userInfo, projectId };
|
||
|
|
},
|
||
|
|
mapTokens: (tokens, extra) => ({
|
||
|
|
accessToken: tokens.access_token,
|
||
|
|
refreshToken: tokens.refresh_token,
|
||
|
|
expiresIn: tokens.expires_in,
|
||
|
|
scope: tokens.scope,
|
||
|
|
email: extra?.userInfo?.email,
|
||
|
|
projectId: extra?.projectId,
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
|
||
|
|
iflow: {
|
||
|
|
config: IFLOW_CONFIG,
|
||
|
|
flowType: "authorization_code",
|
||
|
|
buildAuthUrl: (config, redirectUri, state) => {
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
loginMethod: config.extraParams.loginMethod,
|
||
|
|
type: config.extraParams.type,
|
||
|
|
redirect: redirectUri,
|
||
|
|
state: state,
|
||
|
|
client_id: config.clientId,
|
||
|
|
});
|
||
|
|
return `${config.authorizeUrl}?${params.toString()}`;
|
||
|
|
},
|
||
|
|
exchangeToken: async (config, code, redirectUri) => {
|
||
|
|
// Create Basic Auth header
|
||
|
|
const basicAuth = Buffer.from(
|
||
|
|
`${config.clientId}:${config.clientSecret}`
|
||
|
|
).toString("base64");
|
||
|
|
|
||
|
|
const response = await fetch(config.tokenUrl, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
||
|
|
Accept: "application/json",
|
||
|
|
Authorization: `Basic ${basicAuth}`,
|
||
|
|
},
|
||
|
|
body: new URLSearchParams({
|
||
|
|
grant_type: "authorization_code",
|
||
|
|
code: code,
|
||
|
|
redirect_uri: redirectUri,
|
||
|
|
client_id: config.clientId,
|
||
|
|
client_secret: config.clientSecret,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const error = await response.text();
|
||
|
|
throw new Error(`Token exchange failed: ${error}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return await response.json();
|
||
|
|
},
|
||
|
|
postExchange: async (tokens) => {
|
||
|
|
// Fetch user info
|
||
|
|
const userInfoRes = await fetch(
|
||
|
|
`${IFLOW_CONFIG.userInfoUrl}?accessToken=${encodeURIComponent(tokens.access_token)}`,
|
||
|
|
{
|
||
|
|
headers: {
|
||
|
|
Accept: "application/json",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
const result = userInfoRes.ok ? await userInfoRes.json() : {};
|
||
|
|
const userInfo = result.success ? result.data : {};
|
||
|
|
return { userInfo };
|
||
|
|
},
|
||
|
|
mapTokens: (tokens, extra) => ({
|
||
|
|
accessToken: tokens.access_token,
|
||
|
|
refreshToken: tokens.refresh_token,
|
||
|
|
expiresIn: tokens.expires_in,
|
||
|
|
apiKey: extra?.userInfo?.apiKey,
|
||
|
|
email: extra?.userInfo?.email || extra?.userInfo?.phone,
|
||
|
|
displayName: extra?.userInfo?.nickname || extra?.userInfo?.name,
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
|
||
|
|
qwen: {
|
||
|
|
config: QWEN_CONFIG,
|
||
|
|
flowType: "device_code",
|
||
|
|
requestDeviceCode: async (config, codeChallenge) => {
|
||
|
|
const response = await fetch(config.deviceCodeUrl, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
||
|
|
Accept: "application/json",
|
||
|
|
},
|
||
|
|
body: new URLSearchParams({
|
||
|
|
client_id: config.clientId,
|
||
|
|
scope: config.scope,
|
||
|
|
code_challenge: codeChallenge,
|
||
|
|
code_challenge_method: config.codeChallengeMethod,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const error = await response.text();
|
||
|
|
throw new Error(`Device code request failed: ${error}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return await response.json();
|
||
|
|
},
|
||
|
|
pollToken: async (config, deviceCode, codeVerifier) => {
|
||
|
|
const response = await fetch(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: config.clientId,
|
||
|
|
device_code: deviceCode,
|
||
|
|
code_verifier: codeVerifier,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
return {
|
||
|
|
ok: response.ok,
|
||
|
|
data: await response.json(),
|
||
|
|
};
|
||
|
|
},
|
||
|
|
mapTokens: (tokens) => ({
|
||
|
|
accessToken: tokens.access_token,
|
||
|
|
refreshToken: tokens.refresh_token,
|
||
|
|
expiresIn: tokens.expires_in,
|
||
|
|
providerSpecificData: { resourceUrl: tokens.resource_url },
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
|
||
|
|
github: {
|
||
|
|
config: GITHUB_CONFIG,
|
||
|
|
flowType: "device_code",
|
||
|
|
requestDeviceCode: async (config) => {
|
||
|
|
const response = await fetch(config.deviceCodeUrl, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
||
|
|
Accept: "application/json",
|
||
|
|
},
|
||
|
|
body: new URLSearchParams({
|
||
|
|
client_id: config.clientId,
|
||
|
|
scope: config.scopes,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const error = await response.text();
|
||
|
|
throw new Error(`Device code request failed: ${error}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return await response.json();
|
||
|
|
},
|
||
|
|
pollToken: async (config, deviceCode) => {
|
||
|
|
const response = await fetch(config.tokenUrl, {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
||
|
|
Accept: "application/json",
|
||
|
|
},
|
||
|
|
body: new URLSearchParams({
|
||
|
|
client_id: config.clientId,
|
||
|
|
device_code: deviceCode,
|
||
|
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
// Handle response properly - if not ok, try to get error as text first
|
||
|
|
let data;
|
||
|
|
try {
|
||
|
|
data = await response.json();
|
||
|
|
} catch (e) {
|
||
|
|
// If response is not JSON, get as text
|
||
|
|
const text = await response.text();
|
||
|
|
data = { error: "invalid_response", error_description: text };
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
ok: response.ok,
|
||
|
|
data: data,
|
||
|
|
};
|
||
|
|
},
|
||
|
|
postExchange: async (tokens) => {
|
||
|
|
// Get Copilot token using GitHub access token
|
||
|
|
const copilotRes = await fetch(GITHUB_CONFIG.copilotTokenUrl, {
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${tokens.access_token}`,
|
||
|
|
Accept: "application/json",
|
||
|
|
"X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion,
|
||
|
|
"User-Agent": GITHUB_CONFIG.userAgent,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const copilotToken = copilotRes.ok ? await copilotRes.json() : {};
|
||
|
|
|
||
|
|
// Get user info from GitHub
|
||
|
|
const userRes = await fetch(GITHUB_CONFIG.userInfoUrl, {
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${tokens.access_token}`,
|
||
|
|
Accept: "application/json",
|
||
|
|
"X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion,
|
||
|
|
"User-Agent": GITHUB_CONFIG.userAgent,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
const userInfo = userRes.ok ? await userRes.json() : {};
|
||
|
|
|
||
|
|
return { copilotToken, userInfo };
|
||
|
|
},
|
||
|
|
mapTokens: (tokens, extra) => ({
|
||
|
|
accessToken: tokens.access_token,
|
||
|
|
refreshToken: tokens.refresh_token,
|
||
|
|
expiresIn: tokens.expires_in,
|
||
|
|
providerSpecificData: {
|
||
|
|
copilotToken: extra?.copilotToken?.token,
|
||
|
|
copilotTokenExpiresAt: extra?.copilotToken?.expires_at,
|
||
|
|
githubUserId: extra?.userInfo?.id,
|
||
|
|
githubLogin: extra?.userInfo?.login,
|
||
|
|
githubName: extra?.userInfo?.name,
|
||
|
|
githubEmail: extra?.userInfo?.email,
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get provider handler
|
||
|
|
*/
|
||
|
|
export function getProvider(name) {
|
||
|
|
const provider = PROVIDERS[name];
|
||
|
|
if (!provider) {
|
||
|
|
throw new Error(`Unknown provider: ${name}`);
|
||
|
|
}
|
||
|
|
return provider;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get all provider names
|
||
|
|
*/
|
||
|
|
export function getProviderNames() {
|
||
|
|
return Object.keys(PROVIDERS);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate auth data for a provider
|
||
|
|
*/
|
||
|
|
export function generateAuthData(providerName, redirectUri) {
|
||
|
|
const provider = getProvider(providerName);
|
||
|
|
const { codeVerifier, codeChallenge, state } = generatePKCE();
|
||
|
|
|
||
|
|
let authUrl;
|
||
|
|
if (provider.flowType === "device_code") {
|
||
|
|
// Device code flow doesn't have auth URL upfront
|
||
|
|
authUrl = null;
|
||
|
|
} else if (provider.flowType === "authorization_code_pkce") {
|
||
|
|
authUrl = provider.buildAuthUrl(provider.config, redirectUri, state, codeChallenge);
|
||
|
|
} else {
|
||
|
|
authUrl = provider.buildAuthUrl(provider.config, redirectUri, state);
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
authUrl,
|
||
|
|
state,
|
||
|
|
codeVerifier,
|
||
|
|
codeChallenge,
|
||
|
|
redirectUri,
|
||
|
|
flowType: provider.flowType,
|
||
|
|
fixedPort: provider.fixedPort,
|
||
|
|
callbackPath: provider.callbackPath || "/callback",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Exchange code for tokens
|
||
|
|
*/
|
||
|
|
export async function exchangeTokens(providerName, code, redirectUri, codeVerifier, state) {
|
||
|
|
const provider = getProvider(providerName);
|
||
|
|
|
||
|
|
const tokens = await provider.exchangeToken(provider.config, code, redirectUri, codeVerifier, state);
|
||
|
|
|
||
|
|
let extra = null;
|
||
|
|
if (provider.postExchange) {
|
||
|
|
extra = await provider.postExchange(tokens);
|
||
|
|
}
|
||
|
|
|
||
|
|
return provider.mapTokens(tokens, extra);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Request device code (for device_code flow)
|
||
|
|
*/
|
||
|
|
export async function requestDeviceCode(providerName, codeChallenge) {
|
||
|
|
const provider = getProvider(providerName);
|
||
|
|
if (provider.flowType !== "device_code") {
|
||
|
|
throw new Error(`Provider ${providerName} does not support device code flow`);
|
||
|
|
}
|
||
|
|
return await provider.requestDeviceCode(provider.config, codeChallenge);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Poll for token (for device_code flow)
|
||
|
|
*/
|
||
|
|
export async function pollForToken(providerName, deviceCode, codeVerifier) {
|
||
|
|
const provider = getProvider(providerName);
|
||
|
|
if (provider.flowType !== "device_code") {
|
||
|
|
throw new Error(`Provider ${providerName} does not support device code flow`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await provider.pollToken(provider.config, deviceCode, codeVerifier);
|
||
|
|
|
||
|
|
if (result.ok) {
|
||
|
|
// For device code flows, success is only when we have an access token
|
||
|
|
if (result.data.access_token) {
|
||
|
|
// Call postExchange to get additional data (copilotToken, userInfo, etc.)
|
||
|
|
let extra = null;
|
||
|
|
if (provider.postExchange) {
|
||
|
|
extra = await provider.postExchange(result.data);
|
||
|
|
}
|
||
|
|
return { success: true, tokens: provider.mapTokens(result.data, extra) };
|
||
|
|
} else {
|
||
|
|
// Check if it's still pending authorization
|
||
|
|
if (result.data.error === 'authorization_pending' || result.data.error === 'slow_down') {
|
||
|
|
// This is not a failure, just still waiting
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: result.data.error,
|
||
|
|
errorDescription: result.data.error_description || result.data.message,
|
||
|
|
pending: result.data.error === 'authorization_pending'
|
||
|
|
};
|
||
|
|
} else {
|
||
|
|
// Actual error
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: result.data.error || 'no_access_token',
|
||
|
|
errorDescription: result.data.error_description || result.data.message || 'No access token received'
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return { success: false, error: result.data.error, errorDescription: result.data.error_description };
|
||
|
|
}
|
||
|
|
|