Files
t5de-proxy/internal/control/server.go
T
dhkatzandClaude Opus 5.5 8ae45fc07a
CI / build (push) Successful in 18s
Choose the hub: Settings' choice, else the install's channel's; state kept per hub
The client's Settings > T5DE chooses the hub from the next start on (POST /hub/server, kept in server.json);
without a choice, -channel (which the client passes) decides: staging for nightly and dev, T5DE's hub for
releases. $T5DE_HUB still wins. Each hub's sign-in, product IDs and products are kept in their own folder
(T5DE's hub in the data folder as before, others in hubs/<host>), so a hub never gets another's token and
product IDs from two hubs never clash; a sign-in saved for another hub is ignored. Signing in always goes to
this run's hub.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 09:09:34 -08:00

115 lines
2.9 KiB
Go

package control
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
)
// T5DE's hubs.
const (
Production = "https://hub.t5de.dev"
Staging = "https://hub.staging.t5de.dev"
)
// ChannelDefault is the hub an install uses unless the person chooses one: nightlies and builds outside an
// install (channel "nightly", "dev") use staging, releases T5DE's hub.
func ChannelDefault(channel string) string {
if channel == "nightly" || channel == "dev" {
return Staging
}
return Production
}
// Server is the hub for this run: env ($T5DE_HUB) if set, else the person's choice (the client's Settings, kept
// in choicePath), else the channel's.
func Server(choicePath, channel, env string) string {
if env != "" {
return strings.TrimSuffix(env, "/")
}
if c := LoadChoice(choicePath); c != "" {
return c
}
return ChannelDefault(channel)
}
// LoadChoice is the hub the person chose, "" for the channel's.
func LoadChoice(path string) string {
b, err := os.ReadFile(path)
if err != nil {
return ""
}
var c struct {
Hub string `json:"hub"`
}
if json.Unmarshal(b, &c) != nil {
return ""
}
hub, err := CheckHub(c.Hub)
if err != nil {
return ""
}
return hub
}
// SaveChoice remembers the hub the person chose ("" for the channel's).
func SaveChoice(path, hub string) error {
if hub == "" {
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
b, _ := json.Marshal(map[string]string{"hub": hub})
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return err
}
tmp := path + ".tmp"
if err := os.WriteFile(tmp, b, 0o600); err != nil {
return err
}
return os.Rename(tmp, path)
}
// CheckHub is a hub's address without a trailing slash, if it's https (or this computer's, for development).
func CheckHub(s string) (string, error) {
s = strings.TrimSuffix(strings.TrimSpace(s), "/")
u, err := url.Parse(s)
if err != nil || u.Host == "" || u.RawQuery != "" || u.Fragment != "" {
return "", fmt.Errorf("not a hub address: %q", s)
}
local := u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1"
if u.Scheme != "https" && !(u.Scheme == "http" && local) {
return "", errors.New("the hub needs an https address")
}
return s, nil
}
// StateDir is where the proxy keeps what belongs to a hub (its sign-in, product IDs, products): the data folder
// itself for T5DE's hub, hubs/<name> in it for any other, so a hub never gets another's sign-in and product IDs
// from two hubs never clash.
func StateDir(data, hub string) string {
if hub == Production {
return data
}
u, err := url.Parse(hub)
name := hub
if err == nil {
name = u.Host + u.Path
}
name = strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '.' || r == '-' {
return r
}
if r >= 'A' && r <= 'Z' {
return r + 'a' - 'A'
}
return '_'
}, name)
return filepath.Join(data, "hubs", name)
}