c4c1d4daa4
Sniffs a real Claude Code request on startup to capture exact HTTP headers, then replays them for all proxied requests. Injects the billing header with per-request SHA256 fingerprint into the system prompt. Uses utls with Chrome TLS fingerprint to pass Cloudflare's bot detection on api.anthropic.com. Supports both streaming (SSE) and non-streaming modes, round-robin credential selection with automatic failover, and loading OAuth tokens from both cli-proxy-api auth files and native ~/.claude/.credentials.json.
110 lines
2.7 KiB
Go
110 lines
2.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
tokenEndpoint = "https://api.anthropic.com/v1/oauth/token"
|
|
clientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
|
)
|
|
|
|
type tokenRequest struct {
|
|
ClientID string `json:"client_id"`
|
|
GrantType string `json:"grant_type"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
}
|
|
|
|
type tokenResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
Account struct {
|
|
EmailAddress string `json:"email_address"`
|
|
} `json:"account"`
|
|
}
|
|
|
|
type authFileJSON struct {
|
|
AccessToken string `json:"access_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
Email string `json:"email"`
|
|
Expired string `json:"expired"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// RefreshToken performs an OAuth token refresh for the given credential.
|
|
func RefreshToken(ctx context.Context, cred *Credential) error {
|
|
reqBody := tokenRequest{
|
|
ClientID: clientID,
|
|
GrantType: "refresh_token",
|
|
RefreshToken: cred.RefreshToken,
|
|
}
|
|
|
|
body, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal refresh request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenEndpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return fmt.Errorf("create refresh request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("execute refresh request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("refresh failed with status %d", resp.StatusCode)
|
|
}
|
|
|
|
var tokenResp tokenResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
|
return fmt.Errorf("decode refresh response: %w", err)
|
|
}
|
|
|
|
cred.mu.Lock()
|
|
cred.AccessToken = tokenResp.AccessToken
|
|
cred.RefreshToken = tokenResp.RefreshToken
|
|
cred.ExpiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
|
|
if tokenResp.Account.EmailAddress != "" {
|
|
cred.Email = tokenResp.Account.EmailAddress
|
|
}
|
|
cred.mu.Unlock()
|
|
|
|
return persistCredential(cred)
|
|
}
|
|
|
|
func persistCredential(cred *Credential) error {
|
|
cred.mu.Lock()
|
|
data := authFileJSON{
|
|
AccessToken: cred.AccessToken,
|
|
RefreshToken: cred.RefreshToken,
|
|
Email: cred.Email,
|
|
Expired: cred.ExpiresAt.Format(time.RFC3339),
|
|
Type: "claude",
|
|
}
|
|
filePath := cred.FilePath
|
|
cred.mu.Unlock()
|
|
|
|
out, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("marshal auth file: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(filePath, out, 0600); err != nil {
|
|
return fmt.Errorf("write auth file %s: %w", filePath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|