mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
feat(clinepass): add ClinePass provider support
Register clinepass provider (OAuth + API-key) using Cline's OpenAI-compatible API with 10 curated models, live /v1/models resolver, refreshCline-based token refresh with workos: prefix, and dashboard OAuth login handler. Reference: https://github.com/jellydn/pi-clinepass-provider Closes #2261 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
decolua
co-authored by
Cursor
parent
76752a4396
commit
b08751c4ea
@@ -226,6 +226,7 @@ export class DefaultExecutor extends BaseExecutor {
|
|||||||
gemini: () => this.refreshFromGrant(credentials, proxyOptions),
|
gemini: () => this.refreshFromGrant(credentials, proxyOptions),
|
||||||
kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions),
|
kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions),
|
||||||
cline: () => this.refreshCline(credentials.refreshToken, proxyOptions),
|
cline: () => this.refreshCline(credentials.refreshToken, proxyOptions),
|
||||||
|
clinepass: () => this.refreshCline(credentials.refreshToken, proxyOptions),
|
||||||
"kimi-coding": () => this.refreshKimiCoding(credentials.refreshToken, proxyOptions),
|
"kimi-coding": () => this.refreshKimiCoding(credentials.refreshToken, proxyOptions),
|
||||||
kilocode: () => this.refreshKilocode(credentials.refreshToken, proxyOptions)
|
kilocode: () => this.refreshKilocode(credentials.refreshToken, proxyOptions)
|
||||||
};
|
};
|
||||||
@@ -299,7 +300,11 @@ export class DefaultExecutor extends BaseExecutor {
|
|||||||
const data = payload?.data || payload;
|
const data = payload?.data || payload;
|
||||||
const expiresAtIso = data?.expiresAt;
|
const expiresAtIso = data?.expiresAt;
|
||||||
const expiresIn = expiresAtIso ? Math.max(1, Math.floor((new Date(expiresAtIso).getTime() - Date.now()) / 1000)) : undefined;
|
const expiresIn = expiresAtIso ? Math.max(1, Math.floor((new Date(expiresAtIso).getTime() - Date.now()) / 1000)) : undefined;
|
||||||
return { accessToken: data?.accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn };
|
let accessToken = data?.accessToken;
|
||||||
|
if (accessToken && !accessToken.startsWith("workos:")) {
|
||||||
|
accessToken = `workos:${accessToken}`;
|
||||||
|
}
|
||||||
|
return { accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn };
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshKimiCoding(refreshToken, proxyOptions = null) {
|
async refreshKimiCoding(refreshToken, proxyOptions = null) {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
export default {
|
||||||
|
id: "clinepass",
|
||||||
|
priority: 85,
|
||||||
|
alias: "clinepass",
|
||||||
|
uiAlias: "clinepass",
|
||||||
|
display: {
|
||||||
|
name: "ClinePass",
|
||||||
|
icon: "vpn_key",
|
||||||
|
color: "#5B9BD5",
|
||||||
|
textIcon: "CP",
|
||||||
|
website: "https://cline.bot",
|
||||||
|
notice: {
|
||||||
|
signupUrl: "https://app.cline.bot",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
category: "oauth",
|
||||||
|
authModes: ["oauth", "apikey"],
|
||||||
|
hasOAuth: true,
|
||||||
|
transport: {
|
||||||
|
baseUrl: "https://api.cline.bot/api/v1/chat/completions",
|
||||||
|
headers: {
|
||||||
|
"HTTP-Referer": "https://cline.bot",
|
||||||
|
"X-Title": "Cline",
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
combined: true,
|
||||||
|
header: "Authorization",
|
||||||
|
scheme: "bearer",
|
||||||
|
hooks: [
|
||||||
|
"clineHeaders",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
models: [
|
||||||
|
{ id: "cline-pass/glm-5.2", name: "GLM-5.2 (ClinePass)" },
|
||||||
|
{ id: "cline-pass/kimi-k2.7-code", name: "Kimi K2.7 Code (ClinePass)" },
|
||||||
|
{ id: "cline-pass/kimi-k2.6", name: "Kimi K2.6 (ClinePass)" },
|
||||||
|
{ id: "cline-pass/deepseek-v4-pro", name: "DeepSeek V4 Pro (ClinePass)" },
|
||||||
|
{ id: "cline-pass/deepseek-v4-flash", name: "DeepSeek V4 Flash (ClinePass)" },
|
||||||
|
{ id: "cline-pass/mimo-v2.5", name: "MiMo-V2.5 (ClinePass)" },
|
||||||
|
{ id: "cline-pass/mimo-v2.5-pro", name: "MiMo-V2.5-Pro (ClinePass)" },
|
||||||
|
{ id: "cline-pass/minimax-m3", name: "MiniMax M3 (ClinePass)" },
|
||||||
|
{ id: "cline-pass/qwen3.7-max", name: "Qwen3.7 Max (ClinePass)" },
|
||||||
|
{ id: "cline-pass/qwen3.7-plus", name: "Qwen3.7 Plus (ClinePass)" },
|
||||||
|
],
|
||||||
|
oauth: {
|
||||||
|
appBaseUrl: "https://app.cline.bot",
|
||||||
|
apiBaseUrl: "https://api.cline.bot",
|
||||||
|
authorizeUrl: "https://api.cline.bot/api/v1/auth/authorize",
|
||||||
|
tokenUrl: "https://api.cline.bot/api/v1/auth/token",
|
||||||
|
refreshUrl: "https://api.cline.bot/api/v1/auth/refresh",
|
||||||
|
},
|
||||||
|
thinkingConfig: {
|
||||||
|
options: ["auto", "on", "off"],
|
||||||
|
defaultMode: "auto",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -15,86 +15,87 @@ import p12 from "./cerebras.js";
|
|||||||
import p13 from "./chutes.js";
|
import p13 from "./chutes.js";
|
||||||
import p14 from "./claude.js";
|
import p14 from "./claude.js";
|
||||||
import p15 from "./cline.js";
|
import p15 from "./cline.js";
|
||||||
import p16 from "./cloudflare-ai.js";
|
import p16 from "./clinepass.js";
|
||||||
import p17 from "./codebuddy-cn.js";
|
import p17 from "./cloudflare-ai.js";
|
||||||
import p18 from "./codex.js";
|
import p18 from "./codebuddy-cn.js";
|
||||||
import p19 from "./cohere.js";
|
import p19 from "./codex.js";
|
||||||
import p20 from "./comfyui.js";
|
import p20 from "./cohere.js";
|
||||||
import p21 from "./commandcode.js";
|
import p21 from "./comfyui.js";
|
||||||
import p22 from "./coqui.js";
|
import p22 from "./commandcode.js";
|
||||||
import p23 from "./cursor.js";
|
import p23 from "./coqui.js";
|
||||||
import p24 from "./deepgram.js";
|
import p24 from "./cursor.js";
|
||||||
import p25 from "./deepseek.js";
|
import p25 from "./deepgram.js";
|
||||||
import p26 from "./edge-tts.js";
|
import p26 from "./deepseek.js";
|
||||||
import p27 from "./elevenlabs.js";
|
import p27 from "./edge-tts.js";
|
||||||
import p28 from "./exa.js";
|
import p28 from "./elevenlabs.js";
|
||||||
import p29 from "./fal-ai.js";
|
import p29 from "./exa.js";
|
||||||
import p30 from "./firecrawl.js";
|
import p30 from "./fal-ai.js";
|
||||||
import p31 from "./fireworks.js";
|
import p31 from "./firecrawl.js";
|
||||||
import p32 from "./gemini-cli.js";
|
import p32 from "./fireworks.js";
|
||||||
import p33 from "./gemini.js";
|
import p33 from "./gemini-cli.js";
|
||||||
import p34 from "./github.js";
|
import p34 from "./gemini.js";
|
||||||
import p35 from "./gitlab.js";
|
import p35 from "./github.js";
|
||||||
import p36 from "./glm-cn.js";
|
import p36 from "./gitlab.js";
|
||||||
import p37 from "./glm.js";
|
import p37 from "./glm-cn.js";
|
||||||
import p38 from "./google-pse.js";
|
import p38 from "./glm.js";
|
||||||
import p39 from "./google-tts.js";
|
import p39 from "./google-pse.js";
|
||||||
import p40 from "./grok-web.js";
|
import p40 from "./google-tts.js";
|
||||||
import p41 from "./groq.js";
|
import p41 from "./grok-web.js";
|
||||||
import p42 from "./huggingface.js";
|
import p42 from "./groq.js";
|
||||||
import p43 from "./hyperbolic.js";
|
import p43 from "./huggingface.js";
|
||||||
import p44 from "./iflow.js";
|
import p44 from "./hyperbolic.js";
|
||||||
import p45 from "./inworld.js";
|
import p45 from "./iflow.js";
|
||||||
import p46 from "./jina-ai.js";
|
import p46 from "./inworld.js";
|
||||||
import p47 from "./jina-reader.js";
|
import p47 from "./jina-ai.js";
|
||||||
import p48 from "./kilocode.js";
|
import p48 from "./jina-reader.js";
|
||||||
import p49 from "./kimchi.js";
|
import p49 from "./kilocode.js";
|
||||||
import p50 from "./kimi-coding.js";
|
import p50 from "./kimchi.js";
|
||||||
import p51 from "./kimi.js";
|
import p51 from "./kimi-coding.js";
|
||||||
import p52 from "./kiro.js";
|
import p52 from "./kimi.js";
|
||||||
import p53 from "./linkup.js";
|
import p53 from "./kiro.js";
|
||||||
import p54 from "./local-device.js";
|
import p54 from "./linkup.js";
|
||||||
import p55 from "./mimo-free.js";
|
import p55 from "./local-device.js";
|
||||||
import p56 from "./minimax-cn.js";
|
import p56 from "./mimo-free.js";
|
||||||
import p57 from "./minimax.js";
|
import p57 from "./minimax-cn.js";
|
||||||
import p58 from "./mistral.js";
|
import p58 from "./minimax.js";
|
||||||
import p59 from "./mmf.js";
|
import p59 from "./mistral.js";
|
||||||
import p60 from "./nanobanana.js";
|
import p60 from "./mmf.js";
|
||||||
import p61 from "./nebius.js";
|
import p61 from "./nanobanana.js";
|
||||||
import p62 from "./nvidia.js";
|
import p62 from "./nebius.js";
|
||||||
import p63 from "./ollama-local.js";
|
import p63 from "./nvidia.js";
|
||||||
import p64 from "./ollama.js";
|
import p64 from "./ollama-local.js";
|
||||||
import p65 from "./openai.js";
|
import p65 from "./ollama.js";
|
||||||
import p66 from "./opencode-go.js";
|
import p66 from "./openai.js";
|
||||||
import p67 from "./opencode.js";
|
import p67 from "./opencode-go.js";
|
||||||
import p68 from "./openrouter.js";
|
import p68 from "./opencode.js";
|
||||||
import p69 from "./perplexity-web.js";
|
import p69 from "./openrouter.js";
|
||||||
import p70 from "./perplexity.js";
|
import p70 from "./perplexity-web.js";
|
||||||
import p71 from "./playht.js";
|
import p71 from "./perplexity.js";
|
||||||
import p72 from "./qoder.js";
|
import p72 from "./playht.js";
|
||||||
import p73 from "./qwen.js";
|
import p73 from "./qoder.js";
|
||||||
import p74 from "./recraft.js";
|
import p74 from "./qwen.js";
|
||||||
import p75 from "./runwayml.js";
|
import p75 from "./recraft.js";
|
||||||
import p76 from "./sdwebui.js";
|
import p76 from "./runwayml.js";
|
||||||
import p77 from "./searchapi.js";
|
import p77 from "./sdwebui.js";
|
||||||
import p78 from "./searxng.js";
|
import p78 from "./searchapi.js";
|
||||||
import p79 from "./serper.js";
|
import p79 from "./searxng.js";
|
||||||
import p80 from "./siliconflow.js";
|
import p80 from "./serper.js";
|
||||||
import p81 from "./stability-ai.js";
|
import p81 from "./siliconflow.js";
|
||||||
import p82 from "./tavily.js";
|
import p82 from "./stability-ai.js";
|
||||||
import p83 from "./together.js";
|
import p83 from "./tavily.js";
|
||||||
import p84 from "./topaz.js";
|
import p84 from "./together.js";
|
||||||
import p85 from "./tortoise.js";
|
import p85 from "./topaz.js";
|
||||||
import p86 from "./venice.js";
|
import p86 from "./tortoise.js";
|
||||||
import p87 from "./vercel-ai-gateway.js";
|
import p87 from "./venice.js";
|
||||||
import p88 from "./vertex-partner.js";
|
import p88 from "./vercel-ai-gateway.js";
|
||||||
import p89 from "./vertex.js";
|
import p89 from "./vertex-partner.js";
|
||||||
import p90 from "./volcengine-ark.js";
|
import p90 from "./vertex.js";
|
||||||
import p91 from "./voyage-ai.js";
|
import p91 from "./volcengine-ark.js";
|
||||||
import p92 from "./xai.js";
|
import p92 from "./voyage-ai.js";
|
||||||
import p93 from "./xiaomi-mimo.js";
|
import p93 from "./xai.js";
|
||||||
import p94 from "./xiaomi-tokenplan.js";
|
import p94 from "./xiaomi-mimo.js";
|
||||||
import p95 from "./youcom.js";
|
import p95 from "./xiaomi-tokenplan.js";
|
||||||
|
import p96 from "./youcom.js";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
p0,
|
p0,
|
||||||
@@ -192,5 +193,6 @@ export default [
|
|||||||
p92,
|
p92,
|
||||||
p93,
|
p93,
|
||||||
p94,
|
p94,
|
||||||
p95
|
p95,
|
||||||
|
p96
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { buildClineHeaders } from "../shared/clineAuth.js";
|
||||||
|
|
||||||
|
const CLINEPASS_MODELS_ENDPOINT = "https://api.cline.bot/api/v1/models";
|
||||||
|
const FETCH_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build request headers for the ClinePass /models endpoint (Cline's upstream API).
|
||||||
|
* - API keys are sent as plain Bearer tokens.
|
||||||
|
* - OAuth access tokens must carry the WorkOS `workos:` prefix (handled by buildClineHeaders).
|
||||||
|
*/
|
||||||
|
function buildModelListHeaders(token, isApiKey) {
|
||||||
|
if (isApiKey) {
|
||||||
|
return {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return buildClineHeaders(token, { Accept: "application/json" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch ClinePass live model catalog from Cline's /models endpoint.
|
||||||
|
*
|
||||||
|
* @param {object} credentials - Connection credentials ({ accessToken, apiKey })
|
||||||
|
* @returns {Promise<{ models: { id: string, name: string }[] } | null>}
|
||||||
|
*/
|
||||||
|
export async function resolveClinepassModels(credentials) {
|
||||||
|
const isApiKey = Boolean(credentials?.apiKey);
|
||||||
|
const token = isApiKey ? credentials.apiKey : credentials?.accessToken;
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = buildModelListHeaders(token, isApiKey);
|
||||||
|
|
||||||
|
const response = await fetch(CLINEPASS_MODELS_ENDPOINT, {
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return null;
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
const rawList = Array.isArray(json) ? json : json?.data;
|
||||||
|
if (!Array.isArray(rawList)) return null;
|
||||||
|
|
||||||
|
const models = rawList
|
||||||
|
.filter((m) => typeof m?.id === "string" && m.id.startsWith("cline-pass/"))
|
||||||
|
.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
name: m.name || m.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return models.length ? { models } : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -232,8 +232,8 @@ export async function POST(request, { params }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cline uses authorization_code without PKCE. Kimchi returns a browser token.
|
// Cline and ClinePass use authorization_code without PKCE. Kimchi returns a browser token.
|
||||||
const noPkceExchangeProviders = ["cline", "kimchi"];
|
const noPkceExchangeProviders = ["cline", "clinepass", "kimchi"];
|
||||||
if (!code || !redirectUri || (!codeVerifier && !noPkceExchangeProviders.includes(provider))) {
|
if (!code || !redirectUri || (!codeVerifier && !noPkceExchangeProviders.includes(provider))) {
|
||||||
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
|
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { resolveKiroModels } from "open-sse/services/kiroModels.js";
|
|||||||
import { resolveKimchiModels } from "open-sse/services/kimchiModels.js";
|
import { resolveKimchiModels } from "open-sse/services/kimchiModels.js";
|
||||||
import { resolveQoderModels } from "open-sse/services/qoderModels.js";
|
import { resolveQoderModels } from "open-sse/services/qoderModels.js";
|
||||||
import { resolveCopilotModels } from "open-sse/services/copilotModels.js";
|
import { resolveCopilotModels } from "open-sse/services/copilotModels.js";
|
||||||
|
import { resolveClinepassModels } from "open-sse/services/clinepassModels.js";
|
||||||
import { updateProviderCredentials } from "@/sse/services/tokenRefresh";
|
import { updateProviderCredentials } from "@/sse/services/tokenRefresh";
|
||||||
import { capabilitiesFromServiceKind } from "open-sse/providers/capabilities.js";
|
import { capabilitiesFromServiceKind } from "open-sse/providers/capabilities.js";
|
||||||
|
|
||||||
@@ -63,6 +64,13 @@ const LIVE_MODEL_RESOLVERS = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return result?.models?.length ? { models: result.models } : null;
|
return result?.models?.length ? { models: result.models } : null;
|
||||||
|
},
|
||||||
|
clinepass: async (conn) => {
|
||||||
|
const result = await resolveClinepassModels({
|
||||||
|
accessToken: conn.accessToken,
|
||||||
|
apiKey: conn.apiKey,
|
||||||
|
});
|
||||||
|
return result?.models?.length ? { models: result.models } : null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ export const KILOCODE_CONFIG = { ...PROVIDER_OAUTH["kilocode"] };
|
|||||||
// Cline OAuth Configuration (Local Callback Flow via app.cline.bot)
|
// Cline OAuth Configuration (Local Callback Flow via app.cline.bot)
|
||||||
export const CLINE_CONFIG = { ...PROVIDER_OAUTH["cline"] };
|
export const CLINE_CONFIG = { ...PROVIDER_OAUTH["cline"] };
|
||||||
|
|
||||||
|
// ClinePass OAuth Configuration (shares Cline's OAuth endpoints)
|
||||||
|
export const CLINEPASS_CONFIG = { ...PROVIDER_OAUTH["clinepass"] };
|
||||||
|
|
||||||
// GitLab Duo OAuth Configuration (Authorization Code Flow with PKCE)
|
// GitLab Duo OAuth Configuration (Authorization Code Flow with PKCE)
|
||||||
export const GITLAB_CONFIG = { ...PROVIDER_OAUTH["gitlab"] };
|
export const GITLAB_CONFIG = { ...PROVIDER_OAUTH["gitlab"] };
|
||||||
|
|
||||||
@@ -130,6 +133,7 @@ export const PROVIDERS = {
|
|||||||
KIMI_CODING: "kimi-coding",
|
KIMI_CODING: "kimi-coding",
|
||||||
KILOCODE: "kilocode",
|
KILOCODE: "kilocode",
|
||||||
CLINE: "cline",
|
CLINE: "cline",
|
||||||
|
CLINEPASS: "clinepass",
|
||||||
GITLAB: "gitlab",
|
GITLAB: "gitlab",
|
||||||
CODEBUDDY: "codebuddy-cn",
|
CODEBUDDY: "codebuddy-cn",
|
||||||
KIMCHI: "kimchi",
|
KIMCHI: "kimchi",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
KIMI_CODING_CONFIG,
|
KIMI_CODING_CONFIG,
|
||||||
KILOCODE_CONFIG,
|
KILOCODE_CONFIG,
|
||||||
CLINE_CONFIG,
|
CLINE_CONFIG,
|
||||||
|
CLINEPASS_CONFIG,
|
||||||
GITLAB_CONFIG,
|
GITLAB_CONFIG,
|
||||||
CODEBUDDY_CONFIG,
|
CODEBUDDY_CONFIG,
|
||||||
KIMCHI_CONFIG,
|
KIMCHI_CONFIG,
|
||||||
@@ -1116,6 +1117,64 @@ const PROVIDERS = {
|
|||||||
providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName },
|
providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
clinepass: {
|
||||||
|
config: CLINEPASS_CONFIG,
|
||||||
|
flowType: "authorization_code",
|
||||||
|
buildAuthUrl: (config, redirectUri) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_type: "extension",
|
||||||
|
callback_url: redirectUri,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
});
|
||||||
|
return `${config.authorizeUrl}?${params.toString()}`;
|
||||||
|
},
|
||||||
|
exchangeToken: async (config, code, redirectUri) => {
|
||||||
|
try {
|
||||||
|
// Cline encodes token data as base64 in the code param
|
||||||
|
let base64 = code;
|
||||||
|
const padding = 4 - (base64.length % 4);
|
||||||
|
if (padding !== 4) base64 += "=".repeat(padding);
|
||||||
|
const decoded = Buffer.from(base64, "base64").toString("utf-8");
|
||||||
|
const lastBrace = decoded.lastIndexOf("}");
|
||||||
|
if (lastBrace === -1) throw new Error("No JSON found in decoded code");
|
||||||
|
const tokenData = JSON.parse(decoded.substring(0, lastBrace + 1));
|
||||||
|
return {
|
||||||
|
access_token: tokenData.accessToken,
|
||||||
|
refresh_token: tokenData.refreshToken,
|
||||||
|
email: tokenData.email,
|
||||||
|
firstName: tokenData.firstName,
|
||||||
|
lastName: tokenData.lastName,
|
||||||
|
expires_at: tokenData.expiresAt,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
const response = await fetch(config.tokenUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
|
body: JSON.stringify({ grant_type: "authorization_code", code, client_type: "extension", redirect_uri: redirectUri }),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`ClinePass token exchange failed: ${error}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
access_token: data.data?.accessToken || data.accessToken,
|
||||||
|
refresh_token: data.data?.refreshToken || data.refreshToken,
|
||||||
|
email: data.data?.userInfo?.email || "",
|
||||||
|
expires_at: data.data?.expiresAt || data.expiresAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mapTokens: (tokens) => ({
|
||||||
|
accessToken: tokens.access_token,
|
||||||
|
refreshToken: tokens.refresh_token,
|
||||||
|
expiresIn: tokens.expires_at
|
||||||
|
? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)
|
||||||
|
: 3600,
|
||||||
|
email: tokens.email,
|
||||||
|
providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName },
|
||||||
|
}),
|
||||||
|
},
|
||||||
// GitLab Duo - Authorization Code Flow with PKCE
|
// GitLab Duo - Authorization Code Flow with PKCE
|
||||||
// Supports two login modes via loginMode metadata: "oauth" (default) or "pat"
|
// Supports two login modes via loginMode metadata: "oauth" (default) or "pat"
|
||||||
gitlab: {
|
gitlab: {
|
||||||
|
|||||||
Reference in New Issue
Block a user