package helps import ( "context" "encoding/json" "errors" "fmt" "net/http" "strings" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/home" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) type homeStatusErr struct { code int msg string } func (e homeStatusErr) Error() string { if e.msg != "" { return e.msg } return fmt.Sprintf("status %d", e.code) } func (e homeStatusErr) StatusCode() int { return e.code } type homeErrorEnvelope struct { Error *homeErrorDetail `json:"error"` } type homeRefreshAuthEnvelope struct { Auth cliproxyauth.Auth `json:"auth"` AuthIndex string `json:"auth_index"` } type homeErrorDetail struct { Type string `json:"type"` Message string `json:"message"` Code string `json:"code,omitempty"` } type homeRefreshClient interface { HeartbeatOK() bool GetRefreshAuth(ctx context.Context, authIndex string, accessTokenSHA256 string) ([]byte, error) } var currentHomeRefreshClient = func() homeRefreshClient { return home.Current() } // RefreshAuthViaHome replaces local refresh logic when home control plane integration is enabled. // It returns (updatedAuth, true, nil) when home refresh succeeds; (nil, true, err) when home is // enabled but refresh fails; and (nil, false, nil) when home is disabled. func RefreshAuthViaHome(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, bool, error) { if cfg == nil || !cfg.Home.Enabled { return nil, false, nil } if ctx == nil { ctx = context.Background() } if auth == nil { return nil, true, homeStatusErr{code: http.StatusInternalServerError, msg: "home refresh: auth is nil"} } client := currentHomeRefreshClient() if client == nil || !client.HeartbeatOK() { return nil, true, homeStatusErr{code: http.StatusServiceUnavailable, msg: "home control center unavailable"} } authIndex := strings.TrimSpace(auth.Index) if authIndex == "" { authIndex = strings.TrimSpace(auth.EnsureIndex()) } if authIndex == "" { return nil, true, homeStatusErr{code: http.StatusBadGateway, msg: "home refresh: auth_index is empty"} } raw, err := client.GetRefreshAuth(ctx, authIndex, authAccessTokenSHA256(auth)) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return nil, true, err } return nil, true, homeStatusErr{code: http.StatusServiceUnavailable, msg: "home refresh temporarily unavailable"} } var env homeErrorEnvelope if errUnmarshal := json.Unmarshal(raw, &env); errUnmarshal == nil && env.Error != nil { code := strings.TrimSpace(env.Error.Type) if code == "" { code = strings.TrimSpace(env.Error.Code) } statusCode := statusFromHomeErrorCode(code) message := "credential refresh temporarily unavailable" switch statusCode { case http.StatusUnauthorized: message = "credential unauthorized" case http.StatusNotFound: message = "credential refresh target not found" } return nil, true, homeStatusErr{code: statusCode, msg: message} } updated, returnedIndex, errParse := parseHomeRefreshAuth(raw) if errParse != nil { return nil, true, homeStatusErr{code: http.StatusBadGateway, msg: "home returned invalid auth payload"} } if updated.Disabled || updated.Status == cliproxyauth.StatusDisabled { return nil, true, homeStatusErr{code: http.StatusUnauthorized, msg: "credential unauthorized"} } if returnedIndex != "" { authIndex = returnedIndex } updated.Index = authIndex updated.EnsureIndex() return updated, true, nil } func authAccessTokenSHA256(auth *cliproxyauth.Auth) string { return cliproxyauth.AccessTokenSHA256(auth) } func parseHomeRefreshAuth(raw []byte) (*cliproxyauth.Auth, string, error) { var rawObject map[string]json.RawMessage if errUnmarshal := json.Unmarshal(raw, &rawObject); errUnmarshal != nil { return nil, "", errUnmarshal } if _, ok := rawObject["auth"]; ok { var envelope homeRefreshAuthEnvelope if errUnmarshal := json.Unmarshal(raw, &envelope); errUnmarshal != nil { return nil, "", errUnmarshal } return &envelope.Auth, strings.TrimSpace(envelope.AuthIndex), nil } var updated cliproxyauth.Auth if errUnmarshal := json.Unmarshal(raw, &updated); errUnmarshal != nil { return nil, "", errUnmarshal } return &updated, "", nil } func statusFromHomeErrorCode(code string) int { switch strings.ToLower(strings.TrimSpace(code)) { case "authentication_error", "unauthorized", "invalid_grant", "refresh_token_expired", "refresh_token_revoked", "refresh_token_reused": return http.StatusUnauthorized case "model_not_found": return http.StatusNotFound case "auth_not_found", "auth_unavailable", "refresh_temporarily_unavailable", "refresh_unsupported", "home_unavailable": return http.StatusServiceUnavailable default: return http.StatusServiceUnavailable } }