package service import ( "fmt" "regexp" "strings" "time" "browser.local/platform/domain" "browser.local/platform/validator" ) const scumPluginID = "game.scum" func applyPluginCreateDefaults(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) domain.ServerDeploymentDefinition { definition = domain.CopyServerDeploymentDefinition(definition) if definition.Mode != domain.ServerDeploymentModeGuided { return definition } if definition.CreateInputs == nil { definition.CreateInputs = map[string]string{} } for _, field := range plugin.CreateFields { if _, present := definition.CreateInputs[field.Key]; !present && field.DefaultValue != "" { definition.CreateInputs[field.Key] = field.DefaultValue } } return definition } func scumDeploymentPlan(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition, operation string) (*domain.ServerDeploymentPlan, error) { if plugin.ID != scumPluginID || (definition.Mode != domain.ServerDeploymentModeGuided && definition.Mode != domain.ServerDeploymentModeExisting) { return nil, nil } if operation != "install" && operation != "adopt" { return nil, validationError("SCUM deployment operation is invalid") } if len(plugin.RuntimeProfiles.ServerDeployments) == 0 { return nil, validationError("SCUM deployment template is not registered") } profile := plugin.RuntimeProfiles.ServerDeployments[0] if strings.TrimSpace(profile.Key) == "" || strings.TrimSpace(profile.Version) == "" || profile.SteamAppID == "" { return nil, validationError("SCUM deployment template is incomplete") } expectedPrerequisites := map[string]string{"steamcmd": "steamcmd", "vcredist-2012-x86": "windows-vcredist", "vcredist-2012-x64": "windows-vcredist", "vcredist-2013-x86": "windows-vcredist", "vcredist-2013-x64": "windows-vcredist", "vcredist-2015-2022-x86": "windows-vcredist", "vcredist-2015-2022-x64": "windows-vcredist", "directx-jun2010": "windows-directx"} if len(profile.Prerequisites) != len(expectedPrerequisites) { return nil, validationError("SCUM deployment template prerequisite list is incomplete") } for _, prerequisite := range profile.Prerequisites { if expectedPrerequisites[prerequisite.Key] != prerequisite.Kind { return nil, validationError("SCUM deployment template prerequisite is invalid") } } fieldKeys := make(map[string]struct{}, len(plugin.CreateFields)) for _, field := range plugin.CreateFields { fieldKeys[field.Key] = struct{}{} } for _, mapping := range profile.ConfigMappings { if _, ok := fieldKeys[mapping.FieldKey]; !ok { return nil, validationError("SCUM deployment template maps an undeclared create field") } if strings.TrimSpace(mapping.ConfigKey) == "" || strings.TrimSpace(mapping.ValueType) == "" { return nil, validationError("SCUM deployment template contains an incomplete config mapping") } } for _, check := range profile.VerificationChecks { if check.Required && (strings.TrimSpace(check.Key) == "" || strings.TrimSpace(check.Kind) == "") { return nil, validationError("SCUM deployment template contains an incomplete verification check") } } return &domain.ServerDeploymentPlan{ SchemaVersion: "1", Operation: operation, PluginID: plugin.ID, TemplateKey: profile.Key, TemplateVersion: profile.Version, SteamAppID: profile.SteamAppID, ExecutableKey: profile.ExecutableKey, InstallRootKey: profile.InstallRootKey, ConfigKey: profile.ConfigKey, ConfigFormat: profile.ConfigFormat, Prerequisites: append([]domain.RuntimeServerPrerequisite(nil), profile.Prerequisites...), ConfigMappings: append([]domain.RuntimeServerConfigMapping(nil), profile.ConfigMappings...), DiscoveryMarkers: append([]domain.RuntimeServerDiscoveryMarker(nil), profile.DiscoveryMarkers...), VerificationChecks: append([]domain.RuntimeServerVerificationCheck(nil), profile.VerificationChecks...), }, nil } func scumDeploymentProjection(plan *domain.ServerDeploymentPlan, operation string, stamp time.Time) domain.ServerDeploymentProjection { projection := domain.ServerDeploymentProjection{State: "draft", Operation: operation, UpdatedAt: stamp} if plan != nil { projection.TemplateKey = plan.TemplateKey projection.TemplateVersion = plan.TemplateVersion } return projection } func scumDeploymentCapabilityRequired(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) bool { return plugin.ID == scumPluginID && (definition.Mode == domain.ServerDeploymentModeGuided || definition.Mode == domain.ServerDeploymentModeExisting) } func deploymentNeedsCompleteRuntimeBinding(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) bool { return !scumDeploymentCapabilityRequired(plugin, definition) } func validateSCUMDeploymentTarget(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition, endpoint domain.RunEndpoint) error { if !scumDeploymentCapabilityRequired(plugin, definition) { return nil } plan, err := scumDeploymentPlan(plugin, definition, "install") if err != nil { return err } for _, profile := range plugin.RuntimeProfiles.ServerDeployments { if profile.Key != plan.TemplateKey { continue } for _, target := range profile.SupportedTargets { if target.OS == endpoint.Platform && target.Arch == endpoint.Architecture { return nil } } } return validationError("SCUM deployment template is incompatible with the selected Run target") } func validateScumDeploymentInputs(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) error { plan, err := scumDeploymentPlan(plugin, definition, "install") if err != nil || plan == nil { return err } if definition.Mode == domain.ServerDeploymentModeExisting { if strings.TrimSpace(definition.ServerRoot) == "" { return validationError("SCUM adoption requires an existing server directory") } return nil } if strings.TrimSpace(definition.ServerRoot) == "" { return validationError("SCUM installation requires an install directory") } for _, mapping := range plan.ConfigMappings { if mapping.Required && strings.TrimSpace(definition.CreateInputs[mapping.FieldKey]) == "" { field, found := findPluginCreateField(plugin.CreateFields, mapping.FieldKey) if !found || field.DefaultValue == "" { return validator.ValidationError{Violations: []string{"createInputs." + mapping.FieldKey + " is required by the SCUM deployment template"}} } } } return nil } func findPluginCreateField(fields []domain.PluginCreateField, key string) (domain.PluginCreateField, bool) { for _, field := range fields { if field.Key == key { return field, true } } return domain.PluginCreateField{}, false } func validateSCUMDeploymentEvidence(plan *domain.ServerDeploymentPlan, result domain.RunJobResult) error { if plan == nil { return nil } evidence := result.ExecutionResult.ServerDeploymentEvidence if evidence == nil { return validationError("SCUM deployment result must include deployment evidence") } if evidence.TemplateKey != plan.TemplateKey || evidence.TemplateVersion != plan.TemplateVersion { return validationError("SCUM deployment result template fence is invalid") } for name, values := range map[string]map[string]string{"discoveredFacts": evidence.DiscoveredFacts, "mappingResults": evidence.MappingResults, "verificationResults": evidence.VerificationResults} { if len(values) > 64 { return validationError("SCUM deployment evidence contains too many " + name) } for key, value := range values { if !regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]{0,119}$`).MatchString(key) || len(value) > 160 || strings.TrimSpace(value) != value || unsafeSCUMEvidenceValue(value) { return validationError(fmt.Sprintf("SCUM deployment evidence contains an unsafe %s value", name)) } } } if len(evidence.FailureCode) > 80 || (evidence.FailureCode != "" && !regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,79}$`).MatchString(evidence.FailureCode)) { return validationError("SCUM deployment failure code is invalid") } if result.State == domain.JobStateFailed { if result.ExecutionResult.Kind != "scum.install.failed" && result.ExecutionResult.Kind != "scum.adopt.failed" { return validationError("SCUM deployment failure result type is invalid") } if strings.TrimSpace(evidence.FailureCode) == "" { return validationError("SCUM deployment failure must include a stable failure code") } return nil } if result.State != domain.JobStateSucceeded { return nil } expectedKind := "scum.install.completed" if plan.Operation == "adopt" { expectedKind = "scum.adopt.completed" } if result.ExecutionResult.Kind != expectedKind { return validationError("SCUM deployment result type is invalid") } if evidence.PreflightState != "passed" || evidence.DiscoveryState != "passed" || evidence.VerificationState != "passed" { return validationError("SCUM deployment result is missing successful preflight, discovery, or verification evidence") } validMappingState := evidence.MappingState == "passed" || evidence.MappingState == "applied" || evidence.MappingState == "unchanged" if plan.Operation == "adopt" && evidence.MappingState == "skipped" { validMappingState = true } if !validMappingState { return validationError("SCUM deployment result is missing a successful config mapping state") } for _, mapping := range plan.ConfigMappings { mappingResult := evidence.MappingResults[mapping.FieldKey] if plan.Operation == "adopt" && mappingResult == "" { mappingResult = "skipped" } if mapping.Required && mappingResult != "applied" && mappingResult != "unchanged" && !(plan.Operation == "adopt" && mappingResult == "skipped") { return validationError("SCUM deployment result is missing a required config mapping") } } for _, check := range plan.VerificationChecks { if check.Required && evidence.VerificationResults[check.Key] != "passed" { return validationError("SCUM deployment result is missing a required verification check") } } return nil } func unsafeSCUMEvidenceValue(value string) bool { lower := strings.ToLower(value) for _, token := range []string{"password", "secret", "credential", "token", "private key", "powershell", "cmd.exe", "bash -c", "ssh://", "tcp://", "udp://"} { if strings.Contains(lower, token) { return true } } return strings.HasPrefix(value, "/") || strings.HasPrefix(value, `\\`) || regexp.MustCompile(`^[A-Za-z]:[\\/]`).MatchString(value) }