provider.go

 1// Package hyper provides a fantasy.Provider that proxies requests to Hyper.
 2package hyper
 3
 4import (
 5	"cmp"
 6	"context"
 7	_ "embed"
 8	"encoding/json"
 9	"fmt"
10	"log/slog"
11	"net/http"
12	"os"
13	"strconv"
14	"sync"
15	"time"
16
17	"charm.land/catwalk/pkg/catwalk"
18)
19
20//go:generate wget -O provider.json https://hyper.charm.land/v1/provider
21
22//go:embed provider.json
23var embedded []byte
24
25// Enabled returns true if hyper is enabled.
26var Enabled = sync.OnceValue(func() bool {
27	b, _ := strconv.ParseBool(
28		cmp.Or(
29			os.Getenv("HYPER"),
30			os.Getenv("HYPERCRUSH"),
31			os.Getenv("HYPER_ENABLE"),
32			os.Getenv("HYPER_ENABLED"),
33		),
34	)
35	return b
36})
37
38// Embedded returns the embedded Hyper provider.
39var Embedded = sync.OnceValue(func() catwalk.Provider {
40	var provider catwalk.Provider
41	if err := json.Unmarshal(embedded, &provider); err != nil {
42		slog.Error("Could not use embedded provider data", "err", err)
43	}
44	if e := os.Getenv("HYPER_URL"); e != "" {
45		provider.APIEndpoint = e + "/api/v1/fantasy"
46	}
47	return provider
48})
49
50const (
51	// Name is the default name of this meta provider.
52	Name = "hyper"
53	// DisplayName is the display name of Hyper.
54	DisplayName = "Charm Hyper"
55	// defaultBaseURL is the default proxy URL.
56	defaultBaseURL = "https://hyper.charm.land"
57)
58
59// BaseURL returns the base URL, which is either $HYPER_URL or the default.
60var BaseURL = sync.OnceValue(func() string {
61	return cmp.Or(os.Getenv("HYPER_URL"), defaultBaseURL)
62})
63
64// FetchCredits calls the Hyper /v1/credits endpoint and returns the remaining
65// credits count.
66func FetchCredits(ctx context.Context, apiKey string) (int, error) {
67	req, err := http.NewRequestWithContext(
68		ctx,
69		http.MethodGet,
70		BaseURL()+"/v1/credits",
71		nil,
72	)
73	if err != nil {
74		return 0, fmt.Errorf("could not create request: %w", err)
75	}
76	req.Header.Set("Authorization", "Bearer "+apiKey)
77
78	client := &http.Client{Timeout: 10 * time.Second}
79	resp, err := client.Do(req)
80	if err != nil {
81		return 0, fmt.Errorf("failed to make request: %w", err)
82	}
83	defer resp.Body.Close() //nolint:errcheck
84
85	if resp.StatusCode != http.StatusOK {
86		return 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
87	}
88
89	var result struct {
90		Balance int `json:"balance"`
91	}
92	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
93		return 0, fmt.Errorf("failed to decode response: %w", err)
94	}
95
96	return result.Balance, nil
97}