Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
182
backend/internal/runtime/executor/claude_executor_auth.go
Normal file
182
backend/internal/runtime/executor/claude_executor_auth.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
|
||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
claudeAccountProfileCheckedAtKey = "claude_account_profile_checked_at"
|
||||
claudeAccountProfileTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type claudeOAuthProfileFetcher func(context.Context, *cliproxyauth.Auth, string) (*claudeauth.OAuthProfile, error)
|
||||
|
||||
func (e *ClaudeExecutor) ShouldPrepareRequestAuth(auth *cliproxyauth.Auth) bool {
|
||||
apiKey, _ := claudeCreds(auth)
|
||||
if !isClaudeOAuthToken(apiKey) || auth == nil {
|
||||
return false
|
||||
}
|
||||
if !claudeauth.HasCanonicalDeviceIDPool(claudeauth.ReadDeviceIDPool(&auth.Metadata)) {
|
||||
return true
|
||||
}
|
||||
return helps.ClaudeCredentialAccountUUID(auth) == ""
|
||||
}
|
||||
|
||||
func isClaudeSetupToken(auth *cliproxyauth.Auth, apiKey string) bool {
|
||||
if !isClaudeOAuthToken(apiKey) || auth == nil {
|
||||
return false
|
||||
}
|
||||
if skip, _ := auth.Metadata["skip_account_profile"].(bool); skip {
|
||||
return true
|
||||
}
|
||||
if isSetup, _ := auth.Metadata["is_setup_token"].(bool); isSetup {
|
||||
return true
|
||||
}
|
||||
if isSetup, _ := auth.Metadata["setup_token"].(bool); isSetup {
|
||||
return true
|
||||
}
|
||||
if kind := strings.ToLower(auth.Attributes["auth_kind"]); kind == "setup_token" || kind == "setup-token" {
|
||||
return true
|
||||
}
|
||||
scopes := strings.ToLower(claudeauth.ReadMetadataString(&auth.Metadata, "scopes"))
|
||||
if scopes == "" {
|
||||
scopes = strings.ToLower(claudeauth.ReadMetadataString(&auth.Metadata, "scope"))
|
||||
}
|
||||
if scopes != "" && !strings.Contains(scopes, "user:profile") && !strings.Contains(scopes, "user:office") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isClaudeOAuthScope403(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
return strings.Contains(msg, "status 403") ||
|
||||
strings.Contains(msg, "403 forbidden") ||
|
||||
strings.Contains(msg, "403") ||
|
||||
strings.Contains(msg, "forbidden") ||
|
||||
strings.Contains(msg, "permission_error") ||
|
||||
strings.Contains(msg, "scope requirement") ||
|
||||
strings.Contains(msg, "insufficient_scope") ||
|
||||
strings.Contains(msg, "user:profile") ||
|
||||
strings.Contains(msg, "user:office")
|
||||
}
|
||||
|
||||
func (e *ClaudeExecutor) PrepareRequestAuth(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
||||
if auth == nil || !e.ShouldPrepareRequestAuth(auth) {
|
||||
return auth, nil
|
||||
}
|
||||
apiKey, _ := claudeCreds(auth)
|
||||
claudeauth.EnsureMetadataMap(&auth.Metadata)
|
||||
if _, errDeviceIDs := helps.EnsureClaudeCredentialDevicePoolRequired(ctx, auth); errDeviceIDs != nil {
|
||||
return nil, errDeviceIDs
|
||||
}
|
||||
if helps.ClaudeCredentialAccountUUID(auth) != "" {
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
if isClaudeSetupToken(auth, apiKey) {
|
||||
seed := helps.ClaudeCLIAuthIdentitySeed(auth)
|
||||
if seed == "" {
|
||||
seed = "claude-setup-token|" + apiKey
|
||||
}
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "account_uuid", helps.StableClaudeCLIAccountUUID(seed))
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, claudeAccountProfileCheckedAtKey, time.Now().UTC().Format(time.RFC3339))
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
profile, errProfile := e.fetchClaudeOAuthProfile(ctx, auth, apiKey)
|
||||
if errProfile != nil {
|
||||
if errContext := ctx.Err(); errContext != nil {
|
||||
return nil, errContext
|
||||
}
|
||||
if isClaudeOAuthScope403(errProfile) {
|
||||
log.Debugf("Claude OAuth account profile lookup returned 403 for auth %s: %v (falling back to stable credential identity)", auth.ID, errProfile)
|
||||
seed := helps.ClaudeCLIAuthIdentitySeed(auth)
|
||||
if seed == "" {
|
||||
seed = "claude-oauth-fallback|" + apiKey
|
||||
}
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "account_uuid", helps.StableClaudeCLIAccountUUID(seed))
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, claudeAccountProfileCheckedAtKey, time.Now().UTC().Format(time.RFC3339))
|
||||
return auth, nil
|
||||
}
|
||||
return nil, fmt.Errorf("populate Claude OAuth account profile: %w", errProfile)
|
||||
}
|
||||
if profile == nil || strings.TrimSpace(profile.Account.UUID) == "" {
|
||||
log.Debugf("Claude OAuth account profile lookup returned empty account UUID for auth %s (falling back to stable credential identity)", auth.ID)
|
||||
seed := helps.ClaudeCLIAuthIdentitySeed(auth)
|
||||
if seed == "" {
|
||||
seed = "claude-oauth-fallback|" + apiKey
|
||||
}
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "account_uuid", helps.StableClaudeCLIAccountUUID(seed))
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, claudeAccountProfileCheckedAtKey, time.Now().UTC().Format(time.RFC3339))
|
||||
return auth, nil
|
||||
}
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "account_uuid", profile.Account.UUID)
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "email", profile.Account.Email)
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "organization_uuid", profile.Organization.UUID)
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "organization_name", profile.Organization.Name)
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, claudeAccountProfileCheckedAtKey, time.Now().UTC().Format(time.RFC3339))
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func (e *ClaudeExecutor) fetchClaudeOAuthProfile(ctx context.Context, auth *cliproxyauth.Auth, apiKey string) (*claudeauth.OAuthProfile, error) {
|
||||
if e == nil {
|
||||
return nil, fmt.Errorf("fetch Claude OAuth profile: executor is nil")
|
||||
}
|
||||
if e.oauthProfileFetcher != nil {
|
||||
return e.oauthProfileFetcher(ctx, auth, apiKey)
|
||||
}
|
||||
if auth == nil {
|
||||
return nil, fmt.Errorf("fetch Claude OAuth profile: auth is nil")
|
||||
}
|
||||
profileCtx, cancelProfile := context.WithTimeout(ctx, claudeAccountProfileTimeout)
|
||||
defer cancelProfile()
|
||||
service := claudeauth.NewClaudeAuthWithProxyURL(e.cfg, auth.ProxyURL)
|
||||
return service.FetchOAuthProfile(profileCtx, apiKey)
|
||||
}
|
||||
|
||||
func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
||||
log.Debugf("claude executor: refresh called")
|
||||
if refreshed, handled, err := helps.RefreshAuthViaHome(ctx, e.cfg, auth); handled {
|
||||
return refreshed, err
|
||||
}
|
||||
if auth == nil {
|
||||
return nil, fmt.Errorf("claude executor: auth is nil")
|
||||
}
|
||||
refreshToken := claudeauth.ReadMetadataString(&auth.Metadata, "refresh_token")
|
||||
if refreshToken == "" {
|
||||
refreshToken = claudeauth.ReadMetadataString(&auth.Metadata, "refreshToken")
|
||||
}
|
||||
if refreshToken == "" {
|
||||
return auth, nil
|
||||
}
|
||||
svc := claudeauth.NewClaudeAuthWithProxyURL(e.cfg, auth.ProxyURL)
|
||||
td, err := svc.RefreshTokensWithRetry(ctx, refreshToken, 3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claudeauth.EnsureMetadataMap(&auth.Metadata)
|
||||
claudeauth.StoreMetadataValue(&auth.Metadata, "access_token", td.AccessToken)
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "refresh_token", td.RefreshToken)
|
||||
// Profile fields are optional when token rotation succeeds but the follow-up
|
||||
// profile lookup fails. Never erase the previously resolved credential identity.
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "email", td.Email)
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "account_uuid", td.AccountUUID)
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "organization_uuid", td.OrganizationUUID)
|
||||
claudeauth.StoreMetadataString(&auth.Metadata, "organization_name", td.OrganizationName)
|
||||
claudeauth.StoreMetadataValue(&auth.Metadata, "expired", td.Expire)
|
||||
claudeauth.StoreMetadataValue(&auth.Metadata, "type", "claude")
|
||||
claudeauth.StoreMetadataValue(&auth.Metadata, "last_refresh", time.Now().Format(time.RFC3339))
|
||||
return auth, nil
|
||||
}
|
||||
Loading…
Reference in a new issue