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}