package service import ( "context" "fmt" "log/slog" "time" "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/domain" "github.com/Wei-Shaw/sub2api/internal/pkg/windsurf" ) type WindsurfLSService struct { cfg config.WindsurfConfig connector windsurf.LSConnector } func NewWindsurfLSService(cfg config.WindsurfConfig, pool *windsurf.LSPool) *WindsurfLSService { var connector windsurf.LSConnector switch cfg.LSMode { case "docker": connector = windsurf.NewCompatDockerConnector( cfg.Docker.Host, cfg.Docker.Port, windsurf.DockerDiscoveryConfig{ DefaultCSRFToken: cfg.Docker.CSRFToken, ProbeInterval: cfg.Docker.ProbeInterval, ProbeTimeout: cfg.Docker.ProbeTimeout, DiscoverInterval: cfg.Docker.DiscoverInterval, }, ) case "embedded": connector = windsurf.NewEmbeddedConnector(pool) case "external": port := 0 if cfg.External.BaseURL != "" { port = 443 } connector = windsurf.NewExternalConnector( cfg.External.BaseURL, port, cfg.External.CSRFToken, ) default: connector = windsurf.NewDockerConnector( cfg.Docker.Host, cfg.Docker.Port, cfg.Docker.CSRFToken, ) slog.Warn("windsurf_ls_unknown_mode", "mode", cfg.LSMode, "fallback", "docker") } return &WindsurfLSService{ cfg: cfg, connector: connector, } } func (s *WindsurfLSService) Connector() windsurf.LSConnector { return s.connector } func (s *WindsurfLSService) Acquire(ctx context.Context, proxyURL string) (*windsurf.LSLease, error) { return s.connector.Acquire(ctx, proxyURL) } func (s *WindsurfLSService) AcquireByBinding(binding WindsurfLSBinding) (*windsurf.LSLease, error) { if binding.ContainerID == "" && binding.ContainerName == "" { return s.connector.Acquire(context.Background(), "") } if dc, ok := s.connector.(*windsurf.DockerDiscoveryConnector); ok { id := binding.ContainerID if id == "" { id = binding.ContainerName } return dc.AcquireByID(id) } return s.connector.Acquire(context.Background(), "") } func (s *WindsurfLSService) Health(ctx context.Context) error { return s.connector.Health(ctx) } func (s *WindsurfLSService) Status() *windsurf.LSConnectorStatus { return s.connector.Status() } // Stop terminates LS resources owned by this service (embedded LS processes, // docker discovery goroutines). Safe to call on a nil receiver. func (s *WindsurfLSService) Stop() { if s == nil || s.connector == nil { return } s.connector.Shutdown() } type WindsurfAuthService struct { cfg config.WindsurfConfig authClient *windsurf.AuthClient accountRepo AccountRepository proxyRepo ProxyRepository adminSvc AdminService } func NewWindsurfAuthService( cfg config.WindsurfConfig, accountRepo AccountRepository, proxyRepo ProxyRepository, adminSvc AdminService, ) *WindsurfAuthService { authClient := &windsurf.AuthClient{ Auth1BaseURL: cfg.Auth1BaseURL, SeatServiceBaseURL: cfg.SeatServiceBaseURL, CodeiumRegisterURL: cfg.CodeiumRegisterURL, FirebaseAPIKey: cfg.FirebaseAPIKey, RequestTimeout: cfg.RequestTimeout, } return &WindsurfAuthService{ cfg: cfg, authClient: authClient, accountRepo: accountRepo, proxyRepo: proxyRepo, adminSvc: adminSvc, } } type WindsurfLoginInput struct { Email string Password string Name string Notes *string ProxyID *int64 GroupIDs []int64 Concurrency int Priority int ProbeAfter bool LSInstanceID string } type WindsurfLoginOutput struct { AccountID int64 `json:"account_id"` Email string `json:"email"` Tier string `json:"tier"` AuthMethod string `json:"auth_method"` APIKeyPresent bool `json:"api_key_present"` RefreshTokenPresent bool `json:"refresh_token_present"` } func (s *WindsurfAuthService) Login(ctx context.Context, input *WindsurfLoginInput) (*WindsurfLoginOutput, error) { existing, err := s.accountRepo.FindByCredentialField(ctx, domain.PlatformWindsurf, "email", input.Email) if err != nil { return nil, fmt.Errorf("check existing account: %w", err) } if len(existing) > 0 { return nil, fmt.Errorf("windsurf account with email %s already exists (account_id=%d)", input.Email, existing[0].ID) } proxyURL := "" if input.ProxyID != nil { proxy, err := s.proxyRepo.GetByID(ctx, *input.ProxyID) if err != nil { return nil, fmt.Errorf("get proxy: %w", err) } proxyURL = proxy.URL() } result, err := s.authClient.Login(ctx, input.Email, input.Password, proxyURL) if err != nil { return nil, err } creds := WindsurfCredentials{ Email: input.Email, APIKey: result.APIKey, RefreshToken: result.RefreshToken, IDToken: result.IDToken, SessionToken: result.SessionToken, Auth1Token: result.Auth1Token, AuthMethod: result.AuthMethod, APIServerURL: result.APIServerURL, RegisteredAt: time.Now().Format(time.RFC3339), } expiresAt := time.Now().Add(50 * time.Minute) creds.ExpiresAt = expiresAt.Format(time.RFC3339) credMap := StoreWindsurfCredentials(creds) extra := WindsurfExtra{ Profile: WindsurfProfileSnapshot{ TierSource: "login", }, Refresh: WindsurfRefreshState{}, } if input.LSInstanceID != "" { extra.LSBinding = WindsurfLSBinding{ ContainerID: input.LSInstanceID, } } extraMap := StoreWindsurfExtra(extra) name := input.Name if name == "" { if result.Name != "" { name = result.Name } else { name = input.Email } } concurrency := input.Concurrency if concurrency <= 0 { concurrency = 1 } createInput := &CreateAccountInput{ Name: name, Notes: input.Notes, Platform: domain.PlatformWindsurf, Type: domain.AccountTypeWindsurfSession, Credentials: credMap, Extra: extraMap, ProxyID: input.ProxyID, Concurrency: concurrency, Priority: input.Priority, GroupIDs: input.GroupIDs, } account, err := s.adminSvc.CreateAccount(ctx, createInput) if err != nil { return nil, fmt.Errorf("create account: %w", err) } return &WindsurfLoginOutput{ AccountID: account.ID, Email: input.Email, Tier: "unknown", AuthMethod: result.AuthMethod, APIKeyPresent: result.APIKey != "", RefreshTokenPresent: result.RefreshToken != "", }, nil } func (s *WindsurfAuthService) BatchLogin(ctx context.Context, items []string, proxyID *int64, groupIDs []int64, concurrency, priority int, probeAfter bool) ([]WindsurfBatchResult, error) { results := make([]WindsurfBatchResult, 0, len(items)) for _, item := range items { email, password, err := parseEmailPassword(item) if err != nil { results = append(results, WindsurfBatchResult{ Email: item, Success: false, Error: err.Error(), }) continue } input := &WindsurfLoginInput{ Email: email, Password: password, ProxyID: proxyID, GroupIDs: groupIDs, Concurrency: concurrency, Priority: priority, ProbeAfter: probeAfter, } output, loginErr := s.Login(ctx, input) if loginErr != nil { results = append(results, WindsurfBatchResult{ Email: email, Success: false, Error: loginErr.Error(), }) continue } results = append(results, WindsurfBatchResult{ Email: email, Success: true, AccountID: output.AccountID, Output: output, }) } return results, nil } type WindsurfBatchResult struct { Email string `json:"email"` Success bool `json:"success"` AccountID int64 `json:"account_id,omitempty"` Output *WindsurfLoginOutput `json:"output,omitempty"` Error string `json:"error,omitempty"` } func parseEmailPassword(item string) (string, string, error) { sep := "----" idx := -1 for i := 0; i <= len(item)-len(sep); i++ { if item[i:i+len(sep)] == sep { idx = i break } } if idx < 0 { return "", "", fmt.Errorf("invalid format: expected email----password") } email := item[:idx] password := item[idx+len(sep):] if email == "" || password == "" { return "", "", fmt.Errorf("email and password cannot be empty") } return email, password, nil } func (s *WindsurfAuthService) RefreshToken(ctx context.Context, accountID int64) error { account, err := s.accountRepo.GetByID(ctx, accountID) if err != nil { return err } if account.Platform != domain.PlatformWindsurf { return fmt.Errorf("account %d is not a windsurf account", accountID) } creds := LoadWindsurfCredentials(account.Credentials) proxyURL := "" if account.ProxyID != nil { proxy, err := s.proxyRepo.GetByID(ctx, *account.ProxyID) if err == nil { proxyURL = proxy.URL() } } if creds.AuthMethod == "firebase" && creds.RefreshToken != "" { refreshResult, err := s.authClient.RefreshFirebaseToken(ctx, creds.RefreshToken, proxyURL) if err != nil { return fmt.Errorf("firebase refresh: %w", err) } creds.IDToken = refreshResult.IDToken creds.RefreshToken = refreshResult.RefreshToken creds.ExpiresAt = time.Now().Add(time.Duration(refreshResult.ExpiresIn) * time.Second).Format(time.RFC3339) creds.LastRefreshAt = time.Now().Format(time.RFC3339) regResult, err := s.authClient.ReRegisterWithCodeium(ctx, refreshResult.IDToken, proxyURL) if err != nil { slog.Warn("windsurf_reregister_failed", "account_id", accountID, "error", err) } else { creds.APIKey = regResult.APIKey creds.LastReregisterAt = time.Now().Format(time.RFC3339) } } else if creds.AuthMethod == "auth1" { // Auth1 tokens don't use Firebase refresh; re-login would be needed return fmt.Errorf("auth1 accounts require re-login for token refresh") } else { return fmt.Errorf("unknown auth method: %s", creds.AuthMethod) } credMap := StoreWindsurfCredentials(creds) account.Credentials = credMap return s.accountRepo.Update(ctx, account) }