mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-30 01:06:39 +00:00
## 中文说明 ### 连接池优化 - 为 AMP 代理、SOCKS5 代理和 HTTP 代理配置优化的连接池参数 - MaxIdleConnsPerHost 从默认的 2 增加到 20,支持更多并发用户 - MaxConnsPerHost 设为 0(无限制),避免连接瓶颈 - 添加 IdleConnTimeout (90s) 和其他超时配置 ### Kiro 执行器增强 - 添加 Event Stream 消息解析的边界保护,防止越界访问 - 实现实时使用量估算(每 5000 字符或 15 秒发送 ping 事件) - 正确从上游事件中提取并传递 stop_reason - 改进输入 token 计算,优先使用 Claude 格式解析 - 添加 max_tokens 截断警告日志 ### Token 计算改进 - 添加 tokenizer 缓存(sync.Map)避免重复创建 - 为 Claude/Kiro/AmazonQ 模型添加 1.1 调整因子 - 新增 countClaudeChatTokens 函数支持 Claude API 格式 - 支持图像 token 估算(基于尺寸计算) ### 认证刷新优化 - RefreshLead 从 30 分钟改为 5 分钟,与 Antigravity 保持一致 - 修复 NextRefreshAfter 设置,防止频繁刷新检查 - refreshFailureBackoff 从 5 分钟改为 1 分钟,加快失败恢复 --- ## English Description ### Connection Pool Optimization - Configure optimized connection pool parameters for AMP proxy, SOCKS5 proxy, and HTTP proxy - Increase MaxIdleConnsPerHost from default 2 to 20 to support more concurrent users - Set MaxConnsPerHost to 0 (unlimited) to avoid connection bottlenecks - Add IdleConnTimeout (90s) and other timeout configurations ### Kiro Executor Enhancements - Add boundary protection for Event Stream message parsing to prevent out-of-bounds access - Implement real-time usage estimation (send ping events every 5000 chars or 15 seconds) - Correctly extract and pass stop_reason from upstream events - Improve input token calculation, prioritize Claude format parsing - Add max_tokens truncation warning logs ### Token Calculation Improvements - Add tokenizer cache (sync.Map) to avoid repeated creation - Add 1.1 adjustment factor for Claude/Kiro/AmazonQ models - Add countClaudeChatTokens function to support Claude API format - Support image token estimation (calculated based on dimensions) ### Authentication Refresh Optimization - Change RefreshLead from 30 minutes to 5 minutes, consistent with Antigravity - Fix NextRefreshAfter setting to prevent frequent refresh checks - Change refreshFailureBackoff from 5 minutes to 1 minute for faster failure recovery
364 lines
11 KiB
Go
364 lines
11 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
kiroauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kiro"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
|
)
|
|
|
|
// extractKiroIdentifier extracts a meaningful identifier for file naming.
|
|
// Returns account name if provided, otherwise profile ARN ID.
|
|
// All extracted values are sanitized to prevent path injection attacks.
|
|
func extractKiroIdentifier(accountName, profileArn string) string {
|
|
// Priority 1: Use account name if provided
|
|
if accountName != "" {
|
|
return kiroauth.SanitizeEmailForFilename(accountName)
|
|
}
|
|
|
|
// Priority 2: Use profile ARN ID part (sanitized to prevent path injection)
|
|
if profileArn != "" {
|
|
parts := strings.Split(profileArn, "/")
|
|
if len(parts) >= 2 {
|
|
// Sanitize the ARN component to prevent path traversal
|
|
return kiroauth.SanitizeEmailForFilename(parts[len(parts)-1])
|
|
}
|
|
}
|
|
|
|
// Fallback: timestamp
|
|
return fmt.Sprintf("%d", time.Now().UnixNano()%100000)
|
|
}
|
|
|
|
// KiroAuthenticator implements OAuth authentication for Kiro with Google login.
|
|
type KiroAuthenticator struct{}
|
|
|
|
// NewKiroAuthenticator constructs a Kiro authenticator.
|
|
func NewKiroAuthenticator() *KiroAuthenticator {
|
|
return &KiroAuthenticator{}
|
|
}
|
|
|
|
// Provider returns the provider key for the authenticator.
|
|
func (a *KiroAuthenticator) Provider() string {
|
|
return "kiro"
|
|
}
|
|
|
|
// RefreshLead indicates how soon before expiry a refresh should be attempted.
|
|
// Set to 5 minutes to match Antigravity and avoid frequent refresh checks while still ensuring timely token refresh.
|
|
func (a *KiroAuthenticator) RefreshLead() *time.Duration {
|
|
d := 5 * time.Minute
|
|
return &d
|
|
}
|
|
|
|
// Login performs OAuth login for Kiro with AWS Builder ID.
|
|
func (a *KiroAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("kiro auth: configuration is required")
|
|
}
|
|
|
|
oauth := kiroauth.NewKiroOAuth(cfg)
|
|
|
|
// Use AWS Builder ID device code flow
|
|
tokenData, err := oauth.LoginWithBuilderID(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("login failed: %w", err)
|
|
}
|
|
|
|
// Parse expires_at
|
|
expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt)
|
|
if err != nil {
|
|
expiresAt = time.Now().Add(1 * time.Hour)
|
|
}
|
|
|
|
// Extract identifier for file naming
|
|
idPart := extractKiroIdentifier(tokenData.Email, tokenData.ProfileArn)
|
|
|
|
now := time.Now()
|
|
fileName := fmt.Sprintf("kiro-aws-%s.json", idPart)
|
|
|
|
record := &coreauth.Auth{
|
|
ID: fileName,
|
|
Provider: "kiro",
|
|
FileName: fileName,
|
|
Label: "kiro-aws",
|
|
Status: coreauth.StatusActive,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
Metadata: map[string]any{
|
|
"type": "kiro",
|
|
"access_token": tokenData.AccessToken,
|
|
"refresh_token": tokenData.RefreshToken,
|
|
"profile_arn": tokenData.ProfileArn,
|
|
"expires_at": tokenData.ExpiresAt,
|
|
"auth_method": tokenData.AuthMethod,
|
|
"provider": tokenData.Provider,
|
|
"client_id": tokenData.ClientID,
|
|
"client_secret": tokenData.ClientSecret,
|
|
"email": tokenData.Email,
|
|
},
|
|
Attributes: map[string]string{
|
|
"profile_arn": tokenData.ProfileArn,
|
|
"source": "aws-builder-id",
|
|
"email": tokenData.Email,
|
|
},
|
|
// NextRefreshAfter is aligned with RefreshLead (5min)
|
|
NextRefreshAfter: expiresAt.Add(-5 * time.Minute),
|
|
}
|
|
|
|
if tokenData.Email != "" {
|
|
fmt.Printf("\n✓ Kiro authentication completed successfully! (Account: %s)\n", tokenData.Email)
|
|
} else {
|
|
fmt.Println("\n✓ Kiro authentication completed successfully!")
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
// LoginWithGoogle performs OAuth login for Kiro with Google.
|
|
// This uses a custom protocol handler (kiro://) to receive the callback.
|
|
func (a *KiroAuthenticator) LoginWithGoogle(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("kiro auth: configuration is required")
|
|
}
|
|
|
|
oauth := kiroauth.NewKiroOAuth(cfg)
|
|
|
|
// Use Google OAuth flow with protocol handler
|
|
tokenData, err := oauth.LoginWithGoogle(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("google login failed: %w", err)
|
|
}
|
|
|
|
// Parse expires_at
|
|
expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt)
|
|
if err != nil {
|
|
expiresAt = time.Now().Add(1 * time.Hour)
|
|
}
|
|
|
|
// Extract identifier for file naming
|
|
idPart := extractKiroIdentifier(tokenData.Email, tokenData.ProfileArn)
|
|
|
|
now := time.Now()
|
|
fileName := fmt.Sprintf("kiro-google-%s.json", idPart)
|
|
|
|
record := &coreauth.Auth{
|
|
ID: fileName,
|
|
Provider: "kiro",
|
|
FileName: fileName,
|
|
Label: "kiro-google",
|
|
Status: coreauth.StatusActive,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
Metadata: map[string]any{
|
|
"type": "kiro",
|
|
"access_token": tokenData.AccessToken,
|
|
"refresh_token": tokenData.RefreshToken,
|
|
"profile_arn": tokenData.ProfileArn,
|
|
"expires_at": tokenData.ExpiresAt,
|
|
"auth_method": tokenData.AuthMethod,
|
|
"provider": tokenData.Provider,
|
|
"email": tokenData.Email,
|
|
},
|
|
Attributes: map[string]string{
|
|
"profile_arn": tokenData.ProfileArn,
|
|
"source": "google-oauth",
|
|
"email": tokenData.Email,
|
|
},
|
|
// NextRefreshAfter is aligned with RefreshLead (5min)
|
|
NextRefreshAfter: expiresAt.Add(-5 * time.Minute),
|
|
}
|
|
|
|
if tokenData.Email != "" {
|
|
fmt.Printf("\n✓ Kiro Google authentication completed successfully! (Account: %s)\n", tokenData.Email)
|
|
} else {
|
|
fmt.Println("\n✓ Kiro Google authentication completed successfully!")
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
// LoginWithGitHub performs OAuth login for Kiro with GitHub.
|
|
// This uses a custom protocol handler (kiro://) to receive the callback.
|
|
func (a *KiroAuthenticator) LoginWithGitHub(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("kiro auth: configuration is required")
|
|
}
|
|
|
|
oauth := kiroauth.NewKiroOAuth(cfg)
|
|
|
|
// Use GitHub OAuth flow with protocol handler
|
|
tokenData, err := oauth.LoginWithGitHub(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("github login failed: %w", err)
|
|
}
|
|
|
|
// Parse expires_at
|
|
expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt)
|
|
if err != nil {
|
|
expiresAt = time.Now().Add(1 * time.Hour)
|
|
}
|
|
|
|
// Extract identifier for file naming
|
|
idPart := extractKiroIdentifier(tokenData.Email, tokenData.ProfileArn)
|
|
|
|
now := time.Now()
|
|
fileName := fmt.Sprintf("kiro-github-%s.json", idPart)
|
|
|
|
record := &coreauth.Auth{
|
|
ID: fileName,
|
|
Provider: "kiro",
|
|
FileName: fileName,
|
|
Label: "kiro-github",
|
|
Status: coreauth.StatusActive,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
Metadata: map[string]any{
|
|
"type": "kiro",
|
|
"access_token": tokenData.AccessToken,
|
|
"refresh_token": tokenData.RefreshToken,
|
|
"profile_arn": tokenData.ProfileArn,
|
|
"expires_at": tokenData.ExpiresAt,
|
|
"auth_method": tokenData.AuthMethod,
|
|
"provider": tokenData.Provider,
|
|
"email": tokenData.Email,
|
|
},
|
|
Attributes: map[string]string{
|
|
"profile_arn": tokenData.ProfileArn,
|
|
"source": "github-oauth",
|
|
"email": tokenData.Email,
|
|
},
|
|
// NextRefreshAfter is aligned with RefreshLead (5min)
|
|
NextRefreshAfter: expiresAt.Add(-5 * time.Minute),
|
|
}
|
|
|
|
if tokenData.Email != "" {
|
|
fmt.Printf("\n✓ Kiro GitHub authentication completed successfully! (Account: %s)\n", tokenData.Email)
|
|
} else {
|
|
fmt.Println("\n✓ Kiro GitHub authentication completed successfully!")
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
// ImportFromKiroIDE imports token from Kiro IDE's token file.
|
|
func (a *KiroAuthenticator) ImportFromKiroIDE(ctx context.Context, cfg *config.Config) (*coreauth.Auth, error) {
|
|
tokenData, err := kiroauth.LoadKiroIDEToken()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load Kiro IDE token: %w", err)
|
|
}
|
|
|
|
// Parse expires_at
|
|
expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt)
|
|
if err != nil {
|
|
expiresAt = time.Now().Add(1 * time.Hour)
|
|
}
|
|
|
|
// Extract email from JWT if not already set (for imported tokens)
|
|
if tokenData.Email == "" {
|
|
tokenData.Email = kiroauth.ExtractEmailFromJWT(tokenData.AccessToken)
|
|
}
|
|
|
|
// Extract identifier for file naming
|
|
idPart := extractKiroIdentifier(tokenData.Email, tokenData.ProfileArn)
|
|
// Sanitize provider to prevent path traversal (defense-in-depth)
|
|
provider := kiroauth.SanitizeEmailForFilename(strings.ToLower(strings.TrimSpace(tokenData.Provider)))
|
|
if provider == "" {
|
|
provider = "imported" // Fallback for legacy tokens without provider
|
|
}
|
|
|
|
now := time.Now()
|
|
fileName := fmt.Sprintf("kiro-%s-%s.json", provider, idPart)
|
|
|
|
record := &coreauth.Auth{
|
|
ID: fileName,
|
|
Provider: "kiro",
|
|
FileName: fileName,
|
|
Label: fmt.Sprintf("kiro-%s", provider),
|
|
Status: coreauth.StatusActive,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
Metadata: map[string]any{
|
|
"type": "kiro",
|
|
"access_token": tokenData.AccessToken,
|
|
"refresh_token": tokenData.RefreshToken,
|
|
"profile_arn": tokenData.ProfileArn,
|
|
"expires_at": tokenData.ExpiresAt,
|
|
"auth_method": tokenData.AuthMethod,
|
|
"provider": tokenData.Provider,
|
|
"email": tokenData.Email,
|
|
},
|
|
Attributes: map[string]string{
|
|
"profile_arn": tokenData.ProfileArn,
|
|
"source": "kiro-ide-import",
|
|
"email": tokenData.Email,
|
|
},
|
|
// NextRefreshAfter is aligned with RefreshLead (5min)
|
|
NextRefreshAfter: expiresAt.Add(-5 * time.Minute),
|
|
}
|
|
|
|
// Display the email if extracted
|
|
if tokenData.Email != "" {
|
|
fmt.Printf("\n✓ Imported Kiro token from IDE (Provider: %s, Account: %s)\n", tokenData.Provider, tokenData.Email)
|
|
} else {
|
|
fmt.Printf("\n✓ Imported Kiro token from IDE (Provider: %s)\n", tokenData.Provider)
|
|
}
|
|
|
|
return record, nil
|
|
}
|
|
|
|
// Refresh refreshes an expired Kiro token using AWS SSO OIDC.
|
|
func (a *KiroAuthenticator) Refresh(ctx context.Context, cfg *config.Config, auth *coreauth.Auth) (*coreauth.Auth, error) {
|
|
if auth == nil || auth.Metadata == nil {
|
|
return nil, fmt.Errorf("invalid auth record")
|
|
}
|
|
|
|
refreshToken, ok := auth.Metadata["refresh_token"].(string)
|
|
if !ok || refreshToken == "" {
|
|
return nil, fmt.Errorf("refresh token not found")
|
|
}
|
|
|
|
clientID, _ := auth.Metadata["client_id"].(string)
|
|
clientSecret, _ := auth.Metadata["client_secret"].(string)
|
|
authMethod, _ := auth.Metadata["auth_method"].(string)
|
|
|
|
var tokenData *kiroauth.KiroTokenData
|
|
var err error
|
|
|
|
// Use SSO OIDC refresh for AWS Builder ID, otherwise use Kiro's OAuth refresh endpoint
|
|
if clientID != "" && clientSecret != "" && authMethod == "builder-id" {
|
|
ssoClient := kiroauth.NewSSOOIDCClient(cfg)
|
|
tokenData, err = ssoClient.RefreshToken(ctx, clientID, clientSecret, refreshToken)
|
|
} else {
|
|
// Fallback to Kiro's refresh endpoint (for social auth: Google/GitHub)
|
|
oauth := kiroauth.NewKiroOAuth(cfg)
|
|
tokenData, err = oauth.RefreshToken(ctx, refreshToken)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("token refresh failed: %w", err)
|
|
}
|
|
|
|
// Parse expires_at
|
|
expiresAt, err := time.Parse(time.RFC3339, tokenData.ExpiresAt)
|
|
if err != nil {
|
|
expiresAt = time.Now().Add(1 * time.Hour)
|
|
}
|
|
|
|
// Clone auth to avoid mutating the input parameter
|
|
updated := auth.Clone()
|
|
now := time.Now()
|
|
updated.UpdatedAt = now
|
|
updated.LastRefreshedAt = now
|
|
updated.Metadata["access_token"] = tokenData.AccessToken
|
|
updated.Metadata["refresh_token"] = tokenData.RefreshToken
|
|
updated.Metadata["expires_at"] = tokenData.ExpiresAt
|
|
updated.Metadata["last_refresh"] = now.Format(time.RFC3339) // For double-check optimization
|
|
// NextRefreshAfter is aligned with RefreshLead (5min)
|
|
updated.NextRefreshAfter = expiresAt.Add(-5 * time.Minute)
|
|
|
|
return updated, nil
|
|
}
|