mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-18 03:57:06 +08:00
refactor: remove CursorAccountCard component and related localization entries
- Deleted the CursorAccountCard.vue component, which handled user account login and status. - Removed associated localization entries from catalog.json and various language files. - Updated Home.vue to eliminate references to the removed component. - Refactored clientApi.js to remove unused account-related API functions.
This commit is contained in:
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultBackendListenAddr = "127.0.0.1:18090"
|
||||
DefaultBackendListenAddr = "127.0.0.1:8000"
|
||||
DefaultProxyListenAddr = "127.0.0.1:18080"
|
||||
DefaultFrontendBaseURL = "http://127.0.0.1"
|
||||
DefaultProviderStreamIdleTimeoutSeconds = 240
|
||||
|
||||
@@ -14,12 +14,13 @@ import (
|
||||
type CompatRouteConfig struct {
|
||||
Name string
|
||||
StatusCode int
|
||||
JSONBody map[string]any
|
||||
MockProtoType string
|
||||
MockBuilder func(*RequestContext) (map[string]any, error)
|
||||
ConsoleLog bool
|
||||
}
|
||||
|
||||
const DefaultCursorUpstreamBaseURL = "https://api2.cursor.sh:443"
|
||||
|
||||
func ForwardAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
@@ -30,31 +31,27 @@ func ForwardAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc
|
||||
}
|
||||
}
|
||||
|
||||
// AuthenticatedForwardAction forwards a Cursor control-plane request with the
|
||||
// independent desktop account after the local-mode identity rewrite has run.
|
||||
func AuthenticatedForwardAction(deps Dependencies, cfg CompatRouteConfig, authorizationProvider AuthorizationProvider) server.HandlerFunc {
|
||||
// FallbackForwardAction preserves an MITM request's original upstream URL. A
|
||||
// native request has no original host metadata, so it is resolved against the
|
||||
// configured default upstream while retaining its path and query string.
|
||||
func FallbackForwardAction(deps Dependencies, cfg CompatRouteConfig, defaultBaseURL string) server.HandlerFunc {
|
||||
forward := ForwardAction(deps, cfg)
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, _, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
if ctx == nil || ctx.Request == nil || ctx.Request.URL == nil {
|
||||
return fmt.Errorf("fallback upstream request context is invalid")
|
||||
}
|
||||
if reqCtx == nil || reqCtx.Request == nil {
|
||||
return fmt.Errorf("Cursor 控制面请求上下文无效")
|
||||
if ctx.UpstreamURL == nil {
|
||||
baseURL, err := ParseAndValidateRawURL(defaultBaseURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse fallback upstream URL: %w", err)
|
||||
}
|
||||
targetURL := *ctx.Request.URL
|
||||
targetURL.Scheme = baseURL.Scheme
|
||||
targetURL.Host = baseURL.Host
|
||||
targetURL.User = baseURL.User
|
||||
ctx.UpstreamURL = &targetURL
|
||||
}
|
||||
if authorizationProvider == nil {
|
||||
return fmt.Errorf("Cursor 账号服务未初始化")
|
||||
}
|
||||
authorization, err := authorizationProvider.Authorization(reqCtx.Request.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = ForwardToUpstream(reqCtx, ForwardOptions{
|
||||
PatchHeaders: func(headers http.Header) {
|
||||
headers.Set("Authorization", authorization)
|
||||
headers.Set("x-cursor-checksum", BuildCursorChecksum(authorization))
|
||||
},
|
||||
})
|
||||
return err
|
||||
return forward(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,63 +65,13 @@ func FixedStatusAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerF
|
||||
}
|
||||
}
|
||||
|
||||
func MockJSONAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
func MockDevSessionTokenAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockJSON(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockOAuthAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockOAuth(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockAuthFullStripeProfileAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockAuthFullStripeProfile(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockAuthStripeProfileAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockAuthStripeProfile(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockAuthPollAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockAuthPoll(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockAuthEmailAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockAuthEmail(reqCtx, route)
|
||||
return handleMockDevSessionToken(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +116,6 @@ func newCompatRouteObjects(ctx *server.Context, deps Dependencies, cfg CompatRou
|
||||
Name: cfg.Name,
|
||||
Pattern: ctx.Request.URL.Path,
|
||||
StatusCode: cfg.StatusCode,
|
||||
JSONBody: cfg.JSONBody,
|
||||
MockProtoType: cfg.MockProtoType,
|
||||
MockPayloadBuilder: cfg.MockBuilder,
|
||||
ConsoleLog: cfg.ConsoleLog,
|
||||
@@ -221,10 +167,6 @@ func DashboardTeamsMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardTeamsPayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardManagedSkillsMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardManagedSkillsPayload(reqCtx)
|
||||
}
|
||||
|
||||
// EmptyMockBuilder возвращает пустой proto-ответ для ручек, где клиенту
|
||||
// достаточно успешного "пусто": нет team-настроек, нет репозиториев,
|
||||
// нет маркетплейсов/плагинов/команд, телеметрия принята без обработки.
|
||||
@@ -237,14 +179,6 @@ func SubmitLogsMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return map[string]any{"success": true}, nil
|
||||
}
|
||||
|
||||
func DashboardGetMeMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardGetMePayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardUserPrivacyModeMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardUserPrivacyModePayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardPlanInfoMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardPlanInfoPayload(reqCtx)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package upstream
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
localDevDefaultPlan = "ultra"
|
||||
localDevTokenLifetime = 10 * 365 * 24 * time.Hour
|
||||
localDevSubscriptionActive = "active"
|
||||
)
|
||||
|
||||
var localDevPlans = map[string]struct{}{
|
||||
"free": {},
|
||||
"pro": {},
|
||||
"pro_plus": {},
|
||||
"ultra": {},
|
||||
"enterprise": {},
|
||||
}
|
||||
|
||||
type localDevSessionClaims struct {
|
||||
Subject string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
Plan string `json:"cursor_local_plan"`
|
||||
Trial bool `json:"cursor_local_trial"`
|
||||
TokenType string `json:"type"`
|
||||
Issuer string `json:"iss"`
|
||||
Scope string `json:"scope"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
ExpiresAt int64 `json:"exp"`
|
||||
}
|
||||
|
||||
func handleMockDevSessionToken(reqCtx *RequestContext, route *Route) error {
|
||||
_ = route
|
||||
if reqCtx == nil || reqCtx.Request == nil || reqCtx.ResponseWriter == nil {
|
||||
return fmt.Errorf("dev session request context is invalid")
|
||||
}
|
||||
|
||||
plan, trial, email, err := parseLocalDevSessionQuery(reqCtx.Request)
|
||||
if err != nil {
|
||||
writeJSONError(reqCtx.ResponseWriter, http.StatusBadRequest, err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
token, claims, err := buildLocalDevSessionToken(plan, trial, email, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
responseBody, err := marshalJSONBody(map[string]any{
|
||||
"accessToken": token,
|
||||
"refreshToken": token,
|
||||
"authId": claims.Subject,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseLocalDevSessionQuery(request *http.Request) (string, bool, string, error) {
|
||||
plan := localDevDefaultPlan
|
||||
email := legacyruntime.InjectAccountEmail
|
||||
if request == nil || request.URL == nil {
|
||||
return plan, false, email, nil
|
||||
}
|
||||
|
||||
query := request.URL.Query()
|
||||
if requestedPlan := strings.TrimSpace(query.Get("plan")); requestedPlan != "" {
|
||||
plan = requestedPlan
|
||||
}
|
||||
if _, ok := localDevPlans[plan]; !ok {
|
||||
return "", false, "", fmt.Errorf("unsupported dev plan %q", plan)
|
||||
}
|
||||
|
||||
trial := false
|
||||
if rawTrial := strings.TrimSpace(query.Get("trial")); rawTrial != "" {
|
||||
parsed, err := strconv.ParseBool(rawTrial)
|
||||
if err != nil {
|
||||
return "", false, "", fmt.Errorf("invalid trial value %q", rawTrial)
|
||||
}
|
||||
trial = parsed
|
||||
}
|
||||
if trial && plan != "pro" && plan != "pro_plus" {
|
||||
return "", false, "", fmt.Errorf("trial is only supported for pro and pro_plus")
|
||||
}
|
||||
|
||||
if requestedEmail := strings.TrimSpace(query.Get("email")); requestedEmail != "" {
|
||||
email = requestedEmail
|
||||
}
|
||||
return plan, trial, email, nil
|
||||
}
|
||||
|
||||
func buildLocalDevSessionToken(plan string, trial bool, email string, now time.Time) (string, localDevSessionClaims, error) {
|
||||
authID := "local-dev-" + strings.ReplaceAll(plan, "_", "-")
|
||||
if trial {
|
||||
authID += "-trial"
|
||||
}
|
||||
claims := localDevSessionClaims{
|
||||
Subject: authID,
|
||||
Email: strings.TrimSpace(email),
|
||||
Plan: plan,
|
||||
Trial: trial,
|
||||
TokenType: "session",
|
||||
Issuer: "cursor-local-backend",
|
||||
Scope: "openid profile email",
|
||||
IssuedAt: now.Unix(),
|
||||
ExpiresAt: now.Add(localDevTokenLifetime).Unix(),
|
||||
}
|
||||
headerJSON, err := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})
|
||||
if err != nil {
|
||||
return "", localDevSessionClaims{}, err
|
||||
}
|
||||
claimsJSON, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", localDevSessionClaims{}, err
|
||||
}
|
||||
encode := base64.RawURLEncoding.EncodeToString
|
||||
token := encode(headerJSON) + "." + encode(claimsJSON) + ".local-dev"
|
||||
return token, claims, nil
|
||||
}
|
||||
|
||||
func localDevClaimsFromRequest(reqCtx *RequestContext) (localDevSessionClaims, bool) {
|
||||
if reqCtx == nil {
|
||||
return localDevSessionClaims{}, false
|
||||
}
|
||||
return localDevClaimsFromAuthorization(reqCtx.Headers.Get("authorization"))
|
||||
}
|
||||
|
||||
func localDevClaimsFromAuthorization(authorization string) (localDevSessionClaims, bool) {
|
||||
authorization = strings.TrimSpace(authorization)
|
||||
if len(authorization) >= len("Bearer ") && strings.EqualFold(authorization[:len("Bearer ")], "Bearer ") {
|
||||
authorization = strings.TrimSpace(authorization[len("Bearer "):])
|
||||
}
|
||||
parts := strings.Split(authorization, ".")
|
||||
if len(parts) != 3 {
|
||||
return localDevSessionClaims{}, false
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return localDevSessionClaims{}, false
|
||||
}
|
||||
claims := localDevSessionClaims{}
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return localDevSessionClaims{}, false
|
||||
}
|
||||
if claims.Issuer != "cursor-local-backend" {
|
||||
return localDevSessionClaims{}, false
|
||||
}
|
||||
if _, ok := localDevPlans[claims.Plan]; !ok || strings.TrimSpace(claims.Subject) == "" {
|
||||
return localDevSessionClaims{}, false
|
||||
}
|
||||
return claims, true
|
||||
}
|
||||
|
||||
func localDevPlanFromRequest(reqCtx *RequestContext) string {
|
||||
if claims, ok := localDevClaimsFromRequest(reqCtx); ok {
|
||||
return claims.Plan
|
||||
}
|
||||
return localDevDefaultPlan
|
||||
}
|
||||
|
||||
func localDevPlanDetails(plan string) (string, int) {
|
||||
switch plan {
|
||||
case "free":
|
||||
return "Free Plan", 0
|
||||
case "pro":
|
||||
return "Pro Plan", 2000
|
||||
case "pro_plus":
|
||||
return "Pro+ Plan", 6000
|
||||
case "enterprise":
|
||||
return "Enterprise Plan", 0
|
||||
default:
|
||||
return "Ultra Plan", localUltraPlanIncludedCents
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSONError(writer http.ResponseWriter, statusCode int, message string) {
|
||||
writer.Header().Set("content-type", "application/json")
|
||||
writer.WriteHeader(statusCode)
|
||||
payload, _ := json.Marshal(map[string]string{"error": message})
|
||||
_, _ = writer.Write(payload)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package upstream
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cursor/gen/aiserverv1"
|
||||
"cursor/internal/backend/server"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestMockDevSessionTokenActionSupportsCursorDevLoginModes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
query string
|
||||
plan string
|
||||
trial bool
|
||||
}{
|
||||
{name: "default", query: "", plan: "ultra"},
|
||||
{name: "free", query: "?plan=free", plan: "free"},
|
||||
{name: "pro trial", query: "?plan=pro&trial=true", plan: "pro", trial: true},
|
||||
{name: "pro", query: "?plan=pro", plan: "pro"},
|
||||
{name: "pro plus trial", query: "?plan=pro_plus&trial=true", plan: "pro_plus", trial: true},
|
||||
{name: "pro plus", query: "?plan=pro_plus", plan: "pro_plus"},
|
||||
{name: "ultra", query: "?plan=ultra", plan: "ultra"},
|
||||
{name: "enterprise", query: "?plan=enterprise", plan: "enterprise"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodGet, "http://local/auth/cursor_dev_session_token"+testCase.query, nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
handler := MockDevSessionTokenAction(Dependencies{}, CompatRouteConfig{Name: "dev_login", StatusCode: http.StatusOK})
|
||||
if err := handler(&server.Context{Writer: recorder, Request: request}); err != nil {
|
||||
t.Fatalf("dev login handler: %v", err)
|
||||
}
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d, want %d; body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
|
||||
}
|
||||
|
||||
var response struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
AuthID string `json:"authId"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if response.AccessToken == "" || response.RefreshToken != response.AccessToken {
|
||||
t.Fatalf("unexpected tokens: access=%q refresh=%q", response.AccessToken, response.RefreshToken)
|
||||
}
|
||||
claims, ok := localDevClaimsFromAuthorization("Bearer " + response.AccessToken)
|
||||
if !ok {
|
||||
t.Fatal("response access token is not a local dev JWT")
|
||||
}
|
||||
if claims.Plan != testCase.plan || claims.Trial != testCase.trial {
|
||||
t.Fatalf("claims: got plan=%q trial=%v, want plan=%q trial=%v", claims.Plan, claims.Trial, testCase.plan, testCase.trial)
|
||||
}
|
||||
if response.AuthID != claims.Subject || claims.ExpiresAt <= time.Now().Unix() {
|
||||
t.Fatalf("unexpected identity claims: response=%+v claims=%+v", response, claims)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockDevSessionTokenActionUsesRequestedEmail(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodGet, "http://local/auth/cursor_dev_session_token?plan=pro&email=dev%2Bcursor%40example.com", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
handler := MockDevSessionTokenAction(Dependencies{}, CompatRouteConfig{Name: "dev_login", StatusCode: http.StatusOK})
|
||||
if err := handler(&server.Context{Writer: recorder, Request: request}); err != nil {
|
||||
t.Fatalf("dev login handler: %v", err)
|
||||
}
|
||||
|
||||
var response map[string]string
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
claims, ok := localDevClaimsFromAuthorization(response["accessToken"])
|
||||
if !ok || claims.Email != "dev+cursor@example.com" {
|
||||
t.Fatalf("unexpected email claims: %+v", claims)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockDevSessionTokenActionRejectsUnsupportedOptions(t *testing.T) {
|
||||
for _, query := range []string{"?plan=business", "?plan=ultra&trial=true", "?plan=pro&trial=maybe"} {
|
||||
request := httptest.NewRequest(http.MethodGet, "http://local/auth/cursor_dev_session_token"+query, nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
handler := MockDevSessionTokenAction(Dependencies{}, CompatRouteConfig{Name: "dev_login", StatusCode: http.StatusOK})
|
||||
if err := handler(&server.Context{Writer: recorder, Request: request}); err != nil {
|
||||
t.Fatalf("dev login handler for %q: %v", query, err)
|
||||
}
|
||||
if recorder.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status for %q: got %d, want %d", query, recorder.Code, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnterpriseDevSessionProvidesBillableTeam(t *testing.T) {
|
||||
token, _, err := buildLocalDevSessionToken("enterprise", false, "enterprise@example.com", time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("build token: %v", err)
|
||||
}
|
||||
reqCtx := authRequestContext(http.MethodPost, "/aiserver.v1.DashboardService/GetTeams", "", token)
|
||||
payload, err := buildDashboardTeamsPayload(reqCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("build teams: %v", err)
|
||||
}
|
||||
encoded, err := encodeMockProto("aiserver.v1.GetTeamsResponse", payload)
|
||||
if err != nil {
|
||||
t.Fatalf("encode teams: %v", err)
|
||||
}
|
||||
response := &aiserverv1.GetTeamsResponse{}
|
||||
if err := proto.Unmarshal(encoded, response); err != nil {
|
||||
t.Fatalf("decode teams: %v", err)
|
||||
}
|
||||
if len(response.Teams) != 1 || !response.Teams[0].GetHasBilling() || response.Teams[0].GetSeats() == 0 || !response.Teams[0].GetIsEnterprise() {
|
||||
t.Fatalf("unexpected enterprise teams: %+v", response.Teams)
|
||||
}
|
||||
}
|
||||
|
||||
func authRequestContext(method string, path string, body string, token string) *RequestContext {
|
||||
request := httptest.NewRequest(method, "http://local"+path, strings.NewReader(body))
|
||||
if token != "" {
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
return &RequestContext{
|
||||
ResponseWriter: httptest.NewRecorder(),
|
||||
Request: request,
|
||||
Method: method,
|
||||
Headers: request.Header.Clone(),
|
||||
RequestBody: []byte(body),
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,18 @@ package upstream
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cursor/gen/agentv1"
|
||||
"cursor/gen/aiserverv1"
|
||||
"cursor/internal/logger"
|
||||
"cursor/internal/netproxy"
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -87,14 +82,6 @@ func buildUpstreamRequest(reqCtx *RequestContext, body []byte, options ForwardOp
|
||||
}
|
||||
upstreamRequest.Host = reqCtx.TargetURL.Host
|
||||
|
||||
if shouldRewriteHost(reqCtx.TargetURL.Hostname()) {
|
||||
auth := formatBearerAuthorization(legacyruntime.LocalRelayToken)
|
||||
if auth == "" {
|
||||
return nil, nil, legacyruntime.ErrInvalidSystemSetting
|
||||
}
|
||||
upstreamRequest.Header.Set("Authorization", auth)
|
||||
upstreamRequest.Header.Set("x-cursor-checksum", BuildCursorChecksum(auth))
|
||||
}
|
||||
if options.PatchHeaders != nil {
|
||||
options.PatchHeaders(upstreamRequest.Header)
|
||||
}
|
||||
@@ -167,61 +154,21 @@ func copyRequestHeadersForUpstream(target http.Header, source http.Header) {
|
||||
}
|
||||
|
||||
func copyResponseHeadersToClient(target http.Header, source http.Header) {
|
||||
localWildcardCORS := target.Get("Access-Control-Allow-Origin") == "*"
|
||||
for key, values := range source {
|
||||
lowerKey := strings.ToLower(key)
|
||||
if _, exists := hopByHopHeaders[lowerKey]; exists {
|
||||
continue
|
||||
}
|
||||
if localWildcardCORS && (lowerKey == "access-control-allow-origin" || lowerKey == "access-control-allow-credentials") {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
target.Add(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRewriteHost(host string) bool {
|
||||
normalized := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(host)), ".")
|
||||
if normalized == "" {
|
||||
return false
|
||||
}
|
||||
return normalized == "cursor.sh" || strings.HasSuffix(normalized, ".cursor.sh")
|
||||
}
|
||||
|
||||
func BuildCursorChecksum(authorization string) string {
|
||||
const (
|
||||
checksumTimestampDivisor = 1_000_000
|
||||
checksumInitialSeed = 165
|
||||
)
|
||||
timestamp := time.Now().UnixMilli() / checksumTimestampDivisor
|
||||
timestampBytes := make([]byte, 6)
|
||||
timestampBigInt := big.NewInt(timestamp)
|
||||
for index := 0; index < len(timestampBytes); index++ {
|
||||
shift := uint((len(timestampBytes) - 1 - index) * 8)
|
||||
timestampBytes[index] = byte(new(big.Int).Rsh(timestampBigInt, shift).Uint64() & 0xff)
|
||||
}
|
||||
seed := checksumInitialSeed
|
||||
for index := 0; index < len(timestampBytes); index++ {
|
||||
current := int(timestampBytes[index]^byte(seed)) + (index % 256)
|
||||
current &= 0xff
|
||||
timestampBytes[index] = byte(current)
|
||||
seed = current
|
||||
}
|
||||
prefix := strings.TrimRight(base64.StdEncoding.EncodeToString(timestampBytes), "=")
|
||||
hashBytes := sha256.Sum256([]byte(strings.TrimSpace(authorization)))
|
||||
hash := fmt.Sprintf("%x", hashBytes)
|
||||
return prefix + hash[:32]
|
||||
}
|
||||
|
||||
func formatBearerAuthorization(raw string) string {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(value), "bearer ") {
|
||||
return value
|
||||
}
|
||||
return "Bearer " + value
|
||||
}
|
||||
|
||||
func shouldRequestCarryBody(method string) bool {
|
||||
switch strings.ToUpper(strings.TrimSpace(method)) {
|
||||
case http.MethodGet, http.MethodHead, http.MethodDelete:
|
||||
@@ -238,17 +185,6 @@ func marshalJSONBody(payload map[string]any) ([]byte, error) {
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
func handleMockJSON(reqCtx *RequestContext, route *Route) error {
|
||||
responseBody, err := marshalJSONBody(route.JSONBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(route.StatusCode)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockProto(reqCtx *RequestContext, route *Route) error {
|
||||
payload := map[string]any{}
|
||||
if route.MockPayloadBuilder != nil {
|
||||
@@ -270,91 +206,6 @@ func handleMockProto(reqCtx *RequestContext, route *Route) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockOAuth(reqCtx *RequestContext, route *Route) error {
|
||||
payload := struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}{}
|
||||
_ = json.Unmarshal(reqCtx.RequestBody, &payload)
|
||||
responseBody, err := marshalJSONBody(map[string]any{
|
||||
"access_token": payload.RefreshToken,
|
||||
"id_token": payload.RefreshToken,
|
||||
"shouldLogout": false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockAuthFullStripeProfile(reqCtx *RequestContext, route *Route) error {
|
||||
_ = route
|
||||
responseBody, err := marshalJSONBody(map[string]any{
|
||||
"membershipType": localUltraMembershipType,
|
||||
"subscriptionStatus": localUltraSubscriptionStatus,
|
||||
"lastPaymentFailed": false,
|
||||
"pendingCancellationDate": "",
|
||||
"daysRemainingOnTrial": 0,
|
||||
"paymentId": localUltraPaymentID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockAuthStripeProfile(reqCtx *RequestContext, route *Route) error {
|
||||
_ = route
|
||||
responseBody, err := json.Marshal(localUltraPaymentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockAuthPoll(reqCtx *RequestContext, route *Route) error {
|
||||
_ = route
|
||||
responseBody, err := marshalJSONBody(map[string]any{
|
||||
"accessToken": legacyruntime.InjectAuthToken,
|
||||
"refreshToken": legacyruntime.InjectAuthToken,
|
||||
"authId": "local_auth",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockAuthEmail(reqCtx *RequestContext, route *Route) error {
|
||||
_ = route
|
||||
responseBody := encodeAuthGetEmailResponse(legacyruntime.InjectAccountEmail)
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/proto")
|
||||
reqCtx.ResponseWriter.Header().Set("content-length", strconv.Itoa(len(responseBody)))
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeAuthGetEmailResponse(email string) []byte {
|
||||
output := make([]byte, 0, len(email)+8)
|
||||
output = append(output, 0x0a)
|
||||
output = appendProtoVarint(output, uint64(len(email)))
|
||||
output = append(output, []byte(email)...)
|
||||
output = append(output, 0x10, 0x03) // GetEmailResponse.SignUpType.SIGN_UP_TYPE_GOOGLE
|
||||
return output
|
||||
}
|
||||
|
||||
func appendProtoVarint(output []byte, value uint64) []byte {
|
||||
for value >= 0x80 {
|
||||
output = append(output, byte(value)|0x80)
|
||||
@@ -431,8 +282,6 @@ func newProtoMessage(typeName string) (proto.Message, error) {
|
||||
return &aiserverv1.GetTeamAdminSettingsResponse{}, nil
|
||||
case "aiserver.v1.GetTeamReposResponse":
|
||||
return &aiserverv1.GetTeamReposResponse{}, nil
|
||||
case "aiserver.v1.ListMarketplacesResponse":
|
||||
return &aiserverv1.ListMarketplacesResponse{}, nil
|
||||
case "aiserver.v1.GetUsableModelsResponse":
|
||||
return &agentv1.GetUsableModelsResponse{}, nil
|
||||
case "aiserver.v1.GetDefaultModelForCliResponse":
|
||||
@@ -441,10 +290,6 @@ func newProtoMessage(typeName string) (proto.Message, error) {
|
||||
return &aiserverv1.GetDefaultModelResponse{}, nil
|
||||
case "aiserver.v1.GetGlobalCommandsResponse":
|
||||
return &aiserverv1.GetGlobalCommandsResponse{}, nil
|
||||
case "aiserver.v1.GetEffectiveUserPluginsResponse":
|
||||
return &aiserverv1.GetEffectiveUserPluginsResponse{}, nil
|
||||
case "aiserver.v1.RegisterMarketplaceAndPluginsResponse":
|
||||
return &aiserverv1.RegisterMarketplaceAndPluginsResponse{}, nil
|
||||
case "aiserver.v1.GetCliDownloadUrlResponse":
|
||||
return &aiserverv1.GetCliDownloadUrlResponse{}, nil
|
||||
case "aiserver.v1.SubmitLogsResponse":
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package upstream
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cursor/internal/backend/server"
|
||||
)
|
||||
|
||||
type fallbackHTTPClientFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (fn fallbackHTTPClientFunc) Do(request *http.Request) (*http.Response, error) {
|
||||
return fn(request)
|
||||
}
|
||||
|
||||
func TestFallbackForwardActionUsesOriginalMITMUpstreamURL(t *testing.T) {
|
||||
originalURL := "https://api3.cursor.sh/aiserver.v1.UnknownService/Call?mode=exact"
|
||||
parsedURL, err := url.Parse(originalURL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse original URL: %v", err)
|
||||
}
|
||||
|
||||
client := fallbackHTTPClientFunc(func(request *http.Request) (*http.Response, error) {
|
||||
if got := request.URL.String(); got != originalURL {
|
||||
t.Fatalf("upstream URL: got %q, want %q", got, originalURL)
|
||||
}
|
||||
if got := request.Method; got != http.MethodPost {
|
||||
t.Fatalf("method: got %q, want POST", got)
|
||||
}
|
||||
body, readErr := io.ReadAll(request.Body)
|
||||
if readErr != nil {
|
||||
t.Fatalf("read request body: %v", readErr)
|
||||
}
|
||||
if got := string(body); got != "request-body" {
|
||||
t.Fatalf("body: got %q", got)
|
||||
}
|
||||
if got := request.Header.Get("X-Test-Header"); got != "preserved" {
|
||||
t.Fatalf("custom header: got %q", got)
|
||||
}
|
||||
if got := request.Header.Get(server.HeaderServerUpstreamURL); got != "" {
|
||||
t.Fatalf("internal upstream header leaked: %q", got)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
Status: "202 Accepted",
|
||||
Header: http.Header{"X-Upstream-Response": []string{"preserved"}},
|
||||
Body: io.NopCloser(strings.NewReader("upstream-body")),
|
||||
}, nil
|
||||
})
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "http://localhost:8000/ignored", strings.NewReader("request-body"))
|
||||
request.Header.Set("X-Test-Header", "preserved")
|
||||
request.Header.Set(server.HeaderServerUpstreamURL, originalURL)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := &server.Context{Writer: recorder, Request: request, UpstreamURL: parsedURL}
|
||||
action := FallbackForwardAction(Dependencies{HTTPClient: client}, CompatRouteConfig{Name: "fallback"}, DefaultCursorUpstreamBaseURL)
|
||||
|
||||
if err := action(ctx); err != nil {
|
||||
t.Fatalf("forward fallback request: %v", err)
|
||||
}
|
||||
if got := recorder.Code; got != http.StatusAccepted {
|
||||
t.Fatalf("response status: got %d, want %d", got, http.StatusAccepted)
|
||||
}
|
||||
if got := recorder.Header().Get("X-Upstream-Response"); got != "preserved" {
|
||||
t.Fatalf("response header: got %q", got)
|
||||
}
|
||||
if got := recorder.Body.String(); got != "upstream-body" {
|
||||
t.Fatalf("response body: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackForwardActionUsesDefaultUpstreamForNativeRequest(t *testing.T) {
|
||||
const defaultBaseURL = "https://fallback.example:8443"
|
||||
wantURL := defaultBaseURL + "/aiserver.v1.UnknownService/Call?mode=native"
|
||||
client := fallbackHTTPClientFunc(func(request *http.Request) (*http.Response, error) {
|
||||
if got := request.URL.String(); got != wantURL {
|
||||
t.Fatalf("upstream URL: got %q, want %q", got, wantURL)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNoContent,
|
||||
Status: "204 No Content",
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
}, nil
|
||||
})
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "http://localhost:8000/aiserver.v1.UnknownService/Call?mode=native", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := &server.Context{Writer: recorder, Request: request}
|
||||
action := FallbackForwardAction(Dependencies{HTTPClient: client}, CompatRouteConfig{Name: "fallback"}, defaultBaseURL)
|
||||
|
||||
if err := action(ctx); err != nil {
|
||||
t.Fatalf("forward fallback request: %v", err)
|
||||
}
|
||||
if got := recorder.Code; got != http.StatusNoContent {
|
||||
t.Fatalf("response status: got %d, want %d", got, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackForwardActionPreservesAuthorization(t *testing.T) {
|
||||
const (
|
||||
originalURL = "https://api2.cursor.sh/aiserver.v1.AuthService/GetEmail"
|
||||
officialAuthorization = "Bearer official-access-token"
|
||||
officialChecksum = "official-checksum"
|
||||
)
|
||||
parsedURL, err := url.Parse(originalURL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse original URL: %v", err)
|
||||
}
|
||||
|
||||
client := fallbackHTTPClientFunc(func(request *http.Request) (*http.Response, error) {
|
||||
if got := request.Header.Get("Authorization"); got != officialAuthorization {
|
||||
t.Fatalf("authorization: got %q, want %q", got, officialAuthorization)
|
||||
}
|
||||
if got := request.Header.Get("x-cursor-checksum"); got != officialChecksum {
|
||||
t.Fatalf("checksum: got %q, want %q", got, officialChecksum)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Status: "200 OK",
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader("upstream-account")),
|
||||
}, nil
|
||||
})
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "http://localhost:8000/aiserver.v1.AuthService/GetEmail", nil)
|
||||
request.Header.Set("Authorization", officialAuthorization)
|
||||
request.Header.Set("x-cursor-checksum", officialChecksum)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := &server.Context{Writer: recorder, Request: request, UpstreamURL: parsedURL}
|
||||
action := FallbackForwardAction(
|
||||
Dependencies{HTTPClient: client},
|
||||
CompatRouteConfig{Name: "fallback"},
|
||||
DefaultCursorUpstreamBaseURL,
|
||||
)
|
||||
|
||||
if err := action(ctx); err != nil {
|
||||
t.Fatalf("forward authenticated fallback request: %v", err)
|
||||
}
|
||||
if got := recorder.Body.String(); got != "upstream-account" {
|
||||
t.Fatalf("response body: got %q, want upstream-account", got)
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,7 @@ const (
|
||||
// файловых инструментов падают с "[unimplemented] HTTP 404".
|
||||
localPathEncryptionKey = "6f6e63652d6c6f63616c2d706174682d656e6372797074696f6e2d6b6579"
|
||||
|
||||
localUltraMembershipType = "ultra"
|
||||
localUltraPaymentID = "local_ultra"
|
||||
localUltraSubscriptionStatus = "active"
|
||||
localUltraPlanIncludedCents = 20000
|
||||
localUltraDashboardUserID = 1
|
||||
localUltraBillingCycleDuration = 30 * 24 * time.Hour
|
||||
@@ -431,7 +429,8 @@ func buildServerTimePayload(*RequestContext) (map[string]any, error) {
|
||||
|
||||
func buildServerConfigPayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"configVersion": "local_cli_sandbox_defaults_disabled_v2",
|
||||
"configVersion": "local_cli_sandbox_defaults_disabled_v2",
|
||||
"isDevDoNotUseForSecretThingsBecauseCanBeSpoofedByUsers": true,
|
||||
"http2Config": "HTTP2_CONFIG_FORCE_ALL_DISABLED",
|
||||
"cliSandboxDefaultEnabled": true,
|
||||
"indexingConfig": map[string]any{
|
||||
@@ -547,26 +546,29 @@ func buildFirstWindowStatsigDecisionPayload(*RequestContext) (map[string]any, er
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardCurrentPeriodUsagePayload(*RequestContext) (map[string]any, error) {
|
||||
func buildDashboardCurrentPeriodUsagePayload(reqCtx *RequestContext) (map[string]any, error) {
|
||||
plan := localDevPlanFromRequest(reqCtx)
|
||||
planName, includedSpend := localDevPlanDetails(plan)
|
||||
billingCycleStart := time.Now().Add(-localUltraBillingCycleDuration).UnixMilli()
|
||||
billingCycleEnd := time.Now().Add(10 * 365 * 24 * time.Hour).UnixMilli()
|
||||
displayMessage := planName + " active"
|
||||
return map[string]any{
|
||||
"autoModelSelectedDisplayMessage": "Ultra plan active",
|
||||
"autoModelSelectedDisplayMessage": displayMessage,
|
||||
"billingCycleEnd": billingCycleEnd,
|
||||
"billingCycleStart": billingCycleStart,
|
||||
"displayMessage": "Ultra plan active",
|
||||
"displayMessage": displayMessage,
|
||||
"displayThreshold": 99999999,
|
||||
"enabled": true,
|
||||
"namedModelSelectedDisplayMessage": "Ultra plan active",
|
||||
"namedModelSelectedDisplayMessage": displayMessage,
|
||||
"planUsage": map[string]any{
|
||||
"apiPercentUsed": 0,
|
||||
"apiSpend": 0,
|
||||
"autoPercentUsed": 0,
|
||||
"autoSpend": 0,
|
||||
"bonusTooltip": "Ultra local account mock is active.",
|
||||
"includedSpend": localUltraPlanIncludedCents,
|
||||
"limit": localUltraPlanIncludedCents,
|
||||
"remaining": localUltraPlanIncludedCents,
|
||||
"bonusTooltip": "Local account mock is active.",
|
||||
"includedSpend": includedSpend,
|
||||
"limit": includedSpend,
|
||||
"remaining": includedSpend,
|
||||
"remainingBonus": false,
|
||||
"totalPercentUsed": 0,
|
||||
"totalSpend": 0,
|
||||
@@ -577,62 +579,45 @@ func buildDashboardCurrentPeriodUsagePayload(*RequestContext) (map[string]any, e
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardTeamsPayload(*RequestContext) (map[string]any, error) {
|
||||
func buildDashboardTeamsPayload(reqCtx *RequestContext) (map[string]any, error) {
|
||||
if claims, ok := localDevClaimsFromRequest(reqCtx); ok && claims.Plan == "enterprise" {
|
||||
return map[string]any{
|
||||
"teams": []map[string]any{{
|
||||
"name": "Local Enterprise",
|
||||
"id": 1,
|
||||
"seats": 1,
|
||||
"hasBilling": true,
|
||||
"subscriptionStatus": localDevSubscriptionActive,
|
||||
"verified": true,
|
||||
"isEnterprise": true,
|
||||
"membershipType": "enterprise",
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
return map[string]any{
|
||||
"teams": []map[string]any{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardManagedSkillsPayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"skills": []map[string]any{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardGetMePayload(reqCtx *RequestContext) (map[string]any, error) {
|
||||
authID := ""
|
||||
if reqCtx != nil {
|
||||
authID = authIDFromBearer(reqCtx.Headers.Get("authorization"))
|
||||
func buildDashboardPlanInfoPayload(reqCtx *RequestContext) (map[string]any, error) {
|
||||
plan := localDevPlanFromRequest(reqCtx)
|
||||
planName, includedAmountCents := localDevPlanDetails(plan)
|
||||
price := "$200/mo"
|
||||
switch plan {
|
||||
case "free":
|
||||
price = "$0/mo"
|
||||
case "pro":
|
||||
price = "$20/mo"
|
||||
case "pro_plus":
|
||||
price = "$60/mo"
|
||||
case "enterprise":
|
||||
price = "Custom"
|
||||
}
|
||||
if authID == "" {
|
||||
authID = authIDFromJWT(legacyruntime.InjectAuthToken)
|
||||
}
|
||||
if authID == "" {
|
||||
authID = localUltraPaymentID
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"authId": authID,
|
||||
"userId": localUltraDashboardUserID,
|
||||
"email": legacyruntime.InjectAccountEmail,
|
||||
"firstName": "Cursor",
|
||||
"lastName": "Local",
|
||||
"createdAt": time.Now().UTC().Format(time.RFC3339),
|
||||
"isEnterpriseUser": false,
|
||||
"teamName": "",
|
||||
"emailDomainType": "personal",
|
||||
"country": "US",
|
||||
"profilePictureUrl": "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardUserPrivacyModePayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"privacyMode": "PRIVACY_MODE_NO_STORAGE",
|
||||
"hoursRemainingInGracePeriod": 0,
|
||||
"isEnforcedByTeam": false,
|
||||
"isNotMigratedToServerSourceOfTruth": false,
|
||||
"partnerDataShare": false,
|
||||
"hasAcknowledgedGracePeriodDisclaimer": true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardPlanInfoPayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"planInfo": map[string]any{
|
||||
"planName": "Ultra Plan",
|
||||
"includedAmountCents": localUltraPlanIncludedCents,
|
||||
"price": "$200/mo",
|
||||
"planName": planName,
|
||||
"includedAmountCents": includedAmountCents,
|
||||
"price": price,
|
||||
"billingCycleEnd": time.Now().Add(10 * 365 * 24 * time.Hour).UnixMilli(),
|
||||
},
|
||||
}, nil
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"cursor/gen/agentv1"
|
||||
"cursor/gen/aiserverv1"
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -54,6 +55,26 @@ func TestEncodeCLIModelsUsesAgentModelDetailsWireFormat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildServerConfigEnablesDevUserBackendCommands(t *testing.T) {
|
||||
payload, err := buildServerConfigPayload(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build server config: %v", err)
|
||||
}
|
||||
|
||||
encoded, err := encodeMockProto("aiserver.v1.GetServerConfigResponse", payload)
|
||||
if err != nil {
|
||||
t.Fatalf("encode server config: %v", err)
|
||||
}
|
||||
|
||||
response := &aiserverv1.GetServerConfigResponse{}
|
||||
if err := proto.Unmarshal(encoded, response); err != nil {
|
||||
t.Fatalf("decode server config: %v", err)
|
||||
}
|
||||
if !response.GetIsDevDoNotUseForSecretThingsBecauseCanBeSpoofedByUsers() {
|
||||
t.Fatal("expected server config to enable dev-user backend commands")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBootstrapStatsigConfigJSONDisablesAlwaysLocalDecompositionGate(t *testing.T) {
|
||||
payload, err := buildBootstrapStatsigConfigJSON(12345, "test-auth-id")
|
||||
if err != nil {
|
||||
|
||||
@@ -20,13 +20,6 @@ type SystemSettingService interface {
|
||||
ResolveModelAdapters(context.Context) ([]legacyruntime.ModelAdapterConfig, error)
|
||||
}
|
||||
|
||||
// AuthorizationProvider supplies the independent Cursor account used only by
|
||||
// official control-plane requests such as Plugins, Skills, and MCP registry.
|
||||
type AuthorizationProvider interface {
|
||||
Authorization(context.Context) (string, error)
|
||||
SignedIn() bool
|
||||
}
|
||||
|
||||
type HTTPClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
@@ -91,7 +84,6 @@ type Route struct {
|
||||
Matcher Matcher
|
||||
ConsoleLog bool
|
||||
StatusCode int
|
||||
JSONBody map[string]any
|
||||
MockProtoType string
|
||||
MockPayloadBuilder func(*RequestContext) (map[string]any, error)
|
||||
Handler RouteHandler
|
||||
|
||||
Reference in New Issue
Block a user