package api import ( "encoding/json" "fmt" "net/url" "os" "path/filepath" "strings" ) // RewriteServerInfoAddressables rewrites AddressablesCatalogUrlRoot values so // clients fetch resources from publicBaseURL while leaving business API URLs // untouched. func RewriteServerInfoAddressables(raw []byte, publicBaseURL string) ([]byte, error) { var doc map[string]any if err := json.Unmarshal(raw, &doc); err != nil { return nil, fmt.Errorf("parse server-info: %w", err) } groups, _ := doc["ConnectionGroups"].([]any) for _, g := range groups { group, ok := g.(map[string]any) if !ok { continue } rewriteGroupAddressables(group, publicBaseURL) if overrides, ok := group["OverrideConnectionGroups"].([]any); ok { for _, o := range overrides { if og, ok := o.(map[string]any); ok { rewriteGroupAddressables(og, publicBaseURL) } } } } return json.Marshal(doc) } func rewriteGroupAddressables(group map[string]any, publicBaseURL string) { raw, _ := group["AddressablesCatalogUrlRoot"].(string) if raw == "" { return } if rewritten, ok := rewriteAddressablesRoot(raw, publicBaseURL); ok { group["AddressablesCatalogUrlRoot"] = rewritten } } func rewriteAddressablesRoot(original, publicBaseURL string) (string, bool) { original = strings.TrimRight(original, "/") publicBaseURL = strings.TrimRight(publicBaseURL, "/") // Already rewritten to this base. if strings.HasPrefix(original, publicBaseURL+"/"+ClientPatchHost) || strings.HasPrefix(original, publicBaseURL+"/"+ServerInfoHost) { return original, true } u, err := url.Parse(original) if err != nil || u.Host == "" { return "", false } // Keep host/path so CDN path mapping stays 1:1 with destination_for_url. path := strings.TrimPrefix(u.Path, "/") if path == "" { return publicBaseURL + "/" + u.Host, true } return publicBaseURL + "/" + u.Host + "/" + path, true } // LoadServerInfoBytes loads server-info from an explicit file, or from the // release tree under yostar-serverinfo host, or synthesizes a minimal document // from the release snapshot addressables root. func LoadServerInfoBytes(cfg Config, idx *ReleaseIndex, requestName string) ([]byte, error) { if cfg.ServerInfoFile != "" { return os.ReadFile(cfg.ServerInfoFile) } if idx != nil && idx.ResourceRoot != "" { name := requestName if name == "" { name = "server-info.json" } // Official path layout. candidates := []string{ filepath.Join(idx.ResourceRoot, ServerInfoHost, name), filepath.Join(idx.ResourceRoot, ServerInfoHost, requestName), } for _, c := range candidates { if c == "" { continue } if data, err := os.ReadFile(c); err == nil { return data, nil } } // Any single json under server-info host. dir := filepath.Join(idx.ResourceRoot, ServerInfoHost) if entries, err := os.ReadDir(dir); err == nil { for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { continue } return os.ReadFile(filepath.Join(dir, e.Name())) } } } if idx != nil && idx.Snapshot != nil && idx.Snapshot.AddressablesRoot != "" { doc := map[string]any{ "ConnectionGroups": []any{ map[string]any{ "Name": defaultString(idx.Snapshot.ConnectionGroupName, "Prod"), "AddressablesCatalogUrlRoot": idx.Snapshot.AddressablesRoot, "BundleVersion": idx.Snapshot.BundleVersion, "IsLivePublished": true, }, }, } return json.Marshal(doc) } return nil, fmt.Errorf("server-info source not found") } func defaultString(v, fallback string) string { if v == "" { return fallback } return v }