Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 40 additions & 22 deletions internal/api/handlers/management/auth_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,31 +425,49 @@ func extractCodexIDTokenClaims(auth *coreauth.Auth) gin.H {
if !strings.EqualFold(strings.TrimSpace(auth.Provider), "codex") {
return nil
}
idTokenRaw, ok := auth.Metadata["id_token"].(string)
if !ok {
return nil
}
idToken := strings.TrimSpace(idTokenRaw)
if idToken == "" {
return nil
}
claims, err := codex.ParseJWTToken(idToken)
if err != nil || claims == nil {
return nil
}

result := gin.H{}
if v := strings.TrimSpace(claims.CodexAuthInfo.ChatgptAccountID); v != "" {
result["chatgpt_account_id"] = v
}
if v := strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType); v != "" {
result["plan_type"] = v
}
if v := claims.CodexAuthInfo.ChatgptSubscriptionActiveStart; v != nil {
result["chatgpt_subscription_active_start"] = v

// Check Metadata/Attributes first before parsing id_token.
// account_id is stored under "account_id" in the JSON file (CodexTokenStorage.AccountID).
if v, ok := auth.Metadata["account_id"].(string); ok && strings.TrimSpace(v) != "" {
result["chatgpt_account_id"] = strings.TrimSpace(v)
}
if v := claims.CodexAuthInfo.ChatgptSubscriptionActiveUntil; v != nil {
result["chatgpt_subscription_active_until"] = v
// plan_type is synthesized into Attributes by the file watcher, not stored in Metadata.
if auth.Attributes != nil {
if v := strings.TrimSpace(auth.Attributes["plan_type"]); v != "" {
result["plan_type"] = v
}
}

// Fall back to parsing id_token for any fields still missing
needsIDToken := result["chatgpt_account_id"] == nil || result["plan_type"] == nil
if needsIDToken {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse ID token before populating subscription fields

The new needsIDToken gate only parses id_token when chatgpt_account_id or plan_type is missing, but chatgpt_subscription_active_start and chatgpt_subscription_active_until are also populated only inside that block. In cases where metadata already has account ID and attributes already has plan type, subscription dates are silently dropped even when id_token is present, which regresses /auth-files response completeness.

Useful? React with 👍 / 👎.

idTokenRaw, ok := auth.Metadata["id_token"].(string)
if ok {
idToken := strings.TrimSpace(idTokenRaw)
if idToken != "" {
claims, err := codex.ParseJWTToken(idToken)
if err == nil && claims != nil {
if result["chatgpt_account_id"] == nil {
if v := strings.TrimSpace(claims.CodexAuthInfo.ChatgptAccountID); v != "" {
result["chatgpt_account_id"] = v
}
}
if result["plan_type"] == nil {
if v := strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType); v != "" {
result["plan_type"] = v
}
}
if v := claims.CodexAuthInfo.ChatgptSubscriptionActiveStart; v != nil {
result["chatgpt_subscription_active_start"] = v
}
if v := claims.CodexAuthInfo.ChatgptSubscriptionActiveUntil; v != nil {
result["chatgpt_subscription_active_until"] = v
}
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

当前逻辑存在一个缺陷:当 chatgpt_account_idplan_type 都已经从 MetadataAttributes 中获取时,needsIDToken 会为 false,导致代码块被跳过,从而无法解析 id_token 中包含的订阅日期字段(chatgpt_subscription_active_startchatgpt_subscription_active_until)。根据 PR 描述,这些日期字段应该总是从 id_token 中解析。

为了修复此问题并简化代码,建议重构逻辑,移除 needsIDToken 条件,始终尝试解析 id_token(如果存在),并将其用作补充字段和获取订阅日期的来源。

	if idTokenRaw, ok := auth.Metadata["id_token"].(string); ok {
		idToken := strings.TrimSpace(idTokenRaw)
		if idToken != "" {
			claims, err := codex.ParseJWTToken(idToken)
			if err == nil && claims != nil {
				if result["chatgpt_account_id"] == nil {
					if v := strings.TrimSpace(claims.CodexAuthInfo.ChatgptAccountID); v != "" {
						result["chatgpt_account_id"] = v
					}
				}
				if result["plan_type"] == nil {
					if v := strings.TrimSpace(claims.CodexAuthInfo.ChatgptPlanType); v != "" {
						result["plan_type"] = v
					}
				}
				if v := claims.CodexAuthInfo.ChatgptSubscriptionActiveStart; v != nil {
					result["chatgpt_subscription_active_start"] = v
				}
				if v := claims.CodexAuthInfo.ChatgptSubscriptionActiveUntil; v != nil {
					result["chatgpt_subscription_active_until"] = v
				}
			}
		}
	}


if len(result) == 0 {
Expand Down
9 changes: 9 additions & 0 deletions internal/auth/codex/jwt_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,12 @@ func (c *JWTClaims) GetUserEmail() string {
func (c *JWTClaims) GetAccountID() string {
return c.CodexAuthInfo.ChatgptAccountID
}

// GetClientID returns the first audience value from the JWT claims, which represents
// the OAuth client_id used during token issuance.
func (c *JWTClaims) GetClientID() string {
if len(c.Aud) > 0 {
return c.Aud[0]
}
return ""
}
17 changes: 9 additions & 8 deletions internal/auth/codex/openai_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,17 @@ func (o *CodexAuth) ExchangeCodeForTokensWithRedirect(ctx context.Context, code,
}

// RefreshTokens refreshes an access token using a refresh token.
// This method is called when an access token has expired. It makes a request to the
// token endpoint to obtain a new set of tokens.
func (o *CodexAuth) RefreshTokens(ctx context.Context, refreshToken string) (*CodexTokenData, error) {
// clientID overrides the default hardcoded ClientID when non-empty.
func (o *CodexAuth) RefreshTokens(ctx context.Context, refreshToken, clientID string) (*CodexTokenData, error) {
if refreshToken == "" {
return nil, fmt.Errorf("refresh token is required")
}
if clientID == "" {
clientID = ClientID
}

data := url.Values{
"client_id": {ClientID},
"client_id": {clientID},
"grant_type": {"refresh_token"},
"refresh_token": {refreshToken},
"scope": {"openid profile email"},
Expand Down Expand Up @@ -257,9 +259,8 @@ func (o *CodexAuth) CreateTokenStorage(bundle *CodexAuthBundle) *CodexTokenStora
}

// RefreshTokensWithRetry refreshes tokens with a built-in retry mechanism.
// It attempts to refresh the tokens up to a specified maximum number of retries,
// with an exponential backoff strategy to handle transient network errors.
func (o *CodexAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken string, maxRetries int) (*CodexTokenData, error) {
// clientID overrides the default hardcoded ClientID when non-empty.
func (o *CodexAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken, clientID string, maxRetries int) (*CodexTokenData, error) {
var lastErr error

for attempt := 0; attempt < maxRetries; attempt++ {
Expand All @@ -272,7 +273,7 @@ func (o *CodexAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken str
}
}

tokenData, err := o.RefreshTokens(ctx, refreshToken)
tokenData, err := o.RefreshTokens(ctx, refreshToken, clientID)
if err == nil {
return tokenData, nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/auth/codex/openai_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestRefreshTokensWithRetry_NonRetryableOnlyAttemptsOnce(t *testing.T) {
},
}

_, err := auth.RefreshTokensWithRetry(context.Background(), "dummy_refresh_token", 3)
_, err := auth.RefreshTokensWithRetry(context.Background(), "dummy_refresh_token", "app_EMoamEEZ73f0CkXaXp7hrann", 3)
if err == nil {
t.Fatalf("expected error for non-retryable refresh failure")
}
Expand Down
16 changes: 14 additions & 2 deletions internal/runtime/executor/codex_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,17 +562,29 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*
if auth == nil {
return nil, statusErr{code: 500, msg: "codex executor: auth is nil"}
}
var refreshToken string
var refreshToken, clientID string
if auth.Metadata != nil {
if v, ok := auth.Metadata["refresh_token"].(string); ok && v != "" {
refreshToken = v
}
// Prefer explicit client_id stored in metadata
if v, ok := auth.Metadata["client_id"].(string); ok && v != "" {
clientID = v
}
}
if refreshToken == "" {
return auth, nil
}
// Fall back to parsing client_id from id_token.aud[0]
if clientID == "" {
if idTokenRaw, ok := auth.Metadata["id_token"].(string); ok && idTokenRaw != "" {
if claims, err := codexauth.ParseJWTToken(idTokenRaw); err == nil && claims != nil {
clientID = claims.GetClientID()
}
}
}
svc := codexauth.NewCodexAuth(e.cfg)
td, err := svc.RefreshTokensWithRetry(ctx, refreshToken, 3)
td, err := svc.RefreshTokensWithRetry(ctx, refreshToken, clientID, 3)
if err != nil {
return nil, err
}
Expand Down
194 changes: 194 additions & 0 deletions internal/runtime/executor/codex_executor_account_id_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package executor

import (
"bufio"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"testing"

"github.com/google/uuid"
tls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
)

// utlsTransport is a minimal Chrome-fingerprint TLS transport for test use.
// Supports HTTP CONNECT proxy tunneling.
type utlsTransport struct {
proxyURL string
}

func newUtlsTransport(proxyURL string) *utlsTransport {
return &utlsTransport{proxyURL: proxyURL}
}

func (t *utlsTransport) dial(addr string) (net.Conn, error) {
if t.proxyURL == "" {
return net.Dial("tcp", addr)
}
u, err := url.Parse(t.proxyURL)
if err != nil {
return nil, fmt.Errorf("parse proxy url: %w", err)
}
conn, err := net.Dial("tcp", u.Host)
if err != nil {
return nil, fmt.Errorf("connect to proxy: %w", err)
}
// HTTP CONNECT tunnel
req, _ := http.NewRequest(http.MethodConnect, "http://"+addr, nil)
req.Host = addr
if err = req.Write(conn); err != nil {
conn.Close()
return nil, fmt.Errorf("write CONNECT: %w", err)
}
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
if err != nil {
conn.Close()
return nil, fmt.Errorf("read CONNECT response: %w", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
conn.Close()
return nil, fmt.Errorf("proxy CONNECT failed: %s", resp.Status)
}
return conn, nil
}

func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
host := req.URL.Hostname()
addr := host + ":443"

conn, err := t.dial(addr)
if err != nil {
return nil, fmt.Errorf("dial: %w", err)
}

tlsConn := tls.UClient(conn, &tls.Config{ServerName: host}, tls.HelloChrome_Auto)
if err = tlsConn.Handshake(); err != nil {
conn.Close()
return nil, fmt.Errorf("tls handshake: %w", err)
}

tr := &http2.Transport{}
h2Conn, err := tr.NewClientConn(tlsConn)
if err != nil {
tlsConn.Close()
return nil, fmt.Errorf("h2 conn: %w", err)
}

return h2Conn.RoundTrip(req)
}

// planTypePriority returns a numeric priority for a plan_type string.
// Higher value means higher priority: team > plus > free > others.
func planTypePriority(planType string) int {
switch strings.ToLower(planType) {
case "team":
return 3
case "plus":
return 2
case "free":
return 1
default:
return 0
}
}

// pickBestAccountID selects the best account_id from the $.accounts map returned by
// the accounts/check API. Priority: team > plus > free > any other.
// Returns empty string if no accounts are found.
func pickBestAccountID(accounts map[string]any) string {
bestID := ""
bestPriority := -1
for accountID, v := range accounts {
info, ok := v.(map[string]any)
if !ok {
continue
}
account, ok := info["account"].(map[string]any)
if !ok {
continue
}
planType, _ := account["plan_type"].(string)
p := planTypePriority(planType)
if p > bestPriority {
bestPriority = p
bestID = accountID
}
}
return bestID
}

// TestCodexAccountCheck tests GET https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27
// using a real access_token. Set CODEX_ACCESS_TOKEN (and optionally CODEX_PROXY_URL) to run.
//
// Example:
//
// CODEX_ACCESS_TOKEN=eyJ... go test ./internal/runtime/executor/... -run TestCodexAccountCheck -v
// CODEX_ACCESS_TOKEN=eyJ... CODEX_PROXY_URL=http://127.0.0.1:7890 go test ./internal/runtime/executor/... -run TestCodexAccountCheck -v
func TestCodexAccountCheck(t *testing.T) {
accessToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MSJdLCJjbGllbnRfaWQiOiJhcHBfWDh6WTZ2VzJwUTl0UjNkRTduSzFqTDVnSCIsImV4cCI6MTc3NDQ1NTUyNSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7ImNoYXRncHRfYWNjb3VudF9pZCI6Ijg0YzNjZGM2LWFiZWQtNDlhNy1iY2RlLWZmMjQwMjE3NmZkYiIsImNoYXRncHRfYWNjb3VudF91c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUNfXzg0YzNjZGM2LWFiZWQtNDlhNy1iY2RlLWZmMjQwMjE3NmZkYiIsImNoYXRncHRfY29tcHV0ZV9yZXNpZGVuY3kiOiJub19jb25zdHJhaW50IiwiY2hhdGdwdF9wbGFuX3R5cGUiOiJmcmVlIiwiY2hhdGdwdF91c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUMiLCJ1c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUMifSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9wcm9maWxlIjp7ImVtYWlsIjoiZ3VzLmxhcnNvbjJAZDQuemh1ZmFkYS5kZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwiaWF0IjoxNzczNTkxNTI1LCJpc3MiOiJodHRwczovL2F1dGgub3BlbmFpLmNvbSIsImp0aSI6IjYzZDgwY2M3LWI1NzEtNDk3Yi05ZmI2LTZiZTE2MDE1ZjI4MiIsIm5iZiI6MTc3MzU5MTUyNSwicHdkX2F1dGhfdGltZSI6MTc3MzU5MTUyNDA3NCwic2NwIjpbIm9wZW5pZCIsImVtYWlsIiwicHJvZmlsZSIsIm9mZmxpbmVfYWNjZXNzIiwibW9kZWwucmVxdWVzdCIsIm1vZGVsLnJlYWQiLCJvcmdhbml6YXRpb24ucmVhZCIsIm9yZ2FuaXphdGlvbi53cml0ZSJdLCJzZXNzaW9uX2lkIjoiYXV0aHNlc3NfWTFwQWh2VWRXQVVNb1pPVzN0OEllNjFKIiwic2wiOnRydWUsInN1YiI6ImF1dGgwfHhJcnpFSTlvV0xBUUtMMW4wR2tKenBETiJ9.f8tBrHuYqZtVpSY3cxf0NOrEgGHVZlhSQhz4_aMngNIq8_O1oY6ajyWoJpdqtf_m-luzRswZMgA-fKGiEbKu-LqqFiCHnNOFkK5ymdAoXFLsHWEX-BFS5wqTKJ6_nphrqLgVMAaA1mwuWQZ3PD2mCMJ_eErFhFPGlOCBR1TyDSMhJhvDMHB81sqJbxJBpkQNV3J1GDcvvUaNiQebAs4LNOhNaQfYTxJQqJZiGCnwjeHWql_aSSKv4y1vEXSLwH-GEqfjlpHYZqhYTHpdr_PzzOOIWq_X9ScedMOy699UYwyQa7IKcwCw6ZqaVbR_WjAdHunWi8yOl5C7JFuUA2xhrfMOQUgg86vv5oBw_OYTHzX51Dimh_SHhLaUCNC0-SPRZ-IiYz91MveiR_QCSHvU_ZXJO-FY8Xqa6NEdLZ8AbrOb81dxnV8DOABxolHVMtuxINPQuzHAEKxAyNGQkwgo7_O4TAVUycpv4b3LMIoSdCHy7F7q9Dh1UR8jTugM0Zqor6bD0XEwdFL5KxONZk-alHAo93IrDS9D8L0bjp0cMl7A1ZyjYjOMpw8Liq_b6V6uyPPJsw0DY1q-LtrQXN05W1La5bOX-J0yjtEnRrKtm5mGZ8cT2RnKYJJFFSckbMY2EvKuSEMKh0T5YoqPx-LZsXRViscbGfLBh8e2gzvXlq8"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove committed bearer token from test source

This test embeds a full bearer token directly in source control, which is a credential exposure risk and also leaks account-identifying claim data if the token is still valid (or present in history). Tests that require real credentials should read them from environment variables at runtime instead of committing secrets.

Useful? React with 👍 / 👎.

proxyURL := "http://127.0.0.1:7890"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

此测试用例中硬编码了访问令牌(accessToken)和代理URL(proxyURL)。将敏感信息(如令牌)直接提交到代码库中会带来严重的安全风险,即使是测试令牌。此外,硬编码代理URL会降低测试的灵活性。

建议修改此测试,从环境变量中读取这些值,并在令牌不存在时跳过测试。这符合测试注释中描述的预期行为。

请确保在文件顶部添加 import "os"

Suggested change
accessToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MSJdLCJjbGllbnRfaWQiOiJhcHBfWDh6WTZ2VzJwUTl0UjNkRTduSzFqTDVnSCIsImV4cCI6MTc3NDQ1NTUyNSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7ImNoYXRncHRfYWNjb3VudF9pZCI6Ijg0YzNjZGM2LWFiZWQtNDlhNy1iY2RlLWZmMjQwMjE3NmZkYiIsImNoYXRncHRfYWNjb3VudF91c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUNfXzg0YzNjZGM2LWFiZWQtNDlhNy1iY2RlLWZmMjQwMjE3NmZkYiIsImNoYXRncHRfY29tcHV0ZV9yZXNpZGVuY3kiOiJub19jb25zdHJhaW50IiwiY2hhdGdwdF9wbGFuX3R5cGUiOiJmcmVlIiwiY2hhdGdwdF91c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUMiLCJ1c2VyX2lkIjoidXNlci00MXlCY3M0STZscEJBVEh5R2lTNG9wTUMifSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9wcm9maWxlIjp7ImVtYWlsIjoiZ3VzLmxhcnNvbjJAZDQuemh1ZmFkYS5kZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwiaWF0IjoxNzczNTkxNTI1LCJpc3MiOiJodHRwczovL2F1dGgub3BlbmFpLmNvbSIsImp0aSI6IjYzZDgwY2M3LWI1NzEtNDk3Yi05ZmI2LTZiZTE2MDE1ZjI4MiIsIm5iZiI6MTc3MzU5MTUyNSwicHdkX2F1dGhfdGltZSI6MTc3MzU5MTUyNDA3NCwic2NwIjpbIm9wZW5pZCIsImVtYWlsIiwicHJvZmlsZSIsIm9mZmxpbmVfYWNjZXNzIiwibW9kZWwucmVxdWVzdCIsIm1vZGVsLnJlYWQiLCJvcmdhbml6YXRpb24ucmVhZCIsIm9yZ2FuaXphdGlvbi53cml0ZSJdLCJzZXNzaW9uX2lkIjoiYXV0aHNlc3NfWTFwQWh2VWRXQVVNb1pPVzN0OEllNjFKIiwic2wiOnRydWUsInN1YiI6ImF1dGgwfHhJcnpFSTlvV0xBUUtMMW4wR2tKenBETiJ9.f8tBrHuYqZtVpSY3cxf0NOrEgGHVZlhSQhz4_aMngNIq8_O1oY6ajyWoJpdqtf_m-luzRswZMgA-fKGiEbKu-LqqFiCHnNOFkK5ymdAoXFLsHWEX-BFS5wqTKJ6_nphrqLgVMAaA1mwuWQZ3PD2mCMJ_eErFhFPGlOCBR1TyDSMhJhvDMHB81sqJbxJBpkQNV3J1GDcvvUaNiQebAs4LNOhNaQfYTxJQqJZiGCnwjeHWql_aSSKv4y1vEXSLwH-GEqfjlpHYZqhYTHpdr_PzzOOIWq_X9ScedMOy699UYwyQa7IKcwCw6ZqaVbR_WjAdHunWi8yOl5C7JFuUA2xhrfMOQUgg86vv5oBw_OYTHzX51Dimh_SHhLaUCNC0-SPRZ-IiYz91MveiR_QCSHvU_ZXJO-FY8Xqa6NEdLZ8AbrOb81dxnV8DOABxolHVMtuxINPQuzHAEKxAyNGQkwgo7_O4TAVUycpv4b3LMIoSdCHy7F7q9Dh1UR8jTugM0Zqor6bD0XEwdFL5KxONZk-alHAo93IrDS9D8L0bjp0cMl7A1ZyjYjOMpw8Liq_b6V6uyPPJsw0DY1q-LtrQXN05W1La5bOX-J0yjtEnRrKtm5mGZ8cT2RnKYJJFFSckbMY2EvKuSEMKh0T5YoqPx-LZsXRViscbGfLBh8e2gzvXlq8"
proxyURL := "http://127.0.0.1:7890"
accessToken := os.Getenv("CODEX_ACCESS_TOKEN")
if accessToken == "" {
t.Skip("skipping test: environment variable CODEX_ACCESS_TOKEN is not set")
}
proxyURL := os.Getenv("CODEX_PROXY_URL")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate live account-check test behind explicit opt-in

TestCodexAccountCheck runs as a normal unit test but performs a real network call through a fixed local proxy (127.0.0.1:7890) and immediately Fatalfs on connection errors, so go test fails in any environment without that proxy (including typical CI). The comment says env vars should control execution, but the implementation does not check them or skip, making the test suite non-hermetic and brittle.

Useful? React with 👍 / 👎.

deviceID := uuid.NewString()
targetURL := "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27?timezone_offset_min=-480"

req, err := http.NewRequest(http.MethodGet, targetURL, nil)
if err != nil {
t.Fatalf("build request: %v", err)
}

req.Header.Set("accept", "*/*")
req.Header.Set("accept-language", "zh-HK,zh;q=0.9,en-US;q=0.8,en;q=0.7")
req.Header.Set("authorization", "Bearer "+strings.TrimSpace(accessToken))
req.Header.Set("oai-device-id", deviceID)
req.Header.Set("oai-language", "zh-HK")
req.Header.Set("referer", "https://chatgpt.com/")
req.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36")
req.Header.Set("sec-ch-ua", `"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"macOS"`)
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-site", "same-origin")
req.Header.Set("priority", "u=1, i")

client := &http.Client{
Transport: newUtlsTransport(proxyURL),
}

resp, err := client.Do(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read response: %v", err)
}

t.Logf("status: %d", resp.StatusCode)
t.Logf("device_id: %s", deviceID)
t.Logf("response: %s", string(body))

if resp.StatusCode != http.StatusOK {
t.Errorf("expected 200, got %d", resp.StatusCode)
return
}

// Parse response and pick the best account_id
var parsed map[string]any
if err = json.Unmarshal(body, &parsed); err != nil {
t.Fatalf("unmarshal response: %v", err)
}
if accounts, ok := parsed["accounts"].(map[string]any); ok {
bestID := pickBestAccountID(accounts)
t.Logf("best_account_id (team>plus>free): %s", bestID)
} else {
t.Logf("no $.accounts map found in response")
}
}
Loading