package service import ( "errors" "fmt" "sort" "strings" "browser.local/platform/domain" "browser.local/platform/repo" "browser.local/platform/validator" ) func (svc *CoreService) GetServerRuntimeBindingForSession(sessionID, serverInstanceID string) (domain.RuntimeBindingView, error) { instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID) if err != nil { return domain.RuntimeBindingView{}, err } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return domain.RuntimeBindingView{}, err } binding, err := svc.runtimeBindingForServer(instance.ID) if errors.Is(err, repo.ErrNotFound) { return domain.RuntimeBindingView{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Status: domain.RuntimeBindingStatusIncomplete, Reason: "runtime profile is not configured"}, nil } if err != nil { return domain.RuntimeBindingView{}, err } return runtimeBindingView(plugin, binding) } func (svc *CoreService) UpdateServerRuntimeBindingForSession(sessionID, serverInstanceID string, update domain.RuntimeBindingUpdate) (domain.RuntimeBindingView, error) { _, instance, err := svc.requireServerOwner(sessionID, serverInstanceID) if err != nil { return domain.RuntimeBindingView{}, err } existing, existingErr := svc.runtimeBindingForServer(instance.ID) if existingErr != nil && !errors.Is(existingErr, repo.ErrNotFound) { return domain.RuntimeBindingView{}, existingErr } if (instance.State == domain.ServerInstanceStateInstalling || instance.State == domain.ServerInstanceStateRunning) && existingErr == nil || instance.State == domain.ServerInstanceStateDeleted { return domain.RuntimeBindingView{}, validationError("runtime binding cannot be changed while the server is active") } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return domain.RuntimeBindingView{}, err } binding, err := svc.buildRuntimeBinding(instance, plugin, update, false) if err != nil { return domain.RuntimeBindingView{}, err } if existingErr == nil { binding.CreatedAt = existing.CreatedAt if existing.ProfileKey == binding.ProfileKey { merged := domain.CopyStringMap(existing.Bindings) for key, value := range update.Bindings { if strings.TrimSpace(value) == "" { delete(merged, key) } else { merged[key] = value } } binding, err = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: update.ProfileKey, Bindings: merged}, false) if err != nil { return domain.RuntimeBindingView{}, err } binding.CreatedAt = existing.CreatedAt } if err := svc.store.RuntimeBindings().Update(binding); err != nil { return domain.RuntimeBindingView{}, err } } else if errors.Is(existingErr, repo.ErrNotFound) { if err := svc.store.RuntimeBindings().Create(binding); err != nil { return domain.RuntimeBindingView{}, err } } return runtimeBindingView(plugin, binding) } func (svc *CoreService) buildRuntimeBinding(instance domain.ServerInstance, plugin domain.GamePlugin, update domain.RuntimeBindingUpdate, requireComplete bool) (domain.RuntimeBinding, error) { update = domain.CopyRuntimeBindingUpdate(update) profile, ok := runtimeLifecycleProfile(plugin.RuntimeProfiles, update.ProfileKey) if !ok { return domain.RuntimeBinding{}, validationError("profileKey must reference a declared lifecycle profile") } stamp := svc.now() binding := domain.RuntimeBinding{ID: "runtime-binding-" + instance.ID, ServerInstanceID: instance.ID, PluginID: plugin.ID, PluginVersion: plugin.Version, ProfileKey: profile.Key, Mode: profile.Mode, Bindings: update.Bindings, CreatedAt: stamp, UpdatedAt: stamp} binding, err := normalizeRuntimeBinding(plugin, binding) if err != nil { return domain.RuntimeBinding{}, err } if requireComplete && binding.Status != domain.RuntimeBindingStatusComplete { return domain.RuntimeBinding{}, validationError("missing runtime bindings: " + strings.Join(binding.MissingKeys, ", ")) } return binding, nil } func runtimeLifecycleProfile(profiles domain.GamePluginRuntimeProfiles, key string) (domain.RuntimeLifecycleProfile, bool) { for _, profile := range profiles.LifecycleProfiles { if profile.Key == key { return profile, true } } return domain.RuntimeLifecycleProfile{}, false } func runtimeBindingKeys(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile) ([]string, map[string]struct{}) { requiredSet := map[string]struct{}{} allowed := map[string]struct{}{} add := func(key string, required bool) { if key == "" { return } allowed[key] = struct{}{} if required { requiredSet[key] = struct{}{} } } for _, probe := range profiles.Discovery { add(probe.TargetKey, probe.Required) } for _, probe := range profiles.DependencyProbes { add(probe.TargetKey, probe.Required) } for _, source := range profiles.LogSources { add(source.TargetKey, source.TargetKey != "") } for _, plan := range profiles.InstallPlans { for _, step := range plan.Steps { add(step.TargetKey, false) } } for _, transport := range profiles.TransportProfiles { if containsString(profile.TransportKeys, transport.Key) { if transport.TargetKey != "" { add(transport.TargetKey, true) } else { add(transport.Key, true) } } } if profile.ClientManagerRef != "" { add(profile.ClientManagerRef, true) } required := make([]string, 0, len(requiredSet)) for key := range requiredSet { required = append(required, key) } sort.Strings(required) return required, allowed } func normalizeRuntimeBinding(plugin domain.GamePlugin, binding domain.RuntimeBinding) (domain.RuntimeBinding, error) { profile, _ := runtimeLifecycleProfile(plugin.RuntimeProfiles, binding.ProfileKey) if profile.Key == "" { return domain.RuntimeBinding{}, validationError("runtime profile is no longer declared") } required, allowed := runtimeBindingKeys(plugin.RuntimeProfiles, profile) for key := range binding.Bindings { if _, ok := allowed[key]; !ok { return domain.RuntimeBinding{}, validationError(fmt.Sprintf("bindings.%s is not declared by runtime profile", key)) } } missing := make([]string, 0) for _, key := range required { if strings.TrimSpace(binding.Bindings[key]) == "" { missing = append(missing, key) } } binding.Mode = profile.Mode binding.MissingKeys = missing binding.Status = domain.RuntimeBindingStatusComplete if len(missing) > 0 { binding.Status = domain.RuntimeBindingStatusIncomplete } if err := validator.ValidateRuntimeBinding(binding); err != nil { return domain.RuntimeBinding{}, err } return binding, nil } func runtimeBindingView(plugin domain.GamePlugin, binding domain.RuntimeBinding) (domain.RuntimeBindingView, error) { binding, err := normalizeRuntimeBinding(plugin, binding) if err != nil { return domain.RuntimeBindingView{}, err } profile, _ := runtimeLifecycleProfile(plugin.RuntimeProfiles, binding.ProfileKey) required, allowed := runtimeBindingKeys(plugin.RuntimeProfiles, profile) requiredSet := map[string]struct{}{} for _, key := range required { requiredSet[key] = struct{}{} } keys := make([]string, 0, len(allowed)) for key := range allowed { keys = append(keys, key) } sort.Strings(keys) items := make([]domain.RuntimeBindingKeyView, 0, len(keys)) for _, key := range keys { value := strings.TrimSpace(binding.Bindings[key]) _, isRequired := requiredSet[key] items = append(items, domain.RuntimeBindingKeyView{Key: key, Required: isRequired, Configured: value != "", Secret: strings.HasPrefix(value, "secret://")}) } reason := "" if binding.Status != domain.RuntimeBindingStatusComplete { reason = "required logical bindings are missing" } return domain.RuntimeBindingView{ServerInstanceID: binding.ServerInstanceID, PluginID: binding.PluginID, ProfileKey: binding.ProfileKey, Mode: binding.Mode, Configured: true, Keys: items, MissingKeys: domain.CopyStringSlice(binding.MissingKeys), Status: binding.Status, Reason: reason, CreatedAt: binding.CreatedAt, UpdatedAt: binding.UpdatedAt}, nil } func (svc *CoreService) runtimeBindingForServer(serverInstanceID string) (domain.RuntimeBinding, error) { bindings, err := svc.store.RuntimeBindings().List(domain.RuntimeBindingFilter{ServerInstanceID: serverInstanceID}) if err != nil { return domain.RuntimeBinding{}, err } if len(bindings) == 0 { return domain.RuntimeBinding{}, repo.ErrNotFound } if len(bindings) > 1 { return domain.RuntimeBinding{}, validationError("server has multiple runtime bindings") } return bindings[0], nil }