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	"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}