mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-28 13:36:08 +08:00
Add additive Codex device-code login flow
This commit is contained in:
@@ -58,6 +58,7 @@ func main() {
|
|||||||
// Command-line flags to control the application's behavior.
|
// Command-line flags to control the application's behavior.
|
||||||
var login bool
|
var login bool
|
||||||
var codexLogin bool
|
var codexLogin bool
|
||||||
|
var codexDeviceLogin bool
|
||||||
var claudeLogin bool
|
var claudeLogin bool
|
||||||
var qwenLogin bool
|
var qwenLogin bool
|
||||||
var iflowLogin bool
|
var iflowLogin bool
|
||||||
@@ -76,6 +77,7 @@ func main() {
|
|||||||
// Define command-line flags for different operation modes.
|
// Define command-line flags for different operation modes.
|
||||||
flag.BoolVar(&login, "login", false, "Login Google Account")
|
flag.BoolVar(&login, "login", false, "Login Google Account")
|
||||||
flag.BoolVar(&codexLogin, "codex-login", false, "Login to Codex using OAuth")
|
flag.BoolVar(&codexLogin, "codex-login", false, "Login to Codex using OAuth")
|
||||||
|
flag.BoolVar(&codexDeviceLogin, "codex-device-login", false, "Login to Codex using device code flow")
|
||||||
flag.BoolVar(&claudeLogin, "claude-login", false, "Login to Claude using OAuth")
|
flag.BoolVar(&claudeLogin, "claude-login", false, "Login to Claude using OAuth")
|
||||||
flag.BoolVar(&qwenLogin, "qwen-login", false, "Login to Qwen using OAuth")
|
flag.BoolVar(&qwenLogin, "qwen-login", false, "Login to Qwen using OAuth")
|
||||||
flag.BoolVar(&iflowLogin, "iflow-login", false, "Login to iFlow using OAuth")
|
flag.BoolVar(&iflowLogin, "iflow-login", false, "Login to iFlow using OAuth")
|
||||||
@@ -467,6 +469,9 @@ func main() {
|
|||||||
} else if codexLogin {
|
} else if codexLogin {
|
||||||
// Handle Codex login
|
// Handle Codex login
|
||||||
cmd.DoCodexLogin(cfg, options)
|
cmd.DoCodexLogin(cfg, options)
|
||||||
|
} else if codexDeviceLogin {
|
||||||
|
// Handle Codex device-code login
|
||||||
|
cmd.DoCodexDeviceLogin(cfg, options)
|
||||||
} else if claudeLogin {
|
} else if claudeLogin {
|
||||||
// Handle Claude login
|
// Handle Claude login
|
||||||
cmd.DoClaudeLogin(cfg, options)
|
cmd.DoClaudeLogin(cfg, options)
|
||||||
|
|||||||
@@ -71,16 +71,26 @@ func (o *CodexAuth) GenerateAuthURL(state string, pkceCodes *PKCECodes) (string,
|
|||||||
// It performs an HTTP POST request to the OpenAI token endpoint with the provided
|
// It performs an HTTP POST request to the OpenAI token endpoint with the provided
|
||||||
// authorization code and PKCE verifier.
|
// authorization code and PKCE verifier.
|
||||||
func (o *CodexAuth) ExchangeCodeForTokens(ctx context.Context, code string, pkceCodes *PKCECodes) (*CodexAuthBundle, error) {
|
func (o *CodexAuth) ExchangeCodeForTokens(ctx context.Context, code string, pkceCodes *PKCECodes) (*CodexAuthBundle, error) {
|
||||||
|
return o.ExchangeCodeForTokensWithRedirect(ctx, code, RedirectURI, pkceCodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeCodeForTokensWithRedirect exchanges an authorization code for tokens using
|
||||||
|
// a caller-provided redirect URI. This supports alternate auth flows such as device
|
||||||
|
// login while preserving the existing token parsing and storage behavior.
|
||||||
|
func (o *CodexAuth) ExchangeCodeForTokensWithRedirect(ctx context.Context, code, redirectURI string, pkceCodes *PKCECodes) (*CodexAuthBundle, error) {
|
||||||
if pkceCodes == nil {
|
if pkceCodes == nil {
|
||||||
return nil, fmt.Errorf("PKCE codes are required for token exchange")
|
return nil, fmt.Errorf("PKCE codes are required for token exchange")
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(redirectURI) == "" {
|
||||||
|
return nil, fmt.Errorf("redirect URI is required for token exchange")
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare token exchange request
|
// Prepare token exchange request
|
||||||
data := url.Values{
|
data := url.Values{
|
||||||
"grant_type": {"authorization_code"},
|
"grant_type": {"authorization_code"},
|
||||||
"client_id": {ClientID},
|
"client_id": {ClientID},
|
||||||
"code": {code},
|
"code": {code},
|
||||||
"redirect_uri": {RedirectURI},
|
"redirect_uri": {strings.TrimSpace(redirectURI)},
|
||||||
"code_verifier": {pkceCodes.CodeVerifier},
|
"code_verifier": {pkceCodes.CodeVerifier},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
internal/cmd/openai_device_login.go
Normal file
60
internal/cmd/openai_device_login.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
codexLoginModeMetadataKey = "codex_login_mode"
|
||||||
|
codexLoginModeDevice = "device"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DoCodexDeviceLogin triggers the Codex device-code flow while keeping the
|
||||||
|
// existing codex-login OAuth callback flow intact.
|
||||||
|
func DoCodexDeviceLogin(cfg *config.Config, options *LoginOptions) {
|
||||||
|
if options == nil {
|
||||||
|
options = &LoginOptions{}
|
||||||
|
}
|
||||||
|
|
||||||
|
promptFn := options.Prompt
|
||||||
|
if promptFn == nil {
|
||||||
|
promptFn = defaultProjectPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := newAuthManager()
|
||||||
|
|
||||||
|
authOpts := &sdkAuth.LoginOptions{
|
||||||
|
NoBrowser: options.NoBrowser,
|
||||||
|
CallbackPort: options.CallbackPort,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
codexLoginModeMetadataKey: codexLoginModeDevice,
|
||||||
|
},
|
||||||
|
Prompt: promptFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, savedPath, err := manager.Login(context.Background(), "codex", cfg, authOpts)
|
||||||
|
if err != nil {
|
||||||
|
if authErr, ok := errors.AsType[*codex.AuthenticationError](err); ok {
|
||||||
|
log.Error(codex.GetUserFriendlyMessage(authErr))
|
||||||
|
if authErr.Type == codex.ErrPortInUse.Type {
|
||||||
|
os.Exit(codex.ErrPortInUse.Code)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Codex device authentication failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if savedPath != "" {
|
||||||
|
fmt.Printf("Authentication saved to %s\n", savedPath)
|
||||||
|
}
|
||||||
|
fmt.Println("Codex device authentication successful!")
|
||||||
|
}
|
||||||
@@ -2,8 +2,6 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -48,6 +46,10 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
|||||||
opts = &LoginOptions{}
|
opts = &LoginOptions{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldUseCodexDeviceFlow(opts) {
|
||||||
|
return a.loginWithDeviceFlow(ctx, cfg, opts)
|
||||||
|
}
|
||||||
|
|
||||||
callbackPort := a.CallbackPort
|
callbackPort := a.CallbackPort
|
||||||
if opts.CallbackPort > 0 {
|
if opts.CallbackPort > 0 {
|
||||||
callbackPort = opts.CallbackPort
|
callbackPort = opts.CallbackPort
|
||||||
@@ -186,39 +188,5 @@ waitForCallback:
|
|||||||
return nil, codex.NewAuthenticationError(codex.ErrCodeExchangeFailed, err)
|
return nil, codex.NewAuthenticationError(codex.ErrCodeExchangeFailed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenStorage := authSvc.CreateTokenStorage(authBundle)
|
return a.buildAuthRecord(authSvc, authBundle)
|
||||||
|
|
||||||
if tokenStorage == nil || tokenStorage.Email == "" {
|
|
||||||
return nil, fmt.Errorf("codex token storage missing account information")
|
|
||||||
}
|
|
||||||
|
|
||||||
planType := ""
|
|
||||||
hashAccountID := ""
|
|
||||||
if tokenStorage.IDToken != "" {
|
|
||||||
if claims, errParse := codex.ParseJWTToken(tokenStorage.IDToken); errParse == nil && claims != nil {
|
|
||||||
planType = strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType)
|
|
||||||
accountID := strings.TrimSpace(claims.CodexAuthInfo.ChatgptAccountID)
|
|
||||||
if accountID != "" {
|
|
||||||
digest := sha256.Sum256([]byte(accountID))
|
|
||||||
hashAccountID = hex.EncodeToString(digest[:])[:8]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fileName := codex.CredentialFileName(tokenStorage.Email, planType, hashAccountID, true)
|
|
||||||
metadata := map[string]any{
|
|
||||||
"email": tokenStorage.Email,
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Codex authentication successful")
|
|
||||||
if authBundle.APIKey != "" {
|
|
||||||
fmt.Println("Codex API key obtained and stored")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &coreauth.Auth{
|
|
||||||
ID: fileName,
|
|
||||||
Provider: a.Provider(),
|
|
||||||
FileName: fileName,
|
|
||||||
Storage: tokenStorage,
|
|
||||||
Metadata: metadata,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
291
sdk/auth/codex_device.go
Normal file
291
sdk/auth/codex_device.go
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
codexLoginModeMetadataKey = "codex_login_mode"
|
||||||
|
codexLoginModeDevice = "device"
|
||||||
|
codexDeviceUserCodeURL = "https://auth.openai.com/api/accounts/deviceauth/usercode"
|
||||||
|
codexDeviceTokenURL = "https://auth.openai.com/api/accounts/deviceauth/token"
|
||||||
|
codexDeviceVerificationURL = "https://auth.openai.com/codex/device"
|
||||||
|
codexDeviceTokenExchangeRedirectURI = "https://auth.openai.com/deviceauth/callback"
|
||||||
|
codexDeviceTimeout = 15 * time.Minute
|
||||||
|
codexDeviceDefaultPollIntervalSeconds = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
type codexDeviceUserCodeRequest struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type codexDeviceUserCodeResponse struct {
|
||||||
|
DeviceAuthID string `json:"device_auth_id"`
|
||||||
|
UserCode string `json:"user_code"`
|
||||||
|
UserCodeAlt string `json:"usercode"`
|
||||||
|
Interval json.RawMessage `json:"interval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type codexDeviceTokenRequest struct {
|
||||||
|
DeviceAuthID string `json:"device_auth_id"`
|
||||||
|
UserCode string `json:"user_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type codexDeviceTokenResponse struct {
|
||||||
|
AuthorizationCode string `json:"authorization_code"`
|
||||||
|
CodeVerifier string `json:"code_verifier"`
|
||||||
|
CodeChallenge string `json:"code_challenge"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldUseCodexDeviceFlow(opts *LoginOptions) bool {
|
||||||
|
if opts == nil || opts.Metadata == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.EqualFold(strings.TrimSpace(opts.Metadata[codexLoginModeMetadataKey]), codexLoginModeDevice)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CodexAuthenticator) loginWithDeviceFlow(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := util.SetProxy(&cfg.SDKConfig, &http.Client{})
|
||||||
|
|
||||||
|
userCodeResp, err := requestCodexDeviceUserCode(ctx, httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceCode := strings.TrimSpace(userCodeResp.UserCode)
|
||||||
|
if deviceCode == "" {
|
||||||
|
deviceCode = strings.TrimSpace(userCodeResp.UserCodeAlt)
|
||||||
|
}
|
||||||
|
deviceAuthID := strings.TrimSpace(userCodeResp.DeviceAuthID)
|
||||||
|
if deviceCode == "" || deviceAuthID == "" {
|
||||||
|
return nil, fmt.Errorf("codex device flow did not return required fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
pollInterval := parseCodexDevicePollInterval(userCodeResp.Interval)
|
||||||
|
|
||||||
|
fmt.Println("Starting Codex device authentication...")
|
||||||
|
fmt.Printf("Codex device URL: %s\n", codexDeviceVerificationURL)
|
||||||
|
fmt.Printf("Codex device code: %s\n", deviceCode)
|
||||||
|
|
||||||
|
if !opts.NoBrowser {
|
||||||
|
if !browser.IsAvailable() {
|
||||||
|
log.Warn("No browser available; please open the device URL manually")
|
||||||
|
} else if errOpen := browser.OpenURL(codexDeviceVerificationURL); errOpen != nil {
|
||||||
|
log.Warnf("Failed to open browser automatically: %v", errOpen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResp, err := pollCodexDeviceToken(ctx, httpClient, deviceAuthID, deviceCode, pollInterval)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authCode := strings.TrimSpace(tokenResp.AuthorizationCode)
|
||||||
|
codeVerifier := strings.TrimSpace(tokenResp.CodeVerifier)
|
||||||
|
codeChallenge := strings.TrimSpace(tokenResp.CodeChallenge)
|
||||||
|
if authCode == "" || codeVerifier == "" || codeChallenge == "" {
|
||||||
|
return nil, fmt.Errorf("codex device flow token response missing required fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
authSvc := codex.NewCodexAuth(cfg)
|
||||||
|
authBundle, err := authSvc.ExchangeCodeForTokensWithRedirect(
|
||||||
|
ctx,
|
||||||
|
authCode,
|
||||||
|
codexDeviceTokenExchangeRedirectURI,
|
||||||
|
&codex.PKCECodes{
|
||||||
|
CodeVerifier: codeVerifier,
|
||||||
|
CodeChallenge: codeChallenge,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, codex.NewAuthenticationError(codex.ErrCodeExchangeFailed, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.buildAuthRecord(authSvc, authBundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestCodexDeviceUserCode(ctx context.Context, client *http.Client) (*codexDeviceUserCodeResponse, error) {
|
||||||
|
body, err := json.Marshal(codexDeviceUserCodeRequest{ClientID: codex.ClientID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode codex device request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, codexDeviceUserCodeURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create codex device request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to request codex device code: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read codex device code response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !codexDeviceIsSuccessStatus(resp.StatusCode) {
|
||||||
|
trimmed := strings.TrimSpace(string(respBody))
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return nil, fmt.Errorf("codex device endpoint is unavailable (status %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if trimmed == "" {
|
||||||
|
trimmed = "empty response body"
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("codex device code request failed with status %d: %s", resp.StatusCode, trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed codexDeviceUserCodeResponse
|
||||||
|
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode codex device code response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &parsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pollCodexDeviceToken(ctx context.Context, client *http.Client, deviceAuthID, userCode string, interval time.Duration) (*codexDeviceTokenResponse, error) {
|
||||||
|
deadline := time.Now().Add(codexDeviceTimeout)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return nil, fmt.Errorf("codex device authentication timed out after 15 minutes")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(codexDeviceTokenRequest{
|
||||||
|
DeviceAuthID: deviceAuthID,
|
||||||
|
UserCode: userCode,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode codex device poll request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, codexDeviceTokenURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create codex device poll request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to poll codex device token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, readErr := io.ReadAll(resp.Body)
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read codex device poll response: %w", readErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case codexDeviceIsSuccessStatus(resp.StatusCode):
|
||||||
|
var parsed codexDeviceTokenResponse
|
||||||
|
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode codex device token response: %w", err)
|
||||||
|
}
|
||||||
|
return &parsed, nil
|
||||||
|
case resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound:
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-time.After(interval):
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
trimmed := strings.TrimSpace(string(respBody))
|
||||||
|
if trimmed == "" {
|
||||||
|
trimmed = "empty response body"
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("codex device token polling failed with status %d: %s", resp.StatusCode, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCodexDevicePollInterval(raw json.RawMessage) time.Duration {
|
||||||
|
defaultInterval := time.Duration(codexDeviceDefaultPollIntervalSeconds) * time.Second
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return defaultInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
var asString string
|
||||||
|
if err := json.Unmarshal(raw, &asString); err == nil {
|
||||||
|
if seconds, convErr := strconv.Atoi(strings.TrimSpace(asString)); convErr == nil && seconds > 0 {
|
||||||
|
return time.Duration(seconds) * time.Second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var asInt int
|
||||||
|
if err := json.Unmarshal(raw, &asInt); err == nil && asInt > 0 {
|
||||||
|
return time.Duration(asInt) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
func codexDeviceIsSuccessStatus(code int) bool {
|
||||||
|
return code >= 200 && code < 300
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CodexAuthenticator) buildAuthRecord(authSvc *codex.CodexAuth, authBundle *codex.CodexAuthBundle) (*coreauth.Auth, error) {
|
||||||
|
tokenStorage := authSvc.CreateTokenStorage(authBundle)
|
||||||
|
|
||||||
|
if tokenStorage == nil || tokenStorage.Email == "" {
|
||||||
|
return nil, fmt.Errorf("codex token storage missing account information")
|
||||||
|
}
|
||||||
|
|
||||||
|
planType := ""
|
||||||
|
hashAccountID := ""
|
||||||
|
if tokenStorage.IDToken != "" {
|
||||||
|
if claims, errParse := codex.ParseJWTToken(tokenStorage.IDToken); errParse == nil && claims != nil {
|
||||||
|
planType = strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType)
|
||||||
|
accountID := strings.TrimSpace(claims.CodexAuthInfo.ChatgptAccountID)
|
||||||
|
if accountID != "" {
|
||||||
|
digest := sha256.Sum256([]byte(accountID))
|
||||||
|
hashAccountID = hex.EncodeToString(digest[:])[:8]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := codex.CredentialFileName(tokenStorage.Email, planType, hashAccountID, true)
|
||||||
|
metadata := map[string]any{
|
||||||
|
"email": tokenStorage.Email,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Codex authentication successful")
|
||||||
|
if authBundle.APIKey != "" {
|
||||||
|
fmt.Println("Codex API key obtained and stored")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
|
Provider: a.Provider(),
|
||||||
|
FileName: fileName,
|
||||||
|
Storage: tokenStorage,
|
||||||
|
Metadata: metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user