Spaces:
Sleeping
Sleeping
| import http from 'http'; | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import crypto from 'crypto'; | |
| import os from 'os'; | |
| import { broadcastEvent } from '../services/ui-manager.js'; | |
| import { autoLinkProviderConfigs } from '../services/service-manager.js'; | |
| import { CONFIG } from '../core/config-manager.js'; | |
| import { getProxyConfigForProvider } from '../utils/proxy-utils.js'; | |
| /** | |
| * Kiro OAuth 配置(支持多种认证方式) | |
| */ | |
| const KIRO_OAUTH_CONFIG = { | |
| // Kiro Auth Service 端点 (用于 Social Auth) | |
| authServiceEndpoint: 'https://prod.us-east-1.auth.desktop.kiro.dev', | |
| // AWS SSO OIDC 端点 (用于 Builder ID) | |
| ssoOIDCEndpoint: 'https://oidc.{{region}}.amazonaws.com', | |
| // AWS Builder ID 起始 URL | |
| builderIDStartURL: 'https://view.awsapps.com/start', | |
| // 本地回调端口范围(用于 Social Auth HTTP 回调) | |
| callbackPortStart: 19876, | |
| callbackPortEnd: 19880, | |
| // 超时配置 | |
| authTimeout: 10 * 60 * 1000, // 10 分钟 | |
| pollInterval: 5000, // 5 秒 | |
| // CodeWhisperer Scopes | |
| scopes: [ | |
| 'codewhisperer:completions', | |
| 'codewhisperer:analysis', | |
| 'codewhisperer:conversations', | |
| // 'codewhisperer:transformations', | |
| // 'codewhisperer:taskassist' | |
| ], | |
| // 凭据存储(符合现有规范) | |
| credentialsDir: '.kiro', | |
| credentialsFile: 'oauth_creds.json', | |
| // 日志前缀 | |
| logPrefix: '[Kiro Auth]' | |
| }; | |
| /** | |
| * 活动的 Kiro 回调服务器管理 | |
| */ | |
| const activeKiroServers = new Map(); | |
| /** | |
| * 活动的 Kiro 轮询任务管理(用于 Builder ID Device Code) | |
| */ | |
| const activeKiroPollingTasks = new Map(); | |
| /** | |
| * 创建带代理支持的 fetch 请求 | |
| * 使用 axios 替代原生 fetch,以正确支持代理配置 | |
| * @param {string} url - 请求 URL | |
| * @param {Object} options - fetch 选项(兼容 fetch API 格式) | |
| * @param {string} providerType - 提供商类型,用于获取代理配置 | |
| * @returns {Promise<Object>} 返回类似 fetch Response 的对象 | |
| */ | |
| async function fetchWithProxy(url, options = {}, providerType) { | |
| const proxyConfig = getProxyConfigForProvider(CONFIG, providerType); | |
| // 构建 axios 配置 | |
| const axiosConfig = { | |
| url, | |
| method: options.method || 'GET', | |
| headers: options.headers || {}, | |
| timeout: 30000, // 30 秒超时 | |
| }; | |
| // 处理请求体 | |
| if (options.body) { | |
| axiosConfig.data = options.body; | |
| } | |
| // 配置代理 | |
| if (proxyConfig) { | |
| axiosConfig.httpAgent = proxyConfig.httpAgent; | |
| axiosConfig.httpsAgent = proxyConfig.httpsAgent; | |
| axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent | |
| console.log(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`); | |
| } | |
| try { | |
| const axios = (await import('axios')).default; | |
| const response = await axios(axiosConfig); | |
| // 返回类似 fetch Response 的对象 | |
| return { | |
| ok: response.status >= 200 && response.status < 300, | |
| status: response.status, | |
| statusText: response.statusText, | |
| headers: response.headers, | |
| json: async () => response.data, | |
| text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data), | |
| }; | |
| } catch (error) { | |
| // 处理 axios 错误,转换为类似 fetch 的响应格式 | |
| if (error.response) { | |
| // 服务器返回了错误状态码 | |
| return { | |
| ok: false, | |
| status: error.response.status, | |
| statusText: error.response.statusText, | |
| headers: error.response.headers, | |
| json: async () => error.response.data, | |
| text: async () => typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data), | |
| }; | |
| } | |
| // 网络错误或其他错误 | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 生成 HTML 响应页面 | |
| * @param {boolean} isSuccess - 是否成功 | |
| * @param {string} message - 显示消息 | |
| * @returns {string} HTML 内容 | |
| */ | |
| function generateResponsePage(isSuccess, message) { | |
| const title = isSuccess ? '授权成功!' : '授权失败'; | |
| return `<!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>${title}</title> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>${title}</h1> | |
| <p>${message}</p> | |
| </div> | |
| </body> | |
| </html>`; | |
| } | |
| /** | |
| * 生成 PKCE 代码验证器 | |
| * @returns {string} Base64URL 编码的随机字符串 | |
| */ | |
| function generateCodeVerifier() { | |
| return crypto.randomBytes(32).toString('base64url'); | |
| } | |
| /** | |
| * 生成 PKCE 代码挑战 | |
| * @param {string} codeVerifier - 代码验证器 | |
| * @returns {string} Base64URL 编码的 SHA256 哈希 | |
| */ | |
| function generateCodeChallenge(codeVerifier) { | |
| const hash = crypto.createHash('sha256'); | |
| hash.update(codeVerifier); | |
| return hash.digest('base64url'); | |
| } | |
| /** | |
| * 处理 Kiro OAuth 授权(统一入口) | |
| * @param {Object} currentConfig - 当前配置对象 | |
| * @param {Object} options - 额外选项 | |
| * - method: 'google' | 'github' | 'builder-id' | |
| * - saveToConfigs: boolean | |
| * @returns {Promise<Object>} 返回授权URL和相关信息 | |
| */ | |
| export async function handleKiroOAuth(currentConfig, options = {}) { | |
| const method = options.method || options.authMethod || 'google'; // 默认使用 Google,同时支持 authMethod 参数 | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Starting OAuth with method: ${method}`); | |
| switch (method) { | |
| case 'google': | |
| return handleKiroSocialAuth('Google', currentConfig, options); | |
| case 'github': | |
| return handleKiroSocialAuth('Github', currentConfig, options); | |
| case 'builder-id': | |
| return handleKiroBuilderIDDeviceCode(currentConfig, options); | |
| default: | |
| throw new Error(`不支持的认证方式: ${method}`); | |
| } | |
| } | |
| /** | |
| * Kiro Social Auth (Google/GitHub) - 使用 HTTP localhost 回调 | |
| */ | |
| async function handleKiroSocialAuth(provider, currentConfig, options = {}) { | |
| // 生成 PKCE 参数 | |
| const codeVerifier = generateCodeVerifier(); | |
| const codeChallenge = generateCodeChallenge(codeVerifier); | |
| const state = crypto.randomBytes(16).toString('base64url'); | |
| // 启动本地回调服务器并获取端口 | |
| let handlerPort; | |
| const providerKey = 'claude-kiro-oauth'; | |
| if (options.port) { | |
| const port = parseInt(options.port); | |
| await closeKiroServer(providerKey, port); | |
| const server = await createKiroHttpCallbackServer(port, codeVerifier, state, options); | |
| activeKiroServers.set(providerKey, { server, port }); | |
| handlerPort = port; | |
| } else { | |
| handlerPort = await startKiroCallbackServer(codeVerifier, state, options); | |
| } | |
| // 使用 HTTP localhost 作为 redirect_uri | |
| const redirectUri = `http://127.0.0.1:${handlerPort}/oauth/callback`; | |
| // 构建授权 URL | |
| const authUrl = `${KIRO_OAUTH_CONFIG.authServiceEndpoint}/login?` + | |
| `idp=${provider}&` + | |
| `redirect_uri=${encodeURIComponent(redirectUri)}&` + | |
| `code_challenge=${codeChallenge}&` + | |
| `code_challenge_method=S256&` + | |
| `state=${state}&` + | |
| `prompt=select_account`; | |
| return { | |
| authUrl, | |
| authInfo: { | |
| provider: 'claude-kiro-oauth', | |
| authMethod: 'social', | |
| socialProvider: provider, | |
| port: handlerPort, | |
| redirectUri: redirectUri, | |
| state: state, | |
| ...options | |
| } | |
| }; | |
| } | |
| /** | |
| * Kiro Builder ID - Device Code Flow(类似 Qwen OAuth 模式) | |
| */ | |
| async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) { | |
| // 停止之前的轮询任务 | |
| for (const [existingTaskId] of activeKiroPollingTasks.entries()) { | |
| if (existingTaskId.startsWith('kiro-')) { | |
| stopKiroPollingTask(existingTaskId); | |
| } | |
| } | |
| // 获取 Builder ID Start URL(优先使用前端传入的值,否则使用默认值) | |
| const builderIDStartURL = options.builderIDStartURL || KIRO_OAUTH_CONFIG.builderIDStartURL; | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Using Builder ID Start URL: ${builderIDStartURL}`); | |
| // 1. 注册 OIDC 客户端 | |
| const region = options.region || 'us-east-1'; | |
| const ssoOIDCEndpoint = KIRO_OAUTH_CONFIG.ssoOIDCEndpoint.replace('{{region}}', region); | |
| const regResponse = await fetchWithProxy(`${ssoOIDCEndpoint}/client/register`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'User-Agent': 'KiroIDE' | |
| }, | |
| body: JSON.stringify({ | |
| clientName: 'Kiro IDE', | |
| clientType: 'public', | |
| scopes: KIRO_OAUTH_CONFIG.scopes, | |
| // grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'] | |
| }) | |
| }, 'claude-kiro-oauth'); | |
| if (!regResponse.ok) { | |
| throw new Error(`Kiro OAuth 客户端注册失败: ${regResponse.status}`); | |
| } | |
| const regData = await regResponse.json(); | |
| // 2. 启动设备授权 | |
| const authResponse = await fetchWithProxy(`${ssoOIDCEndpoint}/device_authorization`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| clientId: regData.clientId, | |
| clientSecret: regData.clientSecret, | |
| startUrl: builderIDStartURL | |
| }) | |
| }, 'claude-kiro-oauth'); | |
| if (!authResponse.ok) { | |
| throw new Error(`Kiro OAuth 设备授权失败: ${authResponse.status}`); | |
| } | |
| const deviceAuth = await authResponse.json(); | |
| // 3. 启动后台轮询(类似 Qwen OAuth 的模式) | |
| const taskId = `kiro-${deviceAuth.deviceCode.substring(0, 8)}-${Date.now()}`; | |
| // 异步轮询 | |
| pollKiroBuilderIDToken( | |
| regData.clientId, | |
| regData.clientSecret, | |
| deviceAuth.deviceCode, | |
| 5, | |
| 300, | |
| taskId, | |
| { ...options, region } | |
| ).catch(error => { | |
| console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error); | |
| broadcastEvent('oauth_error', { | |
| provider: 'claude-kiro-oauth', | |
| error: error.message, | |
| timestamp: new Date().toISOString() | |
| }); | |
| }); | |
| return { | |
| authUrl: deviceAuth.verificationUriComplete, | |
| authInfo: { | |
| provider: 'claude-kiro-oauth', | |
| authMethod: 'builder-id', | |
| deviceCode: deviceAuth.deviceCode, | |
| userCode: deviceAuth.userCode, | |
| verificationUri: deviceAuth.verificationUri, | |
| verificationUriComplete: deviceAuth.verificationUriComplete, | |
| expiresIn: deviceAuth.expiresIn, | |
| interval: deviceAuth.interval, | |
| ...options | |
| } | |
| }; | |
| } | |
| /** | |
| * 轮询获取 Kiro Builder ID Token | |
| */ | |
| async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interval, expiresIn, taskId, options = {}) { | |
| let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile); | |
| const maxAttempts = Math.floor(expiresIn / interval); | |
| let attempts = 0; | |
| const taskControl = { shouldStop: false }; | |
| activeKiroPollingTasks.set(taskId, taskControl); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}]`); | |
| const poll = async () => { | |
| if (taskControl.shouldStop) { | |
| throw new Error('轮询任务已被取消'); | |
| } | |
| if (attempts >= maxAttempts) { | |
| activeKiroPollingTasks.delete(taskId); | |
| throw new Error('授权超时'); | |
| } | |
| attempts++; | |
| try { | |
| const region = options.region || 'us-east-1'; | |
| const ssoOIDCEndpoint = KIRO_OAUTH_CONFIG.ssoOIDCEndpoint.replace('{{region}}', region); | |
| const response = await fetchWithProxy(`${ssoOIDCEndpoint}/token`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'User-Agent': 'KiroIDE' | |
| }, | |
| body: JSON.stringify({ | |
| clientId, | |
| clientSecret, | |
| deviceCode, | |
| grantType: 'urn:ietf:params:oauth:grant-type:device_code' | |
| }) | |
| }, 'claude-kiro-oauth'); | |
| const data = await response.json(); | |
| if (response.ok && data.accessToken) { | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`); | |
| // 保存令牌(符合现有规范) | |
| if (options.saveToConfigs) { | |
| const timestamp = Date.now(); | |
| const folderName = `${timestamp}_kiro-auth-token`; | |
| const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName); | |
| await fs.promises.mkdir(targetDir, { recursive: true }); | |
| credPath = path.join(targetDir, `${folderName}.json`); | |
| } | |
| const tokenData = { | |
| accessToken: data.accessToken, | |
| refreshToken: data.refreshToken, | |
| expiresAt: new Date(Date.now() + data.expiresIn * 1000).toISOString(), | |
| authMethod: 'builder-id', | |
| clientId, | |
| clientSecret, | |
| idcRegion: options.region || 'us-east-1' | |
| }; | |
| await fs.promises.mkdir(path.dirname(credPath), { recursive: true }); | |
| await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2)); | |
| activeKiroPollingTasks.delete(taskId); | |
| // 广播成功事件(符合现有规范) | |
| broadcastEvent('oauth_success', { | |
| provider: 'claude-kiro-oauth', | |
| credPath, | |
| relativePath: path.relative(process.cwd(), credPath), | |
| timestamp: new Date().toISOString() | |
| }); | |
| // 自动关联新生成的凭据到 Pools | |
| await autoLinkProviderConfigs(CONFIG); | |
| return tokenData; | |
| } | |
| // 检查错误类型 | |
| if (data.error === 'authorization_pending') { | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (${attempts}/${maxAttempts})`); | |
| await new Promise(resolve => setTimeout(resolve, interval * 1000)); | |
| return poll(); | |
| } else if (data.error === 'slow_down') { | |
| await new Promise(resolve => setTimeout(resolve, (interval + 5) * 1000)); | |
| return poll(); | |
| } else { | |
| activeKiroPollingTasks.delete(taskId); | |
| throw new Error(`授权失败: ${data.error || '未知错误'}`); | |
| } | |
| } catch (error) { | |
| if (error.message.includes('授权') || error.message.includes('取消')) { | |
| throw error; | |
| } | |
| await new Promise(resolve => setTimeout(resolve, interval * 1000)); | |
| return poll(); | |
| } | |
| }; | |
| return poll(); | |
| } | |
| /** | |
| * 停止 Kiro 轮询任务 | |
| */ | |
| function stopKiroPollingTask(taskId) { | |
| const task = activeKiroPollingTasks.get(taskId); | |
| if (task) { | |
| task.shouldStop = true; | |
| activeKiroPollingTasks.delete(taskId); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`); | |
| } | |
| } | |
| /** | |
| * 启动 Kiro 回调服务器(用于 Social Auth HTTP 回调) | |
| */ | |
| async function startKiroCallbackServer(codeVerifier, expectedState, options = {}) { | |
| const portStart = KIRO_OAUTH_CONFIG.callbackPortStart; | |
| const portEnd = KIRO_OAUTH_CONFIG.callbackPortEnd; | |
| for (let port = portStart; port <= portEnd; port++) { | |
| // 关闭已存在的服务器 | |
| await closeKiroServer(port); | |
| try { | |
| const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options); | |
| activeKiroServers.set('claude-kiro-oauth', { server, port }); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`); | |
| return port; | |
| } catch (err) { | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 端口 ${port} 被占用,尝试下一个...`); | |
| } | |
| } | |
| throw new Error('所有端口都被占用'); | |
| } | |
| /** | |
| * 关闭 Kiro 服务器 | |
| */ | |
| async function closeKiroServer(provider, port = null) { | |
| const existing = activeKiroServers.get(provider); | |
| if (existing) { | |
| await new Promise((resolve) => { | |
| existing.server.close(() => { | |
| activeKiroServers.delete(provider); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); | |
| resolve(); | |
| }); | |
| }); | |
| } | |
| if (port) { | |
| for (const [p, info] of activeKiroServers.entries()) { | |
| if (info.port === port) { | |
| await new Promise((resolve) => { | |
| info.server.close(() => { | |
| activeKiroServers.delete(p); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); | |
| resolve(); | |
| }); | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * 创建 Kiro HTTP 回调服务器 | |
| */ | |
| function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options = {}) { | |
| const redirectUri = `http://127.0.0.1:${port}/oauth/callback`; | |
| return new Promise((resolve, reject) => { | |
| const server = http.createServer(async (req, res) => { | |
| try { | |
| const url = new URL(req.url, `http://127.0.0.1:${port}`); | |
| if (url.pathname === '/oauth/callback') { | |
| const code = url.searchParams.get('code'); | |
| const state = url.searchParams.get('state'); | |
| const errorParam = url.searchParams.get('error'); | |
| if (errorParam) { | |
| res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); | |
| res.end(generateResponsePage(false, `授权失败: ${errorParam}`)); | |
| return; | |
| } | |
| if (state !== expectedState) { | |
| res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); | |
| res.end(generateResponsePage(false, 'State 验证失败')); | |
| return; | |
| } | |
| // 交换 Code 获取 Token(使用动态的 redirect_uri) | |
| const tokenResponse = await fetchWithProxy(`${KIRO_OAUTH_CONFIG.authServiceEndpoint}/oauth/token`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'User-Agent': 'AIClient-2-API/1.0.0' | |
| }, | |
| body: JSON.stringify({ | |
| code, | |
| code_verifier: codeVerifier, | |
| redirect_uri: redirectUri | |
| }) | |
| }, 'claude-kiro-oauth'); | |
| if (!tokenResponse.ok) { | |
| const errorText = await tokenResponse.text(); | |
| console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token exchange failed:`, errorText); | |
| res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); | |
| res.end(generateResponsePage(false, `获取令牌失败: ${tokenResponse.status}`)); | |
| return; | |
| } | |
| const tokenData = await tokenResponse.json(); | |
| // 保存令牌 | |
| let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile); | |
| if (options.saveToConfigs) { | |
| const timestamp = Date.now(); | |
| const folderName = `${timestamp}_kiro-auth-token`; | |
| const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName); | |
| await fs.promises.mkdir(targetDir, { recursive: true }); | |
| credPath = path.join(targetDir, `${folderName}.json`); | |
| } | |
| const saveData = { | |
| accessToken: tokenData.accessToken, | |
| refreshToken: tokenData.refreshToken, | |
| profileArn: tokenData.profileArn, | |
| expiresAt: new Date(Date.now() + (tokenData.expiresIn || 3600) * 1000).toISOString(), | |
| authMethod: 'social', | |
| region: 'us-east-1' | |
| }; | |
| await fs.promises.mkdir(path.dirname(credPath), { recursive: true }); | |
| await fs.promises.writeFile(credPath, JSON.stringify(saveData, null, 2)); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 令牌已保存: ${credPath}`); | |
| // 广播成功事件 | |
| broadcastEvent('oauth_success', { | |
| provider: 'claude-kiro-oauth', | |
| credPath, | |
| relativePath: path.relative(process.cwd(), credPath), | |
| timestamp: new Date().toISOString() | |
| }); | |
| // 自动关联新生成的凭据到 Pools | |
| await autoLinkProviderConfigs(CONFIG); | |
| res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); | |
| res.end(generateResponsePage(true, '授权成功!您可以关闭此页面')); | |
| // 关闭服务器 | |
| server.close(() => { | |
| activeKiroServers.delete('claude-kiro-oauth'); | |
| }); | |
| } else { | |
| res.writeHead(204); | |
| res.end(); | |
| } | |
| } catch (error) { | |
| console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error); | |
| res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); | |
| res.end(generateResponsePage(false, `服务器错误: ${error.message}`)); | |
| } | |
| }); | |
| server.on('error', reject); | |
| server.listen(port, '127.0.0.1', () => resolve(server)); | |
| // 超时自动关闭 | |
| setTimeout(() => { | |
| if (server.listening) { | |
| server.close(() => { | |
| activeKiroServers.delete('claude-kiro-oauth'); | |
| }); | |
| } | |
| }, KIRO_OAUTH_CONFIG.authTimeout); | |
| }); | |
| } | |
| /** | |
| * Kiro Token 刷新常量 | |
| */ | |
| const KIRO_REFRESH_CONSTANTS = { | |
| REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken', | |
| REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token', | |
| CONTENT_TYPE_JSON: 'application/json', | |
| AUTH_METHOD_SOCIAL: 'social', | |
| DEFAULT_PROVIDER: 'Google', | |
| REQUEST_TIMEOUT: 30000, | |
| DEFAULT_REGION: 'us-east-1', | |
| IDC_REGION: 'us-east-1' // 用于 REFRESH_IDC_URL 的区域配置 | |
| }; | |
| /** | |
| * 通过 refreshToken 获取 accessToken | |
| * @param {string} refreshToken - Kiro 的 refresh token | |
| * @param {string} region - AWS 区域 (默认: us-east-1) | |
| * @returns {Promise<Object>} 包含 accessToken 等信息的对象 | |
| */ | |
| async function refreshKiroToken(refreshToken, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION) { | |
| const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_URL.replace('{{region}}', region); | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), KIRO_REFRESH_CONSTANTS.REQUEST_TIMEOUT); | |
| try { | |
| const response = await fetchWithProxy(refreshUrl, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': KIRO_REFRESH_CONSTANTS.CONTENT_TYPE_JSON | |
| }, | |
| body: JSON.stringify({ refreshToken }), | |
| signal: controller.signal | |
| }, 'claude-kiro-oauth'); | |
| clearTimeout(timeoutId); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`HTTP ${response.status}: ${errorText}`); | |
| } | |
| const data = await response.json(); | |
| if (!data.accessToken) { | |
| throw new Error('Invalid refresh response: Missing accessToken'); | |
| } | |
| const expiresIn = data.expiresIn || 3600; | |
| const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); | |
| return { | |
| accessToken: data.accessToken, | |
| refreshToken: data.refreshToken || refreshToken, | |
| profileArn: data.profileArn || '', | |
| expiresAt: expiresAt, | |
| authMethod: KIRO_REFRESH_CONSTANTS.AUTH_METHOD_SOCIAL, | |
| provider: KIRO_REFRESH_CONSTANTS.DEFAULT_PROVIDER, | |
| region: region | |
| }; | |
| } catch (error) { | |
| clearTimeout(timeoutId); | |
| if (error.name === 'AbortError') { | |
| throw new Error('Request timeout'); | |
| } | |
| throw error; | |
| } | |
| } | |
| /** | |
| * 检查 Kiro 凭据是否已存在(基于 refreshToken + provider 组合) | |
| * @param {string} refreshToken - 要检查的 refreshToken | |
| * @param {string} provider - 提供商名称 (默认: 'claude-kiro-oauth') | |
| * @returns {Promise<{isDuplicate: boolean, existingPath?: string}>} 检查结果 | |
| */ | |
| export async function checkKiroCredentialsDuplicate(refreshToken, provider = 'claude-kiro-oauth') { | |
| const kiroDir = path.join(process.cwd(), 'configs', 'kiro'); | |
| try { | |
| // 检查 configs/kiro 目录是否存在 | |
| if (!fs.existsSync(kiroDir)) { | |
| return { isDuplicate: false }; | |
| } | |
| // 递归扫描所有 JSON 文件 | |
| const scanDirectory = async (dirPath) => { | |
| const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); | |
| for (const entry of entries) { | |
| const fullPath = path.join(dirPath, entry.name); | |
| if (entry.isDirectory()) { | |
| const result = await scanDirectory(fullPath); | |
| if (result.isDuplicate) { | |
| return result; | |
| } | |
| } else if (entry.isFile() && entry.name.endsWith('.json')) { | |
| try { | |
| const content = await fs.promises.readFile(fullPath, 'utf8'); | |
| const credentials = JSON.parse(content); | |
| // 检查 refreshToken 是否匹配 | |
| if (credentials.refreshToken && credentials.refreshToken === refreshToken) { | |
| const relativePath = path.relative(process.cwd(), fullPath); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Found duplicate refreshToken in: ${relativePath}`); | |
| return { | |
| isDuplicate: true, | |
| existingPath: relativePath | |
| }; | |
| } | |
| } catch (parseError) { | |
| // 忽略解析错误的文件 | |
| } | |
| } | |
| } | |
| return { isDuplicate: false }; | |
| }; | |
| return await scanDirectory(kiroDir); | |
| } catch (error) { | |
| console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Error checking duplicates:`, error.message); | |
| return { isDuplicate: false }; | |
| } | |
| } | |
| /** | |
| * 批量导入 Kiro refreshToken 并生成凭据文件 | |
| * @param {string[]} refreshTokens - refreshToken 数组 | |
| * @param {string} region - AWS 区域 (默认: us-east-1) | |
| * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false) | |
| * @returns {Promise<Object>} 批量处理结果 | |
| */ | |
| export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, skipDuplicateCheck = false) { | |
| const results = { | |
| total: refreshTokens.length, | |
| success: 0, | |
| failed: 0, | |
| details: [] | |
| }; | |
| for (let i = 0; i < refreshTokens.length; i++) { | |
| const refreshToken = refreshTokens[i].trim(); | |
| if (!refreshToken) { | |
| results.details.push({ | |
| index: i + 1, | |
| success: false, | |
| error: 'Empty token' | |
| }); | |
| results.failed++; | |
| continue; | |
| } | |
| // 检查重复 | |
| if (!skipDuplicateCheck) { | |
| const duplicateCheck = await checkKiroCredentialsDuplicate(refreshToken); | |
| if (duplicateCheck.isDuplicate) { | |
| results.details.push({ | |
| index: i + 1, | |
| success: false, | |
| error: 'duplicate', | |
| existingPath: duplicateCheck.existingPath | |
| }); | |
| results.failed++; | |
| continue; | |
| } | |
| } | |
| try { | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`); | |
| const tokenData = await refreshKiroToken(refreshToken, region); | |
| // 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json | |
| const timestamp = Date.now(); | |
| const folderName = `${timestamp}_kiro-auth-token`; | |
| const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName); | |
| await fs.promises.mkdir(targetDir, { recursive: true }); | |
| const credPath = path.join(targetDir, `${folderName}.json`); | |
| await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2)); | |
| const relativePath = path.relative(process.cwd(), credPath); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`); | |
| results.details.push({ | |
| index: i + 1, | |
| success: true, | |
| path: relativePath, | |
| expiresAt: tokenData.expiresAt | |
| }); | |
| results.success++; | |
| } catch (error) { | |
| console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message); | |
| results.details.push({ | |
| index: i + 1, | |
| success: false, | |
| error: error.message | |
| }); | |
| results.failed++; | |
| } | |
| } | |
| // 如果有成功的,广播事件并自动关联 | |
| if (results.success > 0) { | |
| broadcastEvent('oauth_batch_success', { | |
| provider: 'claude-kiro-oauth', | |
| count: results.success, | |
| timestamp: new Date().toISOString() | |
| }); | |
| // 自动关联新生成的凭据到 Pools | |
| await autoLinkProviderConfigs(CONFIG); | |
| } | |
| return results; | |
| } | |
| /** | |
| * 批量导入 Kiro refreshToken 并生成凭据文件(流式版本,支持实时进度回调) | |
| * @param {string[]} refreshTokens - refreshToken 数组 | |
| * @param {string} region - AWS 区域 (默认: us-east-1) | |
| * @param {Function} onProgress - 进度回调函数,每处理完一个 token 调用 | |
| * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false) | |
| * @returns {Promise<Object>} 批量处理结果 | |
| */ | |
| export async function batchImportKiroRefreshTokensStream(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, onProgress = null, skipDuplicateCheck = false) { | |
| const results = { | |
| total: refreshTokens.length, | |
| success: 0, | |
| failed: 0, | |
| details: [] | |
| }; | |
| for (let i = 0; i < refreshTokens.length; i++) { | |
| const refreshToken = refreshTokens[i].trim(); | |
| const progressData = { | |
| index: i + 1, | |
| total: refreshTokens.length, | |
| current: null | |
| }; | |
| if (!refreshToken) { | |
| progressData.current = { | |
| index: i + 1, | |
| success: false, | |
| error: 'Empty token' | |
| }; | |
| results.details.push(progressData.current); | |
| results.failed++; | |
| // 发送进度更新 | |
| if (onProgress) { | |
| onProgress({ | |
| ...progressData, | |
| successCount: results.success, | |
| failedCount: results.failed | |
| }); | |
| } | |
| continue; | |
| } | |
| // 检查重复 | |
| if (!skipDuplicateCheck) { | |
| const duplicateCheck = await checkKiroCredentialsDuplicate(refreshToken); | |
| if (duplicateCheck.isDuplicate) { | |
| progressData.current = { | |
| index: i + 1, | |
| success: false, | |
| error: 'duplicate', | |
| existingPath: duplicateCheck.existingPath | |
| }; | |
| results.details.push(progressData.current); | |
| results.failed++; | |
| // 发送进度更新 | |
| if (onProgress) { | |
| onProgress({ | |
| ...progressData, | |
| successCount: results.success, | |
| failedCount: results.failed | |
| }); | |
| } | |
| continue; | |
| } | |
| } | |
| try { | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`); | |
| const tokenData = await refreshKiroToken(refreshToken, region); | |
| // 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json | |
| const timestamp = Date.now(); | |
| const folderName = `${timestamp}_kiro-auth-token`; | |
| const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName); | |
| await fs.promises.mkdir(targetDir, { recursive: true }); | |
| const credPath = path.join(targetDir, `${folderName}.json`); | |
| await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2)); | |
| const relativePath = path.relative(process.cwd(), credPath); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`); | |
| progressData.current = { | |
| index: i + 1, | |
| success: true, | |
| path: relativePath, | |
| expiresAt: tokenData.expiresAt | |
| }; | |
| results.details.push(progressData.current); | |
| results.success++; | |
| } catch (error) { | |
| console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message); | |
| progressData.current = { | |
| index: i + 1, | |
| success: false, | |
| error: error.message | |
| }; | |
| results.details.push(progressData.current); | |
| results.failed++; | |
| } | |
| // 发送进度更新 | |
| if (onProgress) { | |
| onProgress({ | |
| ...progressData, | |
| successCount: results.success, | |
| failedCount: results.failed | |
| }); | |
| } | |
| } | |
| // 如果有成功的,广播事件并自动关联 | |
| if (results.success > 0) { | |
| broadcastEvent('oauth_batch_success', { | |
| provider: 'claude-kiro-oauth', | |
| count: results.success, | |
| timestamp: new Date().toISOString() | |
| }); | |
| // 自动关联新生成的凭据到 Pools | |
| await autoLinkProviderConfigs(CONFIG); | |
| } | |
| return results; | |
| } | |
| /** | |
| * 导入 AWS SSO 凭据用于 Kiro (Builder ID 模式) | |
| * 从用户上传的 AWS SSO cache 文件中导入凭据 | |
| * @param {Object} credentials - 合并后的凭据对象,需包含 clientId 和 clientSecret | |
| * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false) | |
| * @returns {Promise<Object>} 导入结果 | |
| */ | |
| export async function importAwsCredentials(credentials, skipDuplicateCheck = false) { | |
| try { | |
| // 验证必需字段 - 需要四个字段都存在 | |
| const missingFields = []; | |
| if (!credentials.clientId) missingFields.push('clientId'); | |
| if (!credentials.clientSecret) missingFields.push('clientSecret'); | |
| if (!credentials.accessToken) missingFields.push('accessToken'); | |
| if (!credentials.refreshToken) missingFields.push('refreshToken'); | |
| if (missingFields.length > 0) { | |
| return { | |
| success: false, | |
| error: `Missing required fields: ${missingFields.join(', ')}` | |
| }; | |
| } | |
| // 检查重复凭据 | |
| if (!skipDuplicateCheck) { | |
| const duplicateCheck = await checkKiroCredentialsDuplicate(credentials.refreshToken); | |
| if (duplicateCheck.isDuplicate) { | |
| return { | |
| success: false, | |
| error: 'duplicate', | |
| existingPath: duplicateCheck.existingPath | |
| }; | |
| } | |
| } | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Importing AWS credentials...`); | |
| // 准备凭据数据 - 四个字段都是必需的 | |
| const credentialsData = { | |
| clientId: credentials.clientId, | |
| clientSecret: credentials.clientSecret, | |
| accessToken: credentials.accessToken, | |
| refreshToken: credentials.refreshToken, | |
| authMethod: credentials.authMethod || 'builder-id', | |
| // region: credentials.region || KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, | |
| idcRegion: credentials.idcRegion || KIRO_REFRESH_CONSTANTS.IDC_REGION | |
| }; | |
| // 可选字段 | |
| if (credentials.expiresAt) { | |
| credentialsData.expiresAt = credentials.expiresAt; | |
| } | |
| if (credentials.startUrl) { | |
| credentialsData.startUrl = credentials.startUrl; | |
| } | |
| if (credentials.registrationExpiresAt) { | |
| credentialsData.registrationExpiresAt = credentials.registrationExpiresAt; | |
| } | |
| // 尝试刷新获取最新的 accessToken | |
| try { | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Attempting to refresh token with provided credentials...`); | |
| const refreshRegion = credentials.idcRegion || KIRO_REFRESH_CONSTANTS.IDC_REGION; | |
| const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_IDC_URL.replace('{{region}}', refreshRegion); | |
| const refreshResponse = await fetchWithProxy(refreshUrl, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| refreshToken: credentials.refreshToken, | |
| clientId: credentials.clientId, | |
| clientSecret: credentials.clientSecret, | |
| grantType: 'refresh_token' | |
| }) | |
| }, 'claude-kiro-oauth'); | |
| if (refreshResponse.ok) { | |
| const tokenData = await refreshResponse.json(); | |
| credentialsData.accessToken = tokenData.accessToken; | |
| credentialsData.refreshToken = tokenData.refreshToken; | |
| const expiresIn = tokenData.expiresIn || 3600; | |
| credentialsData.expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString(); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Token refreshed successfully`); | |
| } else { | |
| console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh failed, saving original credentials`); | |
| } | |
| } catch (refreshError) { | |
| console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh error:`, refreshError.message); | |
| // 继续保存原始凭据 | |
| } | |
| // 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json | |
| const timestamp = Date.now(); | |
| const folderName = `${timestamp}_kiro-auth-token`; | |
| const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName); | |
| await fs.promises.mkdir(targetDir, { recursive: true }); | |
| const credPath = path.join(targetDir, `${folderName}.json`); | |
| await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2)); | |
| const relativePath = path.relative(process.cwd(), credPath); | |
| console.log(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials saved to: ${relativePath}`); | |
| // 广播事件 | |
| broadcastEvent('oauth_success', { | |
| provider: 'claude-kiro-oauth', | |
| relativePath: relativePath, | |
| timestamp: new Date().toISOString() | |
| }); | |
| // 自动关联新生成的凭据到 Pools | |
| await autoLinkProviderConfigs(CONFIG); | |
| return { | |
| success: true, | |
| path: relativePath | |
| }; | |
| } catch (error) { | |
| console.error(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials import failed:`, error); | |
| return { | |
| success: false, | |
| error: error.message | |
| }; | |
| } | |
| } |