aiclient-2-api / src /auth /kiro-oauth.js
Jaasomn
Initial deployment
ceb3821
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
};
}
}