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 "sync"
14 "time"
15
16 "charm.land/catwalk/pkg/catwalk"
17)
18
19//go:generate wget -O provider.json https://hyper.charm.land/v1/provider
20
21//go:embed provider.json
22var embedded []byte
23
24// Embedded returns the embedded Hyper provider.
25var Embedded = sync.OnceValue(func() catwalk.Provider {
26 var provider catwalk.Provider
27 if err := json.Unmarshal(embedded, &provider); err != nil {
28 slog.Error("Could not use embedded provider data", "err", err)
29 }
30 if e := os.Getenv("HYPER_URL"); e != "" {
31 provider.APIEndpoint = e + "/api/v1/fantasy"
32 }
33 return provider
34})
35
36const (
37 // Name is the default name of this meta provider.
38 Name = "hyper"
39 // DisplayName is the display name of Hyper.
40 DisplayName = "Charm Hyper"
41 // defaultBaseURL is the default proxy URL.
42 defaultBaseURL = "https://hyper.charm.land"
43)
44
45// BaseURL returns the base URL, which is either $HYPER_URL or the default.
46var BaseURL = sync.OnceValue(func() string {
47 return cmp.Or(os.Getenv("HYPER_URL"), defaultBaseURL)
48})
49
50// FetchCredits calls the Hyper /v1/credits endpoint and returns the remaining
51// credits count.
52func FetchCredits(ctx context.Context, apiKey string) (int, error) {
53 req, err := http.NewRequestWithContext(
54 ctx,
55 http.MethodGet,
56 BaseURL()+"/v1/credits",
57 nil,
58 )
59 if err != nil {
60 return 0, fmt.Errorf("could not create request: %w", err)
61 }
62 req.Header.Set("Authorization", "Bearer "+apiKey)
63
64 client := &http.Client{Timeout: 10 * time.Second}
65 resp, err := client.Do(req)
66 if err != nil {
67 return 0, fmt.Errorf("failed to make request: %w", err)
68 }
69 defer resp.Body.Close() //nolint:errcheck
70
71 if resp.StatusCode != http.StatusOK {
72 return 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
73 }
74
75 var result struct {
76 Balance int `json:"balance"`
77 }
78 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
79 return 0, fmt.Errorf("failed to decode response: %w", err)
80 }
81
82 return result.Balance, nil
83}