import http from 'http'; import logger from '../utils/logger.js'; 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} 返回类似 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 logger.info(`[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 ? '授权成功!' : '授权失败'; const countdownHtml = isSuccess ? `

此窗口将在 10 秒后自动关闭。

` : ''; return ` ${title}

${isSuccess ? '✅' : '❌'} ${title}

${message}

${countdownHtml}
`; } /** * 生成 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} 返回授权URL和相关信息 */ export async function handleKiroOAuth(currentConfig, options = {}) { const method = options.method || options.authMethod || 'google'; // 默认使用 Google,同时支持 authMethod 参数 logger.info(`${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; logger.info(`${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 => { logger.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); logger.info(`${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) { logger.info(`${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, { onlyCurrentCred: true, credPath: path.relative(process.cwd(), credPath) }); return tokenData; } // 检查错误类型 if (data.error === 'authorization_pending') { logger.info(`${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); logger.info(`${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 }); logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`); return port; } catch (err) { logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 端口 ${port} 被占用,尝试下一个...`); } } throw new Error('所有端口都被占用'); } /** * 关闭 Kiro 服务器 */ async function closeKiroServer(provider, port = null) { const existing = activeKiroServers.get(provider); if (existing) { try { const closePromise = new Promise((resolve, reject) => { existing.server.close((err) => { if (err) reject(err); else resolve(); }); }); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Server close timeout after 2s')), 2000); }); await Promise.race([closePromise, timeoutPromise]); logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); } catch (error) { logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} 关闭提供商 ${provider} 服务器失败或超时: ${error.message}`); } finally { activeKiroServers.delete(provider); } } if (port) { for (const [p, info] of activeKiroServers.entries()) { if (info.port === port) { await closeKiroServer(p); } } } } /** * 创建 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(); logger.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)); logger.info(`${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, { onlyCurrentCred: true, credPath: path.relative(process.cwd(), credPath) }); 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) { logger.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} 包含 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); logger.info(`${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) { logger.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} 批量处理结果 */ 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 { logger.info(`${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); logger.info(`${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) { logger.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 for (const detail of results.details) { if (detail.success && detail.path) { await autoLinkProviderConfigs(CONFIG, { onlyCurrentCred: true, credPath: detail.path }); } } } 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} 批量处理结果 */ 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 { logger.info(`${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); logger.info(`${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) { logger.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 for (const detail of results.details) { if (detail.success && detail.path) { await autoLinkProviderConfigs(CONFIG, { onlyCurrentCred: true, credPath: detail.path }); } } } return results; } /** * 导入 AWS SSO 凭据用于 Kiro (Builder ID 模式) * 从用户上传的 AWS SSO cache 文件中导入凭据 * @param {Object} credentials - 合并后的凭据对象,需包含 clientId 和 clientSecret * @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false) * @returns {Promise} 导入结果 */ 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 }; } } logger.info(`${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 { logger.info(`${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(); logger.info(`${KIRO_OAUTH_CONFIG.logPrefix} Token refreshed successfully`); } else { logger.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh failed, saving original credentials`); } } catch (refreshError) { logger.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); logger.info(`${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, { onlyCurrentCred: true, credPath: relativePath }); return { success: true, path: relativePath }; } catch (error) { logger.error(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials import failed:`, error); return { success: false, error: error.message }; } }