package browser import ( "context" "fmt" "os" "path/filepath" "time" "github.com/chromedp/chromedp" "git.davoryn.de/calic/claude-statusline/internal/config" ) // FetchViaChrome navigates to a URL using headless Chrome with the persistent // browser profile (which has Cloudflare clearance cookies) and returns the // response body. This bypasses Cloudflare JS challenges because Chrome runs // real JavaScript. Falls back to non-headless if headless fails. func FetchViaChrome(url string) ([]byte, error) { profileDir := filepath.Join(config.ConfigDir(), "browser-profile") if err := os.MkdirAll(profileDir, 0o755); err != nil { return nil, fmt.Errorf("create browser profile dir: %w", err) } // Remove stale lock file from unclean shutdown _ = os.Remove(filepath.Join(profileDir, "SingletonLock")) execPath := findBrowserExec() opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.Flag("headless", true), chromedp.UserDataDir(profileDir), ) if execPath != "" { opts = append(opts, chromedp.ExecPath(execPath)) } allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...) defer allocCancel() ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() // Set a total timeout for the operation ctx, timeoutCancel := context.WithTimeout(ctx, 30*time.Second) defer timeoutCancel() var body string err := chromedp.Run(ctx, chromedp.Navigate(url), // Wait for the body to have content (Cloudflare challenge resolves via JS) chromedp.WaitReady("body"), // Chrome renders JSON API responses inside a
 tag
		chromedp.Text("pre", &body, chromedp.ByQuery, chromedp.NodeVisible),
	)
	if err != nil {
		// Fallback: try extracting from body directly (some responses may not use 
)
		var bodyFallback string
		errFb := chromedp.Run(ctx,
			chromedp.Text("body", &bodyFallback, chromedp.ByQuery),
		)
		if errFb == nil && bodyFallback != "" {
			return []byte(bodyFallback), nil
		}
		return nil, fmt.Errorf("chromedp fetch: %w", err)
	}

	if body == "" {
		return nil, fmt.Errorf("chromedp fetch: empty response body")
	}

	// Gracefully close to flush cookies (including refreshed cf_clearance)
	cancel()

	return []byte(body), nil
}