package service import ( "context" "fmt" "net/http" "sync" "time" claude "github.com/Wei-Shaw/sub2api/internal/pkg/claude" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" ) // bootstrapPreflight simulates the real Claude Code CLI's startup bootstrap call. // Real CLI calls GET /api/claude_cli/bootstrap with OAuth token before first v1/messages. // This creates the expected behavioral correlation on Anthropic's backend. type bootstrapPreflight struct { mu sync.Mutex called map[int64]time.Time // accountID → last bootstrap time client *http.Client baseURL string } var globalBootstrapPreflight = &bootstrapPreflight{ called: make(map[int64]time.Time), client: &http.Client{Timeout: 5 * time.Second}, } // SetBootstrapBaseURL configures the API base URL for bootstrap calls. func SetBootstrapBaseURL(baseURL string) { globalBootstrapPreflight.baseURL = baseURL } // TriggerBootstrapIfNeeded fires a non-blocking bootstrap preflight call // for the given OAuth account if it hasn't been called recently (1 hour cooldown). // This matches the real CLI behavior: `void fetchBootstrapData()` fires // as fire-and-forget before the first v1/messages call. func TriggerBootstrapIfNeeded(accountID int64, accessToken string) { bp := globalBootstrapPreflight bp.mu.Lock() lastCall, exists := bp.called[accountID] if exists && time.Since(lastCall) < 1*time.Hour { bp.mu.Unlock() return } bp.called[accountID] = time.Now() bp.mu.Unlock() // Fire-and-forget, matching real CLI's `void fetchBootstrapData()` go bp.doBootstrap(accessToken) } func (bp *bootstrapPreflight) doBootstrap(accessToken string) { baseURL := bp.baseURL if baseURL == "" { baseURL = "https://api.anthropic.com" } endpoint := baseURL + "/api/claude_cli/bootstrap" ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) if err != nil { logger.LegacyPrintf("service.bootstrap", "Failed to create bootstrap request: %v", err) return } // Headers match real CLI's bootstrap call exactly: // Source: extracted/src/services/api/bootstrap.ts:85-91 req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", fmt.Sprintf("claude-code/%s", claude.DefaultCLIVersion)) req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("anthropic-beta", claude.BetaOAuth) resp, err := bp.client.Do(req) if err != nil { logger.LegacyPrintf("service.bootstrap", "Bootstrap preflight failed: %v", err) return } defer resp.Body.Close() // Drain body — we don't need the response, just the side-effect of the call existing // in Anthropic's access logs correlated with this token. resp.Body.Close() logger.LegacyPrintf("service.bootstrap", "Bootstrap preflight completed: status=%d", resp.StatusCode) }