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:
leookun
2026-08-08 21:19:57 +08:00
parent 3cf8bdbc3c
commit c274a9db4c
32 changed files with 1225 additions and 1746 deletions
+54 -22
View File
@@ -19,87 +19,119 @@ type usageLookupRecord struct {
CreatedAt time.Time
}
type aiHandler struct {
mux *http.ServeMux
paths map[string]struct{}
}
func newAIHandlerMux() *aiHandler {
return &aiHandler{
mux: http.NewServeMux(),
paths: make(map[string]struct{}),
}
}
func (handler *aiHandler) Handle(pattern string, target http.Handler) {
handler.paths[pattern] = struct{}{}
handler.mux.Handle(pattern, target)
}
func (handler *aiHandler) HandlesPath(path string) bool {
if handler == nil {
return false
}
_, ok := handler.paths[path]
return ok
}
func (handler *aiHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if handler == nil || handler.mux == nil {
http.NotFound(writer, request)
return
}
handler.mux.ServeHTTP(writer, request)
}
const (
dashboardServiceGetTokenUsageProcedure = "/aiserver.v1.DashboardService/GetTokenUsage"
dashboardServiceGetGlassEarlyPreviewEnrollmentProcedure = "/aiserver.v1.DashboardService/GetGlassEarlyPreviewEnrollment"
)
func newAIHandler(service *Service) http.Handler {
mux := http.NewServeMux()
mux.Handle(
func newAIHandler(service *Service) *aiHandler {
handler := newAIHandlerMux()
handler.Handle(
dashboardServiceGetTokenUsageProcedure,
connect.NewUnaryHandler(dashboardServiceGetTokenUsageProcedure, service.GetTokenUsage),
)
mux.Handle(
handler.Handle(
dashboardServiceGetGlassEarlyPreviewEnrollmentProcedure,
connect.NewUnaryHandler(dashboardServiceGetGlassEarlyPreviewEnrollmentProcedure, service.GetGlassEarlyPreviewEnrollment),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceCountTokensProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceCountTokensProcedure, service.CountTokens),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceGetThoughtAnnotationProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceGetThoughtAnnotationProcedure, service.GetThoughtAnnotation),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceWriteGitCommitMessageProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceWriteGitCommitMessageProcedure, service.WriteGitCommitMessage),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceCreateExperimentalIndexProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceCreateExperimentalIndexProcedure, service.CreateExperimentalIndex),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceListExperimentalIndexFilesProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceListExperimentalIndexFilesProcedure, service.ListExperimentalIndexFiles),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceListenExperimentalIndexProcedure,
connect.NewServerStreamHandler(aiserverv1connect.AiServiceListenExperimentalIndexProcedure, service.ListenExperimentalIndex),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceRegisterFileToIndexProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceRegisterFileToIndexProcedure, service.RegisterFileToIndex),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceSetupIndexDependenciesProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceSetupIndexDependenciesProcedure, service.SetupIndexDependencies),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceComputeIndexTopoSortProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceComputeIndexTopoSortProcedure, service.ComputeIndexTopoSort),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceDocumentationQueryProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceDocumentationQueryProcedure, service.DocumentationQuery),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceAvailableDocsProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceAvailableDocsProcedure, service.AvailableDocs),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceKnowledgeBaseAddProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceKnowledgeBaseAddProcedure, service.KnowledgeBaseAdd),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceKnowledgeBaseListProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceKnowledgeBaseListProcedure, service.KnowledgeBaseList),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceKnowledgeBaseRemoveProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceKnowledgeBaseRemoveProcedure, service.KnowledgeBaseRemove),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceKnowledgeBaseUpdateProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceKnowledgeBaseUpdateProcedure, service.KnowledgeBaseUpdate),
)
mux.Handle(
handler.Handle(
aiserverv1connect.AiServiceFetchRelevantKnowledgeForConversationProcedure,
connect.NewUnaryHandler(aiserverv1connect.AiServiceFetchRelevantKnowledgeForConversationProcedure, service.FetchRelevantKnowledgeForConversation),
)
mux.Handle("/", http.NotFoundHandler())
return mux
return handler
}
func (service *Service) GetThoughtAnnotation(_ context.Context, req *connect.Request[aiserverv1.GetThoughtAnnotationRequest]) (*connect.Response[aiserverv1.GetThoughtAnnotationResponse], error) {
@@ -0,0 +1,20 @@
package forwarder
import (
"testing"
"cursor/gen/aiserverv1/aiserverv1connect"
)
func TestAIHandlerTracksLocallyImplementedPaths(t *testing.T) {
handler := newAIHandler(&Service{})
if !handler.HandlesPath(aiserverv1connect.AiServiceCountTokensProcedure) {
t.Fatalf("expected %q to be handled locally", aiserverv1connect.AiServiceCountTokensProcedure)
}
if !handler.HandlesPath(dashboardServiceGetTokenUsageProcedure) {
t.Fatalf("expected %q to be handled locally", dashboardServiceGetTokenUsageProcedure)
}
if handler.HandlesPath("/aiserver.v1.AiService/UnknownProcedure") {
t.Fatal("unknown AI procedure must fall through to upstream")
}
}
+8
View File
@@ -32,3 +32,11 @@ func NewModule(historyRoot string, channelService modeladapter.ChannelResolver)
UploadServiceHandler: newUploadServiceHandler(service),
}
}
func (module *Module) HandlesAIPath(path string) bool {
if module == nil || module.AiHandler == nil {
return false
}
handler, ok := module.AiHandler.(interface{ HandlesPath(string) bool })
return ok && handler.HandlesPath(path)
}
+119 -264
View File
@@ -2,6 +2,8 @@ package backend
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
@@ -27,11 +29,11 @@ const healthPath = "/healthz"
const tabServerBaseURL = "https://tab.leokun.cn"
type Host struct {
store *serverconfig.Store
listenAddr string
configs *serverconfig.Manager
healthHTTP *http.Client
controlPlaneAuth upstream.AuthorizationProvider
store *serverconfig.Store
listenAddr string
configs *serverconfig.Manager
healthHTTP *http.Client
tlsCertificate *tls.Certificate
runMu sync.RWMutex
httpServer *http.Server
@@ -41,7 +43,20 @@ type Host struct {
mux http.Handler
}
func NewHost(store *serverconfig.Store, controlPlaneAuth upstream.AuthorizationProvider) (*Host, error) {
type HostOption func(*Host) error
func WithTLSCertificate(certificate *tls.Certificate) HostOption {
return func(host *Host) error {
if certificate == nil || len(certificate.Certificate) == 0 || certificate.PrivateKey == nil {
return fmt.Errorf("backend TLS certificate is invalid")
}
copied := *certificate
host.tlsCertificate = &copied
return nil
}
}
func NewHost(store *serverconfig.Store, options ...HostOption) (*Host, error) {
if store == nil {
return nil, fmt.Errorf("backend config store is required")
}
@@ -51,12 +66,19 @@ func NewHost(store *serverconfig.Store, controlPlaneAuth upstream.AuthorizationP
}
cfg := configs.Current()
host := &Host{
store: store,
listenAddr: cfg.BackendListenAddr,
configs: configs,
healthHTTP: newLoopbackHTTPClient(),
controlPlaneAuth: controlPlaneAuth,
store: store,
listenAddr: cfg.BackendListenAddr,
configs: configs,
}
for _, option := range options {
if option == nil {
continue
}
if err := option(host); err != nil {
return nil, err
}
}
host.healthHTTP = newLoopbackHTTPClient(host.tlsCertificate)
if err := host.rebuild(cfg); err != nil {
return nil, err
}
@@ -107,7 +129,14 @@ func (host *Host) BaseURL() string {
if listenAddr == "" {
return ""
}
return "http://" + listenAddr
if host.tlsCertificate == nil {
return "http://" + listenAddr
}
serverName := "localhost"
if _, port, err := net.SplitHostPort(listenAddr); err == nil {
return "https://" + net.JoinHostPort(serverName, port)
}
return "https://" + listenAddr
}
func (host *Host) IsRunning() bool {
@@ -153,6 +182,12 @@ func (host *Host) Start() error {
host.lastRunErr = fmt.Errorf("监听内置后端 %s 失败: %w", host.listenAddr, err)
return host.lastRunErr
}
if host.tlsCertificate != nil {
listener = tls.NewListener(listener, &tls.Config{
Certificates: []tls.Certificate{*host.tlsCertificate},
MinVersion: tls.VersionTLS12,
})
}
host.listenAddr = listener.Addr().String()
host.httpServer = httpServer
host.lastRunErr = nil
@@ -202,7 +237,7 @@ func (host *Host) HealthCheck(ctx context.Context) error {
}
client := host.healthHTTP
if client == nil {
client = newLoopbackHTTPClient()
client = newLoopbackHTTPClient(host.tlsCertificate)
}
response, err := client.Do(request)
if err != nil {
@@ -245,19 +280,34 @@ func (host *Host) InProcessHealthCheck() error {
return nil
}
func newLoopbackHTTPClient() *http.Client {
func newLoopbackHTTPClient(certificate *tls.Certificate) *http.Client {
transport := &http.Transport{
Proxy: nil,
DialContext: (&net.Dialer{
Timeout: 1 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: false,
MaxIdleConns: 1,
MaxIdleConnsPerHost: 1,
IdleConnTimeout: 30 * time.Second,
}
if certificate != nil {
roots := x509.NewCertPool()
for _, rawCertificate := range certificate.Certificate[1:] {
parsed, err := x509.ParseCertificate(rawCertificate)
if err == nil {
roots.AddCert(parsed)
}
}
transport.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: roots,
ServerName: "localhost",
}
}
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DialContext: (&net.Dialer{
Timeout: 1 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: false,
MaxIdleConns: 1,
MaxIdleConnsPerHost: 1,
IdleConnTimeout: 30 * time.Second,
},
Transport: transport,
}
}
@@ -276,8 +326,20 @@ func (host *Host) rebuildLocked(cfg serverconfig.Config) error {
SystemSettingService: &serverSystemSettings{configs: host.configs},
HTTPClient: netproxy.NewHTTPClient(30000 * time.Second),
}
fallbackForward := upstream.FallbackForwardAction(
routeDeps,
upstream.CompatRouteConfig{Name: "upstream_fallback"},
upstream.DefaultCursorUpstreamBaseURL,
)
localAIAction := server.HTTPHandlerAction(agentModule.AiHandler)
aiServiceAction := func(ctx *server.Context) error {
if ctx != nil && ctx.Request != nil && ctx.Request.URL != nil && agentModule.HandlesAIPath(ctx.Request.URL.Path) {
return localAIAction(ctx)
}
return fallbackForward(ctx)
}
host.mux = server.New(
host.mux = withLocalBackendCORS(server.New(
server.Use(
server.Recover(),
server.ServerContext(),
@@ -427,19 +489,11 @@ func (host *Host) rebuildLocked(cfg serverconfig.Config) error {
StatusCode: http.StatusOK,
})),
),
server.POST("/oauth/token",
server.Name("oauth_token"),
server.GET("/auth/cursor_dev_session_token",
server.Name("auth_cursor_dev_session_token"),
server.HTTP(),
server.Local(upstream.MockOAuthAction(routeDeps, upstream.CompatRouteConfig{
Name: "oauth_token",
StatusCode: http.StatusOK,
})),
),
server.POST("/aiserver.v1.AuthService/GetEmail",
server.Name("auth_service_get_email"),
server.ConnectUnary(),
server.Local(upstream.MockAuthEmailAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_service_get_email",
server.Local(upstream.MockDevSessionTokenAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_cursor_dev_session_token",
StatusCode: http.StatusOK,
})),
),
@@ -476,17 +530,14 @@ func (host *Host) rebuildLocked(cfg serverconfig.Config) error {
server.Any("/aiserver.v1.AiService/*",
server.Name("ai_service"),
server.HTTP(),
server.Local(server.HTTPHandlerAction(agentModule.AiHandler)),
server.Local(aiServiceAction),
),
tabServerProcedure("/aiserver.v1.CppService/AvailableModels", "cpp_available_models", server.ConnectUnary(), routeDeps),
tabServerProcedure("/aiserver.v1.CppService/RecordCppFate", "cpp_record_cpp_fate", server.ConnectUnary(), routeDeps),
server.Any("/aiserver.v1.CppService/*",
server.Name("cpp_service"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
server.Local(fallbackForward),
),
tabServerProcedure("/aiserver.v1.FileSyncService/FSSyncFile", "file_sync_sync_file", server.ConnectUnary(), routeDeps),
tabServerProcedure("/aiserver.v1.FileSyncService/FSIsEnabledForUser", "file_sync_is_enabled_for_user", server.ConnectUnary(), routeDeps),
@@ -495,10 +546,7 @@ func (host *Host) rebuildLocked(cfg serverconfig.Config) error {
server.Any("/aiserver.v1.FileSyncService/*",
server.Name("file_sync"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
server.Local(fallbackForward),
),
server.POST("/aiserver.v1.DashboardService/GetTokenUsage",
server.Name("dashboard_token_usage"),
@@ -530,21 +578,6 @@ func (host *Host) rebuildLocked(cfg serverconfig.Config) error {
MockBuilder: upstream.DashboardTeamsMockBuilder,
})),
),
server.POST("/aiserver.v1.DashboardService/GetManagedSkills",
server.Name("dashboard_get_managed_skills"),
server.ConnectUnary(),
server.Local(cursorControlPlaneAction(
host.controlPlaneAuth,
routeDeps,
"dashboard_get_managed_skills",
upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_managed_skills",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetManagedSkillsResponse",
MockBuilder: upstream.DashboardManagedSkillsMockBuilder,
}),
)),
),
server.POST("/aiserver.v1.DashboardService/GetTeamAdminSettingsOrEmptyIfNotInTeam",
server.Name("dashboard_get_team_admin_settings_or_empty"),
server.ConnectUnary(),
@@ -565,76 +598,6 @@ func (host *Host) rebuildLocked(cfg serverconfig.Config) error {
MockBuilder: upstream.EmptyMockBuilder,
})),
),
server.POST("/aiserver.v1.DashboardService/ListMarketplaces",
server.Name("dashboard_list_marketplaces"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_list_marketplaces",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.ListMarketplacesResponse",
MockBuilder: upstream.EmptyMockBuilder,
})),
),
server.POST("/aiserver.v1.DashboardService/GetGlobalCommands",
server.Name("dashboard_get_global_commands"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_global_commands",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetGlobalCommandsResponse",
MockBuilder: upstream.EmptyMockBuilder,
})),
),
server.POST("/aiserver.v1.DashboardService/GetEffectiveUserPlugins",
server.Name("dashboard_get_effective_user_plugins"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_effective_user_plugins",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetEffectiveUserPluginsResponse",
MockBuilder: upstream.EmptyMockBuilder,
})),
),
server.POST("/aiserver.v1.DashboardService/RegisterMarketplaceAndPlugins",
server.Name("dashboard_register_marketplace_and_plugins"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_register_marketplace_and_plugins",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.RegisterMarketplaceAndPluginsResponse",
MockBuilder: upstream.EmptyMockBuilder,
})),
),
server.POST("/aiserver.v1.DashboardService/GetCliDownloadUrl",
server.Name("dashboard_get_cli_download_url"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_cli_download_url",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetCliDownloadUrlResponse",
MockBuilder: upstream.EmptyMockBuilder,
})),
),
server.POST("/aiserver.v1.DashboardService/GetMe",
server.Name("dashboard_get_me"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_get_me",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetMeResponse",
MockBuilder: upstream.DashboardGetMeMockBuilder,
})),
),
server.POST("/aiserver.v1.DashboardService/GetUserPrivacyMode",
server.Name("dashboard_user_privacy_mode"),
server.ConnectUnary(),
server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{
Name: "dashboard_user_privacy_mode",
StatusCode: http.StatusOK,
MockProtoType: "aiserver.v1.GetUserPrivacyModeResponse",
MockBuilder: upstream.DashboardUserPrivacyModeMockBuilder,
})),
),
server.POST("/aiserver.v1.DashboardService/GetPlanInfo",
server.Name("dashboard_plan_info"),
server.ConnectUnary(),
@@ -665,104 +628,36 @@ func (host *Host) rebuildLocked(cfg serverconfig.Config) error {
MockBuilder: upstream.DashboardIsOnNewPricingMockBuilder,
})),
),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/AddMarketplace", "dashboard_add_marketplace", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/AddMcpServersFromPlugin", "dashboard_add_mcp_servers_from_plugin", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/BatchGetPluginMcpConfig", "dashboard_batch_get_plugin_mcp_config", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/GetAvailableMcpServers", "dashboard_get_available_mcp_servers", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/GetEffectiveUserPlugins", "dashboard_get_effective_user_plugins", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/GetPlugin", "dashboard_get_plugin", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/GetPluginMcpConfig", "dashboard_get_plugin_mcp_config", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/InstallUserPlugin", "dashboard_install_user_plugin", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/ListMarketplacePlugins", "dashboard_list_marketplace_plugins", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/ListMarketplaces", "dashboard_list_marketplaces", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/ListUserPluginInstalls", "dashboard_list_user_plugin_installs", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/RefreshMarketplace", "dashboard_refresh_marketplace", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/RegisterMarketplaceAndPlugins", "dashboard_register_marketplace_and_plugins", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/RemoveMarketplace", "dashboard_remove_marketplace", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/ResolvePluginsByRef", "dashboard_resolve_plugins_by_ref", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/UninstallUserPlugin", "dashboard_uninstall_user_plugin", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.DashboardService/UpdateUserPluginInstall", "dashboard_update_user_plugin_install", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
cursorControlPlaneProcedure("/aiserver.v1.MCPRegistryService/GetKnownServers", "mcp_registry_get_known_servers", server.ConnectUnary(), host.controlPlaneAuth, routeDeps),
server.Any("/aiserver.v1.DashboardService/*",
server.Name("dashboard"),
server.Any("/*",
server.Name("upstream_fallback"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
server.Local(fallbackForward),
),
server.Any("/aiserver.v1.NetworkService/*",
server.Name("network_service"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
),
server.Any("/aiserver.v1.InAppAdService/*",
server.Name("in_app_ad"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
),
server.GET("/auth/full_stripe_profile",
server.Name("auth_full_stripe_profile"),
server.HTTP(),
server.Local(upstream.MockAuthFullStripeProfileAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_full_stripe_profile",
StatusCode: http.StatusOK,
})),
),
server.GET("/auth/stripe_profile",
server.Name("auth_stripe_profile"),
server.HTTP(),
server.Local(upstream.MockAuthStripeProfileAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_stripe_profile",
StatusCode: http.StatusOK,
})),
),
server.GET("/auth/has_valid_payment_method",
server.Name("auth_has_valid_payment_method"),
server.HTTP(),
server.Local(upstream.MockJSONAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_has_valid_payment_method",
StatusCode: http.StatusOK,
JSONBody: map[string]any{
"hasValidPaymentMethod": true,
},
})),
),
server.Any("/auth/poll",
server.Name("auth_poll"),
server.HTTP(),
server.Local(upstream.MockAuthPollAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_poll",
StatusCode: http.StatusOK,
})),
),
server.POST("/auth/logout",
server.Name("auth_logout"),
server.HTTP(),
server.Local(upstream.FixedStatusAction(routeDeps, upstream.CompatRouteConfig{
Name: "auth_logout",
StatusCode: http.StatusNoContent,
})),
),
server.Any("/auth/*",
server.Name("auth_proxy"),
server.HTTP(),
server.Local(func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}),
),
)
))
return nil
}
func withLocalBackendCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Del("Access-Control-Allow-Credentials")
if strings.EqualFold(request.Method, http.MethodOptions) && strings.TrimSpace(request.Header.Get("Access-Control-Request-Method")) != "" {
writer.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
requestedHeaders := strings.TrimSpace(request.Header.Get("Access-Control-Request-Headers"))
if requestedHeaders == "" {
requestedHeaders = "authorization,content-type,x-cursor-client-type"
}
writer.Header().Set("Access-Control-Allow-Headers", requestedHeaders)
writer.Header().Set("Access-Control-Max-Age", "86400")
writer.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(writer, request)
})
}
func repositoryServiceProcedure(pattern string, name string, protocol server.RouteOption, module *forwarder.Module) server.Option {
localAction := server.HTTPHandlerAction(module.RepositoryServiceHandler)
return server.POST(pattern,
@@ -803,46 +698,6 @@ func tabServerProcedure(pattern string, name string, protocol server.RouteOption
)
}
func cursorControlPlaneProcedure(
pattern string,
name string,
protocol server.RouteOption,
authorizationProvider upstream.AuthorizationProvider,
deps upstream.Dependencies,
) server.Option {
notFound := func(ctx *server.Context) error {
http.NotFound(ctx.Writer, ctx.Request)
return nil
}
return server.POST(pattern,
server.Name(name),
protocol,
server.Local(cursorControlPlaneAction(authorizationProvider, deps, name, notFound)),
)
}
func cursorControlPlaneAction(
authorizationProvider upstream.AuthorizationProvider,
deps upstream.Dependencies,
name string,
fallback server.HandlerFunc,
) server.HandlerFunc {
forward := upstream.AuthenticatedForwardAction(deps, upstream.CompatRouteConfig{Name: name}, authorizationProvider)
return func(ctx *server.Context) error {
if authorizationProvider == nil || !authorizationProvider.SignedIn() {
return fallback(ctx)
}
if ctx == nil || ctx.Request == nil || ctx.Request.URL == nil {
return fmt.Errorf("Cursor 控制面请求上下文无效")
}
targetURL := *ctx.Request.URL
targetURL.Scheme = "https"
targetURL.Host = "api2.cursor.sh:443"
ctx.UpstreamURL = &targetURL
return forward(ctx)
}
}
type serverSystemSettings struct {
configs *serverconfig.Manager
}
+175
View File
@@ -0,0 +1,175 @@
package backend
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"cursor/gen/aiserverv1"
serverconfig "cursor/internal/backend/server/config"
"cursor/internal/certs"
"google.golang.org/protobuf/proto"
)
func TestHostServesDevLoginAndLocalTeamsRoute(t *testing.T) {
store := serverconfig.NewStore(filepath.Join(t.TempDir(), "config.yaml"), t.TempDir())
host, err := NewHost(store)
if err != nil {
t.Fatalf("new host: %v", err)
}
loginRequest := httptest.NewRequest(http.MethodGet, "http://local/auth/cursor_dev_session_token?plan=enterprise&email=enterprise%40example.com", nil)
loginRecorder := httptest.NewRecorder()
host.mux.ServeHTTP(loginRecorder, loginRequest)
if loginRecorder.Code != http.StatusOK {
t.Fatalf("dev login status: got %d, want %d; body=%s", loginRecorder.Code, http.StatusOK, loginRecorder.Body.String())
}
var loginResponse struct {
AccessToken string `json:"accessToken"`
}
if err := json.Unmarshal(loginRecorder.Body.Bytes(), &loginResponse); err != nil {
t.Fatalf("decode dev login: %v", err)
}
if loginResponse.AccessToken == "" {
t.Fatal("dev login returned an empty access token")
}
teamsRequest := httptest.NewRequest(http.MethodPost, "http://local/aiserver.v1.DashboardService/GetTeams", nil)
teamsRequest.Header.Set("Authorization", "Bearer "+loginResponse.AccessToken)
teamsRecorder := httptest.NewRecorder()
host.mux.ServeHTTP(teamsRecorder, teamsRequest)
if teamsRecorder.Code != http.StatusOK {
t.Fatalf("teams status: got %d, want %d", teamsRecorder.Code, http.StatusOK)
}
teams := &aiserverv1.GetTeamsResponse{}
if err := proto.Unmarshal(teamsRecorder.Body.Bytes(), teams); err != nil {
t.Fatalf("decode teams response: %v", err)
}
if len(teams.GetTeams()) != 1 || !teams.GetTeams()[0].GetIsEnterprise() {
t.Fatalf("unexpected teams response: %v", teams.GetTeams())
}
}
func TestHostAllowsWildcardCORS(t *testing.T) {
store := serverconfig.NewStore(filepath.Join(t.TempDir(), "config.yaml"), t.TempDir())
host, err := NewHost(store)
if err != nil {
t.Fatalf("new host: %v", err)
}
preflightRequest := httptest.NewRequest(http.MethodOptions, "http://local/auth/cursor_dev_session_token?plan=free", nil)
preflightRequest.Header.Set("Origin", "vscode-file://vscode-app")
preflightRequest.Header.Set("Access-Control-Request-Method", http.MethodGet)
preflightRequest.Header.Set("Access-Control-Request-Headers", "x-cursor-client-type")
preflightRecorder := httptest.NewRecorder()
host.mux.ServeHTTP(preflightRecorder, preflightRequest)
if preflightRecorder.Code != http.StatusNoContent {
t.Fatalf("preflight status: got %d, want %d", preflightRecorder.Code, http.StatusNoContent)
}
if got := preflightRecorder.Header().Get("Access-Control-Allow-Origin"); got != "*" {
t.Fatalf("preflight allow origin: got %q", got)
}
if got := preflightRecorder.Header().Get("Access-Control-Allow-Credentials"); got != "" {
t.Fatalf("preflight allow credentials: got %q, want empty", got)
}
if got := preflightRecorder.Header().Get("Access-Control-Allow-Headers"); got != "x-cursor-client-type" {
t.Fatalf("preflight allow headers: got %q", got)
}
loginRequest := httptest.NewRequest(http.MethodGet, "http://local/auth/cursor_dev_session_token?plan=free", nil)
loginRequest.Header.Set("Origin", "vscode-file://vscode-app")
loginRequest.Header.Set("x-cursor-client-type", "ide")
loginRecorder := httptest.NewRecorder()
host.mux.ServeHTTP(loginRecorder, loginRequest)
if loginRecorder.Code != http.StatusOK {
t.Fatalf("dev login status: got %d, want %d", loginRecorder.Code, http.StatusOK)
}
if got := loginRecorder.Header().Get("Access-Control-Allow-Origin"); got != "*" {
t.Fatalf("dev login allow origin: got %q", got)
}
}
func TestHostAllowsRemoteWebOriginWithWildcard(t *testing.T) {
store := serverconfig.NewStore(filepath.Join(t.TempDir(), "config.yaml"), t.TempDir())
host, err := NewHost(store)
if err != nil {
t.Fatalf("new host: %v", err)
}
request := httptest.NewRequest(http.MethodOptions, "http://local/auth/cursor_dev_session_token", nil)
request.Header.Set("Origin", "https://example.com")
request.Header.Set("Access-Control-Request-Method", http.MethodGet)
recorder := httptest.NewRecorder()
host.mux.ServeHTTP(recorder, request)
if got := recorder.Header().Get("Access-Control-Allow-Origin"); got != "*" {
t.Fatalf("remote origin allow origin: got %q, want wildcard", got)
}
}
func TestHostServesDevLoginOverTrustedLocalhostTLS(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("reserve backend port: %v", err)
}
listenAddr := listener.Addr().String()
if err := listener.Close(); err != nil {
t.Fatalf("release backend port: %v", err)
}
store := serverconfig.NewStore(filepath.Join(t.TempDir(), "config.yaml"), t.TempDir())
config := serverconfig.DefaultConfig()
config.BackendListenAddr = listenAddr
if _, err := store.Save(context.Background(), config); err != nil {
t.Fatalf("save backend config: %v", err)
}
certificateManager, err := certs.NewEmbeddedManager()
if err != nil {
t.Fatalf("new certificate manager: %v", err)
}
serverCertificate, err := certificateManager.CertificateForServerName("localhost")
if err != nil {
t.Fatalf("create localhost certificate: %v", err)
}
host, err := NewHost(store, WithTLSCertificate(serverCertificate))
if err != nil {
t.Fatalf("new TLS host: %v", err)
}
if err := host.Start(); err != nil {
t.Fatalf("start TLS host: %v", err)
}
defer func() {
stopContext, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := host.Stop(stopContext); err != nil {
t.Errorf("stop TLS host: %v", err)
}
}()
caCertificate, err := certificateManager.CATLSCertificate()
if err != nil {
t.Fatalf("load CA certificate: %v", err)
}
roots := x509.NewCertPool()
roots.AddCert(caCertificate.Leaf)
client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: roots,
ServerName: "localhost",
}}}
response, err := client.Get(host.BaseURL() + "/auth/cursor_dev_session_token?plan=pro&trial=true")
if err != nil {
t.Fatalf("request dev login over TLS: %v", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("dev login TLS status: got %d, want %d", response.StatusCode, http.StatusOK)
}
}
+127
View File
@@ -0,0 +1,127 @@
package backend
import (
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"cursor/internal/backend/server"
serverconfig "cursor/internal/backend/server/config"
)
func TestHostForwardsUnhandledRoutesToOriginalUpstream(t *testing.T) {
var requestCount atomic.Int32
upstreamServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
requestCount.Add(1)
body, err := io.ReadAll(request.Body)
if err != nil {
t.Errorf("read upstream request body: %v", err)
}
writer.Header().Set("X-Upstream-Path", request.URL.RequestURI())
writer.WriteHeader(http.StatusMultiStatus)
_, _ = writer.Write(body)
}))
defer upstreamServer.Close()
store := serverconfig.NewStore(filepath.Join(t.TempDir(), "config.yaml"), t.TempDir())
host, err := NewHost(store)
if err != nil {
t.Fatalf("new host: %v", err)
}
testCases := []struct {
name string
method string
path string
}{
{name: "managed skills", path: "/aiserver.v1.DashboardService/GetManagedSkills?source=skills"},
{name: "effective plugins", path: "/aiserver.v1.DashboardService/GetEffectiveUserPlugins?source=plugins"},
{name: "MCP registry", path: "/aiserver.v1.MCPRegistryService/GetKnownServers?source=mcp"},
{name: "auth poll", path: "/auth/poll?uuid=local-login&verifier=test"},
{name: "OAuth token", path: "/oauth/token"},
{name: "auth email", path: "/aiserver.v1.AuthService/GetEmail"},
{name: "dashboard me", path: "/aiserver.v1.DashboardService/GetMe"},
{name: "full stripe profile", method: http.MethodGet, path: "/auth/full_stripe_profile"},
{name: "stripe profile", method: http.MethodGet, path: "/auth/stripe_profile"},
{name: "valid payment method", method: http.MethodGet, path: "/auth/has_valid_payment_method"},
{name: "auth logout", path: "/auth/logout"},
{name: "dashboard global commands", path: "/aiserver.v1.DashboardService/GetGlobalCommands"},
{name: "dashboard CLI download", path: "/aiserver.v1.DashboardService/GetCliDownloadUrl"},
{name: "dashboard privacy mode", path: "/aiserver.v1.DashboardService/GetUserPrivacyMode"},
{name: "service catch-all", path: "/aiserver.v1.NetworkService/UnknownProcedure?source=network"},
{name: "AI handler miss", path: "/aiserver.v1.AiService/UnknownProcedure?source=ai"},
{name: "global miss", path: "/unknown/service/path?source=global"},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
method := testCase.method
if method == "" {
method = http.MethodPost
}
body := "payload-" + testCase.name
request := httptest.NewRequest(method, "http://localhost:8000"+testCase.path, strings.NewReader(body))
request.Header.Set(server.HeaderServerUpstreamURL, upstreamServer.URL+testCase.path)
recorder := httptest.NewRecorder()
host.mux.ServeHTTP(recorder, request)
if got := recorder.Code; got != http.StatusMultiStatus {
t.Fatalf("status: got %d, want %d; body=%s", got, http.StatusMultiStatus, recorder.Body.String())
}
if got := recorder.Header().Get("X-Upstream-Path"); got != testCase.path {
t.Fatalf("upstream path: got %q, want %q", got, testCase.path)
}
wantBody := body
if method == http.MethodGet {
wantBody = ""
}
if got := recorder.Body.String(); got != wantBody {
t.Fatalf("response body: got %q, want %q", got, wantBody)
}
})
}
requestsBeforeHealthCheck := requestCount.Load()
healthRequest := httptest.NewRequest(http.MethodGet, "http://localhost:8000"+healthPath, nil)
healthRecorder := httptest.NewRecorder()
host.mux.ServeHTTP(healthRecorder, healthRequest)
if got := healthRecorder.Code; got != http.StatusOK {
t.Fatalf("health status: got %d, want %d", got, http.StatusOK)
}
if got := requestCount.Load(); got != requestsBeforeHealthCheck {
t.Fatalf("local health route unexpectedly reached upstream: requests before=%d after=%d", requestsBeforeHealthCheck, got)
}
}
func TestHostFallbackKeepsWildcardCORSWhenUpstreamReturnsCORSHeaders(t *testing.T) {
upstreamServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Access-Control-Allow-Origin", "vscode-file://vscode-app")
writer.Header().Set("Access-Control-Allow-Credentials", "true")
writer.WriteHeader(http.StatusOK)
}))
defer upstreamServer.Close()
store := serverconfig.NewStore(filepath.Join(t.TempDir(), "config.yaml"), t.TempDir())
host, err := NewHost(store)
if err != nil {
t.Fatalf("new host: %v", err)
}
request := httptest.NewRequest(http.MethodGet, "http://localhost:8000/auth/poll?uuid=test", nil)
request.Header.Set("Origin", "vscode-file://vscode-app")
request.Header.Set(server.HeaderServerUpstreamURL, upstreamServer.URL+request.URL.RequestURI())
recorder := httptest.NewRecorder()
host.mux.ServeHTTP(recorder, request)
if got := recorder.Header().Values("Access-Control-Allow-Origin"); len(got) != 1 || got[0] != "*" {
t.Fatalf("allow origin values: got %q, want [*]", got)
}
if got := recorder.Header().Get("Access-Control-Allow-Credentials"); got != "" {
t.Fatalf("allow credentials: got %q, want empty", got)
}
}
+1 -1
View File
@@ -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
+22 -88
View File
@@ -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)
}
+193
View File
@@ -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),
}
}
+4 -159
View File
@@ -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)
}
}
+44 -59
View File
@@ -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