Add claude code multiplexer service
This commit is contained in:
136
service/ccm/credential.go
Normal file
136
service/ccm/credential.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package ccm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
const (
|
||||
oauth2ClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
||||
oauth2TokenURL = "https://console.anthropic.com/v1/oauth/token"
|
||||
claudeAPIBaseURL = "https://api.anthropic.com"
|
||||
tokenRefreshBufferMs = 60000
|
||||
anthropicBetaOAuthValue = "oauth-2025-04-20"
|
||||
)
|
||||
|
||||
func getRealUser() (*user.User, error) {
|
||||
if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
|
||||
sudoUserInfo, err := user.Lookup(sudoUser)
|
||||
if err == nil {
|
||||
return sudoUserInfo, nil
|
||||
}
|
||||
}
|
||||
return user.Current()
|
||||
}
|
||||
|
||||
func getDefaultCredentialsPath() (string, error) {
|
||||
userInfo, err := getRealUser()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(userInfo.HomeDir, ".claude", ".credentials.json"), nil
|
||||
}
|
||||
|
||||
func readCredentialsFromFile(path string) (*oauthCredentials, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var credentialsContainer struct {
|
||||
ClaudeAIAuth *oauthCredentials `json:"claudeAiOauth,omitempty"`
|
||||
}
|
||||
err = json.Unmarshal(data, &credentialsContainer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if credentialsContainer.ClaudeAIAuth == nil {
|
||||
return nil, E.New("claudeAiOauth field not found in credentials")
|
||||
}
|
||||
return credentialsContainer.ClaudeAIAuth, nil
|
||||
}
|
||||
|
||||
func writeCredentialsToFile(oauthCredentials *oauthCredentials, path string) error {
|
||||
data, err := json.MarshalIndent(map[string]any{
|
||||
"claudeAiOauth": oauthCredentials,
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0o600)
|
||||
}
|
||||
|
||||
type oauthCredentials struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
SubscriptionType string `json:"subscriptionType,omitempty"`
|
||||
IsMax bool `json:"isMax,omitempty"`
|
||||
}
|
||||
|
||||
func (c *oauthCredentials) needsRefresh() bool {
|
||||
if c.ExpiresAt == 0 {
|
||||
return false
|
||||
}
|
||||
return time.Now().UnixMilli() >= c.ExpiresAt-tokenRefreshBufferMs
|
||||
}
|
||||
|
||||
func refreshToken(httpClient *http.Client, credentials *oauthCredentials) (*oauthCredentials, error) {
|
||||
if credentials.RefreshToken == "" {
|
||||
return nil, E.New("refresh token is empty")
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(map[string]string{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": credentials.RefreshToken,
|
||||
"client_id": oauth2ClientID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "marshal request")
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", oauth2TokenURL, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Accept", "application/json")
|
||||
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return nil, E.New("refresh failed: ", response.Status, " ", string(body))
|
||||
}
|
||||
|
||||
var tokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
err = json.NewDecoder(response.Body).Decode(&tokenResponse)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode response")
|
||||
}
|
||||
|
||||
newCredentials := *credentials
|
||||
newCredentials.AccessToken = tokenResponse.AccessToken
|
||||
if tokenResponse.RefreshToken != "" {
|
||||
newCredentials.RefreshToken = tokenResponse.RefreshToken
|
||||
}
|
||||
newCredentials.ExpiresAt = time.Now().UnixMilli() + int64(tokenResponse.ExpiresIn)*1000
|
||||
|
||||
return &newCredentials, nil
|
||||
}
|
||||
Reference in New Issue
Block a user