package validator import ( "fmt" "regexp" "strconv" "strings" "browser.local/platform/domain" ) const ( maxSummaryLength = 512 maxContactNoteLength = 160 maxMarketplaceKeywordSize = 80 maxMarketplaceListSize = 500 maxPluginDescriptionLength = 240 maxPluginPageTitleLength = 40 maxPluginBridgePayloadKeys = 16 maxPluginBridgePayloadSize = 4096 maxProgressMessageLength = 256 maxServerConfigContentSize = 64 * 1024 maxJobExecutionContentSize = 64 * 1024 maxLogicalFileKeyLength = 160 maxProductionMessageLength = 320 ) var ( gameClientBridgeCollectionPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,119}$`) gameClientBridgeFieldPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,79}$`) gameClientBridgeCaptureNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{0,79}$`) ) type ValidationError struct { Violations []string } func (err ValidationError) Error() string { return "validation failed: " + strings.Join(err.Violations, "; ") } func (err ValidationError) IsEmpty() bool { return len(err.Violations) == 0 } func appendRequired(violations []string, field string, value string) []string { if strings.TrimSpace(value) == "" { return append(violations, field+" is required") } return violations } func finish(violations []string) error { if len(violations) == 0 { return nil } return ValidationError{Violations: violations} } func ValidateUser(user domain.User) error { var violations []string violations = appendRequired(violations, "id", user.ID) violations = appendRequired(violations, "displayName", user.DisplayName) if !validUserStatus(user.Status) { violations = append(violations, "status is invalid") } if strings.TrimSpace(user.Email) != "" && !strings.Contains(user.Email, "@") { violations = append(violations, "email is invalid") } if len(user.Profile.ContactNote) > maxContactNoteLength { violations = append(violations, "profile.contactNote is too long") } for i, role := range user.Roles { if strings.TrimSpace(role) == "" { violations = append(violations, fmt.Sprintf("roles[%d] is required", i)) } if !validUserRole(role) { violations = append(violations, fmt.Sprintf("roles[%d] is invalid", i)) } } return finish(violations) } func ValidateAIProvider(provider domain.AIProvider) error { var violations []string violations = appendRequired(violations, "id", provider.ID) violations = appendRequired(violations, "name", provider.Name) violations = appendRequired(violations, "baseUrl", provider.BaseURL) if !validAIProviderKind(provider.Kind) { violations = append(violations, "kind is invalid") } if !validAIRelayMode(provider.RelayMode) { violations = append(violations, "relayMode is invalid") } if !validAIProviderStatus(provider.Status) { violations = append(violations, "status is invalid") } if provider.RelayMode == domain.AIRelayModeDirect || provider.RelayMode == domain.AIRelayModeRelay { violations = appendRequired(violations, "apiKeyRef", provider.APIKeyRef) } if looksLikeRawSecret(provider.APIKeyRef) { violations = append(violations, "apiKeyRef must reference secret storage, not raw key material") } violations = appendRequired(violations, "redactionPolicy", provider.RedactionPolicy) if provider.TimeoutMS <= 0 { violations = append(violations, "timeoutMs must be positive") } if len(provider.Models) == 0 { violations = append(violations, "models must not be empty") } modelSet := map[string]struct{}{} for i, model := range provider.Models { model = strings.TrimSpace(model) if model == "" { violations = append(violations, fmt.Sprintf("models[%d] is required", i)) continue } if _, exists := modelSet[model]; exists { violations = append(violations, fmt.Sprintf("models[%d] duplicates %q", i, model)) } modelSet[model] = struct{}{} } if provider.DefaultModel != "" { if _, exists := modelSet[provider.DefaultModel]; !exists { violations = append(violations, "defaultModel must be included in models") } } return finish(violations) } func ValidateGamePlugin(plugin domain.GamePlugin) error { var violations []string violations = appendRequired(violations, "id", plugin.ID) violations = appendRequired(violations, "name", plugin.Name) if len(plugin.Description) > maxPluginDescriptionLength { violations = append(violations, "description is too long") } violations = appendRequired(violations, "version", plugin.Version) violations = appendRequired(violations, "serverType", plugin.ServerType) violations = appendRequired(violations, "manifestRef", plugin.ManifestRef) violations = appendRequired(violations, "createFormSchemaRef", plugin.CreateFormSchemaRef) if !validGamePluginStatus(plugin.Status) { violations = append(violations, "status is invalid") } for i, capability := range plugin.RequiredRunCapabilities { if strings.TrimSpace(capability) == "" { violations = append(violations, fmt.Sprintf("requiredRunCapabilities[%d] is required", i)) } else if !validPluginRunCapability(capability) { violations = append(violations, fmt.Sprintf("requiredRunCapabilities[%d] is not allowed", i)) } } violations = append(violations, duplicateViolations("requiredRunCapabilities", plugin.RequiredRunCapabilities)...) violations = append(violations, validateDeclaredPluginPermissions(plugin.DeclaredPermissions)...) violations = append(violations, validateBridgeActions("bridgeActions", plugin.BridgeActions)...) violations = append(violations, validatePluginPages(plugin.Pages)...) violations = append(violations, validatePluginFileWorkspace("fileWorkspace", plugin.FileWorkspace)...) violations = append(violations, duplicateViolations("tags", plugin.Tags)...) violations = append(violations, validateAIPurposes(plugin.AIPurposes)...) violations = append(violations, validateProductionLifecycle("productionLifecycle", plugin.ProductionLifecycle, true)...) violations = append(violations, validateRemoteAccess("remoteAccess", plugin.RemoteAccess, plugin.RequiredRunCapabilities)...) if err := ValidateGamePluginRuntimeProfiles(plugin.RuntimeProfiles); err != nil { violations = append(violations, err.Error()) } violations = append(violations, validateRuntimeProfileCapabilityDeclarations(plugin.RuntimeProfiles, plugin.RequiredRunCapabilities)...) violations = append(violations, validateRuntimeLogEventPermissionDeclarations("runtimeProfiles.logEvents", plugin.RuntimeProfiles, plugin.DeclaredPermissions)...) violations = append(violations, validateGameClientBridgeManifest("gameClientBridge", plugin.GameClientBridge, plugin.DeclaredPermissions, plugin.Pages, plugin.RuntimeProfiles)...) violations = append(violations, validatePluginCreateFields("createFields", plugin.CreateFields)...) violations = append(violations, validatePluginAssetFiles("lifecycleAssets", plugin.LifecycleAssets)...) violations = append(violations, validateSafePluginStrings("gamePlugin", pluginSafeStrings(plugin))...) return finish(violations) } func ValidateGamePluginManifestRegistration(registration domain.GamePluginManifestRegistration) error { registration = domain.CopyGamePluginManifestRegistration(registration) manifest := registration.Manifest var violations []string violations = appendRequired(violations, "manifestRef", registration.ManifestRef) if !safeRef(registration.ManifestRef) { violations = append(violations, "manifestRef is unsafe") } violations = appendRequired(violations, "manifest.id", manifest.ID) violations = appendRequired(violations, "manifest.name", manifest.Name) if len(manifest.Description) > maxPluginDescriptionLength { violations = append(violations, "manifest.description is too long") } violations = appendRequired(violations, "manifest.version", manifest.Version) if manifest.Kind != "game-plugin" { violations = append(violations, "manifest.kind must be game-plugin") } violations = appendRequired(violations, "manifest.server.type", manifest.Server.Type) violations = appendRequired(violations, "manifest.server.displayName", manifest.Server.DisplayName) violations = appendRequired(violations, "manifest.server.createFormSchema", manifest.Server.CreateFormSchema) if !safeRelativeJSONRef(manifest.Server.CreateFormSchema) { violations = append(violations, "manifest.server.createFormSchema must be a safe relative JSON reference") } violations = append(violations, validatePluginCreateFields("manifest.server.createFields", manifest.Server.CreateFields)...) for i, osName := range manifest.Server.SupportedOS { if !validPluginSupportedOS(osName) { violations = append(violations, fmt.Sprintf("manifest.server.supportedOs[%d] is not allowed", i)) } } violations = append(violations, duplicateViolations("manifest.server.supportedOs", manifest.Server.SupportedOS)...) for i, capability := range manifest.Capabilities { if !validPluginRunCapability(capability) { violations = append(violations, fmt.Sprintf("manifest.capabilities[%d] is not allowed", i)) } } if len(manifest.Capabilities) == 0 { violations = append(violations, "manifest.capabilities must not be empty") } violations = append(violations, duplicateViolations("manifest.capabilities", manifest.Capabilities)...) if len(manifest.Permissions) == 0 { violations = append(violations, "manifest.permissions must not be empty") } violations = append(violations, validateDeclaredPluginPermissions(manifest.Permissions)...) violations = append(violations, validateBridgeActions("manifest.bridge.actions", manifest.Bridge.Actions)...) violations = append(violations, validateLifecycleActions(manifest.Actions)...) violations = append(violations, validatePluginPages(manifest.Pages)...) violations = append(violations, validatePluginFileWorkspace("manifest.fileWorkspace", manifest.FileWorkspace)...) violations = append(violations, duplicateViolations("manifest.tags", manifest.Tags)...) violations = append(violations, validateAIPurposes(manifest.AI.Purposes)...) violations = append(violations, validateProductionLifecycle("manifest.productionLifecycle", manifest.ProductionLifecycle, true)...) if containsString(manifest.Permissions, "ai.invoke") || len(manifest.AI.Purposes) > 0 { if manifest.AI.Mediation != "platform" { violations = append(violations, "manifest.ai.mediation must be platform") } if manifest.AI.ConfigWritePolicy != "review-required" { violations = append(violations, "manifest.ai.configWritePolicy must be review-required") } } violations = append(violations, validateRemoteAccess("manifest.remoteAccess", manifest.RemoteAccess, manifest.Capabilities)...) if err := ValidateGamePluginRuntimeProfiles(manifest.RuntimeProfiles); err != nil { violations = append(violations, err.Error()) } violations = append(violations, validateRuntimeProfileCapabilityDeclarations(manifest.RuntimeProfiles, manifest.Capabilities)...) violations = append(violations, validateRuntimeLogEventPermissionDeclarations("manifest.runtimeProfiles.logEvents", manifest.RuntimeProfiles, manifest.Permissions)...) violations = append(violations, validateGameClientBridgeManifest("manifest.gameClientBridge", manifest.GameClientBridge, manifest.Permissions, manifest.Pages, manifest.RuntimeProfiles)...) violations = append(violations, validatePluginAssetFileDeclarations("manifest.assetFiles", manifest.AssetFiles)...) violations = append(violations, validatePluginAssetFiles("assetFiles", registration.AssetFiles)...) violations = append(violations, validateRegistrationAssetCoverage(registration.Manifest.AssetFiles, registration.AssetFiles)...) violations = append(violations, validateSafePluginStrings("manifest", manifestSafeStrings(registration))...) return finish(violations) } func validatePluginAssetFileDeclarations(prefix string, files []domain.PluginAssetFile) []string { if len(files) > 64 { return []string{prefix + " has too many files"} } var violations []string seen := map[string]struct{}{} for i, file := range files { field := fmt.Sprintf("%s[%d]", prefix, i) if !validLogicalFileKey(file.Path) { violations = append(violations, field+".path is unsafe") } if _, exists := seen[file.Path]; exists { violations = append(violations, field+".path is duplicated") } seen[file.Path] = struct{}{} if file.Content != "" { violations = append(violations, field+".content must be supplied only in registration assetFiles") } if file.Mode != 0 && file.Mode != 0o600 && file.Mode != 0o700 { violations = append(violations, field+".mode is unsafe") } } return violations } func validatePluginAssetFiles(prefix string, files []domain.PluginAssetFile) []string { if len(files) > 64 { return []string{prefix + " has too many files"} } var violations []string seen := map[string]struct{}{} for i, file := range files { field := fmt.Sprintf("%s[%d]", prefix, i) if !validLogicalFileKey(file.Path) { violations = append(violations, field+".path is unsafe") } if _, exists := seen[file.Path]; exists { violations = append(violations, field+".path is duplicated") } seen[file.Path] = struct{}{} if len([]byte(file.Content)) > 64*1024 || strings.ContainsRune(file.Content, '\x00') || containsUnsafeRuntimeSecret(file.Content) { violations = append(violations, field+".content is unsafe") } if file.Mode != 0 && (file.Mode < 0o400 || file.Mode > 0o700 || file.Mode&0o022 != 0) { violations = append(violations, field+".mode is unsafe") } } return violations } func validateRegistrationAssetCoverage(declared []domain.PluginAssetFile, payload []domain.PluginAssetFile) []string { if len(declared) == 0 { return nil } allowed := map[string]struct{}{} for _, file := range declared { allowed[file.Path] = struct{}{} } provided := map[string]struct{}{} for _, file := range payload { provided[file.Path] = struct{}{} if _, ok := allowed[file.Path]; !ok { return []string{"assetFiles contains undeclared plugin asset " + file.Path} } } var violations []string for _, file := range declared { if _, ok := provided[file.Path]; !ok { violations = append(violations, "assetFiles is missing declared plugin asset "+file.Path) } } return violations } func validatePluginCreateFields(prefix string, fields []domain.PluginCreateField) []string { if len(fields) > 32 { return []string{prefix + " must contain at most 32 fields"} } var violations []string keys := map[string]struct{}{} for i, field := range fields { item := fmt.Sprintf("%s[%d]", prefix, i) if !validDistributionLogicalKey(field.Key) { violations = append(violations, item+".key is invalid") } if _, found := keys[field.Key]; found { violations = append(violations, item+".key duplicates another field") } keys[field.Key] = struct{}{} if strings.TrimSpace(field.Label) == "" || len(field.Label) > 60 || strings.TrimSpace(field.Label) != field.Label { violations = append(violations, item+".label is invalid") } if !validPluginCreateFieldType(field.Type) { violations = append(violations, item+".type is invalid") } if len(field.DefaultValue) > 256 || strings.TrimSpace(field.DefaultValue) != field.DefaultValue || containsUnsafeRuntimeSecret(field.DefaultValue) || looksLikeRawHostPath(field.DefaultValue) { violations = append(violations, item+".defaultValue is unsafe") } if field.ConfigKey != "" && !validDistributionLogicalKey(field.ConfigKey) { violations = append(violations, item+".configKey is invalid") } if len(field.Options) > 32 { violations = append(violations, item+".options has too many values") } options := map[string]struct{}{} for _, option := range field.Options { if strings.TrimSpace(option) == "" || len(option) > 120 || strings.TrimSpace(option) != option || containsUnsafeRuntimeSecret(option) || looksLikeRawHostPath(option) { violations = append(violations, item+".options contains an unsafe value") break } if _, found := options[option]; found { violations = append(violations, item+".options contains duplicates") break } options[option] = struct{}{} } if field.Type == "select" && len(field.Options) == 0 { violations = append(violations, item+".options is required for select") } if field.Type != "select" && len(field.Options) > 0 { violations = append(violations, item+".options is only valid for select") } } return violations } func validPluginCreateFieldType(value string) bool { switch value { case "text", "number", "boolean", "select", "port": return true default: return false } } // ValidatePluginCreateInputs keeps game settings distinct from runtime // bindings while enforcing the plugin's published, non-executable field set. func ValidatePluginCreateInputs(fields []domain.PluginCreateField, inputs map[string]string) error { if len(fields) == 0 { return nil } declared := make(map[string]domain.PluginCreateField, len(fields)) for _, field := range fields { declared[field.Key] = field } var violations []string for key, value := range inputs { field, found := declared[key] if !found { violations = append(violations, "createInputs."+key+" is not declared by plugin") continue } if len(value) > 1024 || strings.TrimSpace(value) != value || containsUnsafeRuntimeSecret(value) { violations = append(violations, "createInputs."+key+" is invalid") continue } if value == "" { if field.Required && field.DefaultValue == "" { violations = append(violations, "createInputs."+key+" is required") } continue } switch field.Type { case "port": port, err := strconv.Atoi(value) if err != nil || port < 1 || port > 65535 { violations = append(violations, "createInputs."+key+" must be a port between 1 and 65535") } case "number": if _, err := strconv.ParseFloat(value, 64); err != nil { violations = append(violations, "createInputs."+key+" must be numeric") } case "boolean": if value != "true" && value != "false" { violations = append(violations, "createInputs."+key+" must be true or false") } case "select": if !containsString(field.Options, value) { violations = append(violations, "createInputs."+key+" is not an allowed option") } } } for _, field := range fields { if field.Required && field.DefaultValue == "" && strings.TrimSpace(inputs[field.Key]) == "" { violations = append(violations, "createInputs."+field.Key+" is required") } } return finish(violations) } func validateGameClientBridgeManifest(field string, bridge domain.GameClientBridgeManifest, permissions []string, pages []domain.GamePluginPage, runtimeProfiles domain.GamePluginRuntimeProfiles) []string { companionPresent := bridge.Companion != (domain.GameClientBridgeCompanionDeclaration{}) if len(bridge.Commands) == 0 && len(bridge.Snapshots) == 0 && len(bridge.QueryTemplates) == 0 && len(bridge.LogProjections) == 0 && len(bridge.DataPacks) == 0 && len(bridge.OperationTemplates) == 0 && len(bridge.Pages) == 0 && len(bridge.Features) == 0 && bridge.Retention.KeepForSeconds == 0 && bridge.Retention.MaxRecords == 0 && !companionPresent { return nil } var violations []string if bridge.Retention.KeepForSeconds <= 0 || bridge.Retention.KeepForSeconds > 365*24*60*60 { violations = append(violations, field+".commandRetentionSeconds is invalid") } if bridge.Retention.MaxRecords <= 0 || bridge.Retention.MaxRecords > 100000 { violations = append(violations, field+".maxCommands is invalid") } if companionPresent { prefix := field + ".companion" companion := bridge.Companion if !clientManagerIdentifierPattern.MatchString(companion.ProfileKey) || !clientManagerIdentifierPattern.MatchString(companion.ConfigTemplateKey) { violations = append(violations, prefix+" profile or config template key is invalid") } if !safeRelativeJSONRef(companion.ConfigSchemaRef) { violations = append(violations, prefix+".configSchemaRef must be a safe relative JSON reference") } if companion.ConfigFormat != "yaml" || companion.PlatformBaseURLSource != "run-control" || companion.RegistrationProof != "hmac-sha256" || companion.ProofMaterialSource != "component-package" || companion.SessionMode != "component-session" || companion.TLSPolicy != "verify-system-roots" { violations = append(violations, prefix+" bootstrap security policy is invalid") } if !validCompanionProofEnvironment(companion.ProofMaterialEnv) { violations = append(violations, prefix+".proofMaterialEnv is invalid") } if companion.HeartbeatIntervalSeconds < 5 || companion.HeartbeatIntervalSeconds > 300 || companion.CommandPollIntervalSeconds < 1 || companion.CommandPollIntervalSeconds > 60 || companion.RequestTimeoutSeconds < 1 || companion.RequestTimeoutSeconds > 60 { violations = append(violations, prefix+" timing policy is invalid") } managerFound := false for _, manager := range runtimeProfiles.ClientManagers { if manager.Key != companion.ProfileKey { continue } managerFound = true if manager.Health.IntervalSeconds != companion.HeartbeatIntervalSeconds { violations = append(violations, prefix+".heartbeatIntervalSeconds must match the Client Manager health interval") } for _, capability := range []string{"component.register", "component.heartbeat", "component.health", "game-client.bridge"} { if !containsString(manager.Health.RequiredCapabilities, capability) { violations = append(violations, prefix+" requires Client Manager capability "+capability) } } templateFound := false for _, template := range manager.ConfigTemplates { if template.Key == companion.ConfigTemplateKey { templateFound = true if template.OutputRef != "config.yaml" { violations = append(violations, prefix+" config template must materialize config.yaml") } } } if !templateFound { violations = append(violations, prefix+".configTemplateKey must reference the Client Manager profile") } } if !managerFound { violations = append(violations, prefix+".profileKey must reference a declared Client Manager profile") } } transports := map[string]domain.RuntimeTransportProfile{} for _, transport := range runtimeProfiles.TransportProfiles { transports[transport.Key] = transport } commandTypes := map[string]struct{}{} for index, command := range bridge.Commands { prefix := fmt.Sprintf("%s.commands[%d]", field, index) if !clientManagerIdentifierPattern.MatchString(command.Type) || unsafeGameClientBridgeCommandType(command.Type) { violations = append(violations, prefix+".type is invalid or unsafe") } if _, exists := commandTypes[command.Type]; exists { violations = append(violations, prefix+".type is duplicated") } commandTypes[command.Type] = struct{}{} if strings.TrimSpace(command.Title) == "" || len([]rune(command.Title)) > 80 { violations = append(violations, prefix+".title is invalid") } if !containsString(permissions, command.Permission) { violations = append(violations, prefix+".permission must be declared by the plugin") } if command.ApprovalLevel != domain.GameClientBridgeApprovalLevelNone && command.ApprovalLevel != domain.GameClientBridgeApprovalLevelOperator && command.ApprovalLevel != domain.GameClientBridgeApprovalLevelPlatformAdmin { violations = append(violations, prefix+".approvalLevel is invalid") } if !safeRelativeJSONRef(command.PayloadSchemaRef) || command.ResultSchemaRef != "" && !safeRelativeJSONRef(command.ResultSchemaRef) { violations = append(violations, prefix+" schema references must be safe relative JSON references") } if command.TimeoutSeconds <= 0 || command.TimeoutSeconds > 3600 { violations = append(violations, prefix+".timeoutSeconds is invalid") } if command.MaxPayloadBytes <= 0 || command.MaxPayloadBytes > maxGameClientBridgePayloadSize { violations = append(violations, prefix+".maxPayloadBytes is invalid") } } snapshotTypes := map[string]struct{}{} for index, snapshot := range bridge.Snapshots { prefix := fmt.Sprintf("%s.snapshots[%d]", field, index) key := snapshot.Type + "\x00" + snapshot.SchemaVersion if !clientManagerIdentifierPattern.MatchString(snapshot.Type) || !clientManagerIdentifierPattern.MatchString(snapshot.SchemaVersion) { violations = append(violations, prefix+" type or schemaVersion is invalid") } if _, exists := snapshotTypes[key]; exists { violations = append(violations, prefix+" type and schemaVersion are duplicated") } snapshotTypes[key] = struct{}{} if !safeRelativeJSONRef(snapshot.SchemaRef) { violations = append(violations, prefix+".schemaRef must be a safe relative JSON reference") } if snapshot.Retention.KeepForSeconds <= 0 || snapshot.Retention.KeepForSeconds > 31*24*60*60 || snapshot.Retention.MaxRecords <= 0 || snapshot.Retention.MaxRecords > 10000 { violations = append(violations, prefix+" retention is invalid") } } queryTemplates := map[string]domain.GameClientBridgeQueryTemplateDeclaration{} for index, template := range bridge.QueryTemplates { prefix := fmt.Sprintf("%s.queryTemplates[%d]", field, index) if !clientManagerIdentifierPattern.MatchString(template.Key) { violations = append(violations, prefix+".key is invalid") } if _, exists := queryTemplates[template.Key]; exists { violations = append(violations, prefix+".key is duplicated") } queryTemplates[template.Key] = template if strings.TrimSpace(template.Title) == "" || len([]rune(template.Title)) > 80 { violations = append(violations, prefix+".title is invalid") } if !containsString(permissions, template.Permission) { violations = append(violations, prefix+".permission must be declared by the plugin") } if template.Engine != "sqlite" { violations = append(violations, prefix+".engine must be sqlite") } if !safeRelativeJSONRef(template.ParameterSchemaRef) || !safeRelativeJSONRef(template.ResultSchemaRef) { violations = append(violations, prefix+" schema references must be safe relative JSON references") } if template.MaxRows < 1 || template.MaxRows > 500 { violations = append(violations, prefix+".maxRows is invalid") } if template.TimeoutSeconds < 1 || template.TimeoutSeconds > 60 { violations = append(violations, prefix+".timeoutSeconds is invalid") } if template.PollIntervalSeconds < 0 || template.PollIntervalSeconds > 86400 { violations = append(violations, prefix+".pollIntervalSeconds is invalid") } projectsRows := template.SQLRef != "" || template.RowTarget != nil if projectsRows { if !safeRelativeSQLRef(template.SQLRef) { violations = append(violations, prefix+".sqlRef must reference a package-relative SQL asset") } if template.RowTarget == nil { violations = append(violations, prefix+".rowTarget is required for projected queries") } else { target := template.RowTarget if !clientManagerIdentifierPattern.MatchString(target.Collection) || len(target.UpsertKeys) == 0 || len(target.ColumnMappings) == 0 { violations = append(violations, prefix+".rowTarget must declare a collection, upsert keys, and column mappings") } if target.WriteMode != "" && target.WriteMode != domain.PluginDataRowWriteModeMerge && target.WriteMode != domain.PluginDataRowWriteModeReplace { violations = append(violations, prefix+".rowTarget.writeMode must be merge or replace") } for _, key := range target.UpsertKeys { if !clientManagerIdentifierPattern.MatchString(key) { violations = append(violations, prefix+".rowTarget upsert key is invalid") } } for destination, source := range target.ColumnMappings { if !clientManagerIdentifierPattern.MatchString(destination) || !clientManagerIdentifierPattern.MatchString(source) { violations = append(violations, prefix+".rowTarget column mapping is invalid") } } } } transport, exists := transports[template.TransportKey] if !exists { violations = append(violations, prefix+".transportKey must reference a declared runtime transport profile") continue } if transport.TargetKey != template.TargetKey || strings.TrimSpace(template.TargetKey) == "" { violations = append(violations, prefix+".targetKey must match the declared runtime transport profile") } if transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery) { violations = append(violations, prefix+" transport must be sqlite with remote.run.db.sqlite.query capability") } } logProjectionKeys := map[string]struct{}{} for index, projection := range bridge.LogProjections { prefix := fmt.Sprintf("%s.logProjections[%d]", field, index) if !clientManagerIdentifierPattern.MatchString(projection.Key) { violations = append(violations, prefix+".key is invalid") } if _, exists := logProjectionKeys[projection.Key]; exists { violations = append(violations, prefix+".key is duplicated") } logProjectionKeys[projection.Key] = struct{}{} violations = append(violations, validateGameClientBridgeLogProjection(prefix, projection)...) } dataPackKeys := map[string]struct{}{} for index, dataPack := range bridge.DataPacks { prefix := fmt.Sprintf("%s.dataPacks[%d]", field, index) if !validDistributionLogicalKey(dataPack.Key) { violations = append(violations, prefix+".key is invalid") } if _, exists := dataPackKeys[dataPack.Key]; exists { violations = append(violations, prefix+".key is duplicated") } dataPackKeys[dataPack.Key] = struct{}{} if dataPack.DatabaseUserVersion < 1 || len(dataPack.LogParserRefs) == 0 || len(dataPack.ConfigMapRefs) == 0 { violations = append(violations, prefix+" must declare a database version and parser/config assets") } refs := append(domain.CopyStringSlice(dataPack.LogParserRefs), dataPack.ConfigMapRefs...) refs = append(refs, dataPack.DataRefs...) for _, ref := range refs { if !safeRelativeJSONRef(ref) { violations = append(violations, prefix+" asset reference is invalid") } } } operationTemplates := map[string]domain.GameClientBridgeOperationTemplateDeclaration{} for index, template := range bridge.OperationTemplates { prefix := fmt.Sprintf("%s.operationTemplates[%d]", field, index) if !clientManagerIdentifierPattern.MatchString(template.Key) || unsafeGameClientBridgeCommandType(template.Key) { violations = append(violations, prefix+".key is invalid or unsafe") } if _, exists := operationTemplates[template.Key]; exists { violations = append(violations, prefix+".key is duplicated") } operationTemplates[template.Key] = template if strings.TrimSpace(template.Title) == "" || len([]rune(template.Title)) > 80 { violations = append(violations, prefix+".title is invalid") } if !containsString(permissions, template.Permission) { violations = append(violations, prefix+".permission must be declared by the plugin") } if template.ApprovalLevel != domain.GameClientBridgeApprovalLevelNone && template.ApprovalLevel != domain.GameClientBridgeApprovalLevelOperator && template.ApprovalLevel != domain.GameClientBridgeApprovalLevelPlatformAdmin { violations = append(violations, prefix+".approvalLevel is invalid") } if template.Kind != domain.GameClientBridgeOperationKindRCON && template.Kind != domain.GameClientBridgeOperationKindSQLiteMutation { violations = append(violations, prefix+".kind is invalid") } if !safeRelativeJSONRef(template.PayloadSchemaRef) || template.ResultSchemaRef != "" && !safeRelativeJSONRef(template.ResultSchemaRef) || template.ConfirmationSchemaRef != "" && !safeRelativeJSONRef(template.ConfirmationSchemaRef) { violations = append(violations, prefix+" schema references must be safe relative JSON references") } if template.TimeoutSeconds < 1 || template.TimeoutSeconds > 3600 { violations = append(violations, prefix+".timeoutSeconds is invalid") } if template.MaxPayloadBytes < 1 || template.MaxPayloadBytes > maxGameClientBridgePayloadSize { violations = append(violations, prefix+".maxPayloadBytes is invalid") } transport, exists := transports[template.TransportKey] if !exists { violations = append(violations, prefix+".transportKey must reference a declared runtime transport profile") continue } if transport.TargetKey != template.TargetKey || strings.TrimSpace(template.TargetKey) == "" { violations = append(violations, prefix+".targetKey must match the declared runtime transport profile") } switch template.Kind { case domain.GameClientBridgeOperationKindRCON: if transport.Kind != "rcon" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunRCONCommand) { violations = append(violations, prefix+" transport must be rcon with remote.run.rcon.command capability") } if template.MaxRowsAffected != 0 { violations = append(violations, prefix+".maxRowsAffected is only valid for sqlite-mutation") } if !emptyGameClientBridgeOperationMutation(template.Mutation) { violations = append(violations, prefix+".mutation is only valid for sqlite-mutation") } case domain.GameClientBridgeOperationKindSQLiteMutation: if transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunProtectedSQL) { violations = append(violations, prefix+" transport must be sqlite with remote.run.protected.sql capability") } if template.ApprovalLevel != domain.GameClientBridgeApprovalLevelPlatformAdmin { violations = append(violations, prefix+".approvalLevel must require platform-admin approval for sqlite-mutation") } if template.MaxRowsAffected < 1 || template.MaxRowsAffected > 10 { violations = append(violations, prefix+".maxRowsAffected is invalid") } if !template.Safety.RequiresBeforeValue || !template.Safety.RequiresConfirmation || (!template.Safety.RequiresOfflinePlayer && !template.Safety.RequiresMaintenanceWindow) { violations = append(violations, prefix+".safety must require before value, confirmation, and offline or maintenance protection") } violations = append(violations, validateGameClientBridgeOperationMutation(prefix+".mutation", template.Mutation, queryTemplates)...) } } pageDeclarations := map[string]domain.GamePluginPage{} for _, page := range pages { pageDeclarations[page.Key] = page } features := map[string]domain.GameClientBridgeFeatureDeclaration{} for index, feature := range bridge.Features { prefix := fmt.Sprintf("%s.features[%d]", field, index) if !clientManagerIdentifierPattern.MatchString(feature.Key) || unsafeGameClientBridgeCommandType(feature.Key) { violations = append(violations, prefix+".key is invalid or unsafe") } if _, exists := features[feature.Key]; exists { violations = append(violations, prefix+".key is duplicated") } features[feature.Key] = feature if strings.TrimSpace(feature.Title) == "" || len([]rune(feature.Title)) > 80 { violations = append(violations, prefix+".title is invalid") } if !containsString(permissions, feature.Permission) { violations = append(violations, prefix+".permission must be declared by the plugin") } if len(feature.RequiredHandlers) == 0 && len(feature.RequiredEventProducers) == 0 { violations = append(violations, prefix+" must require a handler or event producer") } for _, handler := range feature.RequiredHandlers { if !clientManagerIdentifierPattern.MatchString(handler) { violations = append(violations, prefix+".requiredHandlers contains an invalid handler") } } for _, producer := range feature.RequiredEventProducers { if !clientManagerIdentifierPattern.MatchString(producer) { violations = append(violations, prefix+".requiredEventProducers contains an invalid producer") } } violations = append(violations, duplicateViolations(prefix+".requiredHandlers", feature.RequiredHandlers)...) violations = append(violations, duplicateViolations(prefix+".requiredEventProducers", feature.RequiredEventProducers)...) } for _, page := range pages { for _, featureKey := range page.FeatureKeys { feature, exists := features[featureKey] if !exists { violations = append(violations, field+" page "+page.Key+" references undeclared feature "+featureKey) continue } if !containsString(page.Permissions, feature.Permission) { violations = append(violations, field+" page "+page.Key+" must declare feature permission "+feature.Permission) } } } for index, page := range bridge.Pages { prefix := fmt.Sprintf("%s.pages[%d]", field, index) pageDeclaration, pageExists := pageDeclarations[page.PageKey] if !pageExists { violations = append(violations, prefix+".pageKey must reference a declared plugin page") } for _, commandType := range page.CommandTypes { if _, exists := commandTypes[commandType]; !exists { violations = append(violations, prefix+" references undeclared command "+commandType) } } for _, snapshotType := range page.SnapshotTypes { found := false for key := range snapshotTypes { if strings.HasPrefix(key, snapshotType+"\x00") { found = true break } } if !found { violations = append(violations, prefix+" references undeclared snapshot "+snapshotType) } } for _, templateKey := range page.QueryTemplateKeys { template, exists := queryTemplates[templateKey] if !exists { violations = append(violations, prefix+" references undeclared query template "+templateKey) continue } if !containsString(pageDeclaration.Permissions, template.Permission) { violations = append(violations, prefix+" must declare query template permission "+template.Permission) } if !containsString(pageDeclaration.BridgeActions, string(domain.PluginBridgeActionRemoteAccessRequest)) { violations = append(violations, prefix+" must declare remote.access.request for query templates") } } for _, operationKey := range page.OperationKeys { operation, exists := operationTemplates[operationKey] if !exists { violations = append(violations, prefix+" references undeclared operation template "+operationKey) continue } if !containsString(pageDeclaration.Permissions, operation.Permission) { violations = append(violations, prefix+" must declare operation template permission "+operation.Permission) } } for _, featureKey := range page.FeatureKeys { feature, exists := features[featureKey] if !exists { violations = append(violations, prefix+" references undeclared feature "+featureKey) continue } if !containsString(pageDeclaration.Permissions, feature.Permission) { violations = append(violations, prefix+" must declare feature permission "+feature.Permission) } } } return violations } func validateGameClientBridgeLogProjection(prefix string, projection domain.GameClientBridgeLogProjectionDeclaration) []string { var violations []string if len(projection.StreamKeys) < 1 || len(projection.StreamKeys) > 64 { violations = append(violations, prefix+".streamKeys must contain between 1 and 64 streams") } for _, streamKey := range projection.StreamKeys { if !clientManagerIdentifierPattern.MatchString(streamKey) { violations = append(violations, prefix+".streamKeys contains an invalid stream key") } } violations = append(violations, duplicateViolations(prefix+".streamKeys", projection.StreamKeys)...) captures := map[string]struct{}{} if len(projection.Steps) < 1 || len(projection.Steps) > 64 { violations = append(violations, prefix+".steps must contain between 1 and 64 patterns") } for index, step := range projection.Steps { stepPrefix := fmt.Sprintf("%s.steps[%d].pattern", prefix, index) if strings.TrimSpace(step.Pattern) == "" || len([]rune(step.Pattern)) > 16384 { violations = append(violations, stepPrefix+" is empty or too large") continue } compiled, err := regexp.Compile(step.Pattern) if err != nil { violations = append(violations, stepPrefix+" must be a valid regular expression") continue } for _, capture := range compiled.SubexpNames() { if capture != "" { captures[capture] = struct{}{} } } } if len(projection.CorrelationFields) < 1 || len(projection.CorrelationFields) > 64 { violations = append(violations, prefix+".correlationFields must contain between 1 and 64 captures") } for _, field := range projection.CorrelationFields { if !gameClientBridgeCaptureNamePattern.MatchString(field) { violations = append(violations, prefix+".correlationFields contains an invalid capture name") continue } if _, exists := captures[field]; !exists { violations = append(violations, prefix+".correlationFields references undeclared capture "+field) } } violations = append(violations, duplicateViolations(prefix+".correlationFields", projection.CorrelationFields)...) if projection.MaxInterveningLines < 0 || projection.MaxInterveningLines > 100000 { violations = append(violations, prefix+".maxInterveningLines is invalid") } violations = append(violations, validateGameClientBridgeLogProjectionTarget(prefix+".target", projection.Target, captures)...) if projection.Presence == nil { return violations } presence := projection.Presence if !gameClientBridgeFieldPattern.MatchString(presence.TimestampField) || !gameClientBridgeLogProjectionTargetDeclaresField(projection.Target, presence.TimestampField) { violations = append(violations, prefix+".presence.timestampField must reference a projected target field") } if presence.ActiveWindowSeconds < 1 || presence.ActiveWindowSeconds > 31536000 { violations = append(violations, prefix+".presence.activeWindowSeconds is invalid") } if presence.ActivityTarget != nil { violations = append(violations, validateGameClientBridgeLogProjectionTarget(prefix+".presence.activityTarget", *presence.ActivityTarget, captures)...) } return violations } func validateGameClientBridgeLogProjectionTarget(prefix string, target domain.GameClientBridgeLogProjectionTargetDeclaration, captures map[string]struct{}) []string { var violations []string if !gameClientBridgeCollectionPattern.MatchString(target.Collection) { violations = append(violations, prefix+".collection is invalid") } if len(target.UpsertKeys) < 1 || len(target.UpsertKeys) > 8 { violations = append(violations, prefix+".upsertKeys must contain between 1 and 8 fields") } for _, key := range target.UpsertKeys { if !gameClientBridgeFieldPattern.MatchString(key) { violations = append(violations, prefix+".upsertKeys contains an invalid field") } if !gameClientBridgeLogProjectionTargetDeclaresField(target, key) { violations = append(violations, prefix+".upsertKeys field "+key+" is not projected") } } violations = append(violations, duplicateViolations(prefix+".upsertKeys", target.UpsertKeys)...) if len(target.CaptureMappings) < 1 || len(target.CaptureMappings) > 64 { violations = append(violations, prefix+".captureMappings must contain between 1 and 64 mappings") } projectedFields := map[string]struct{}{} for destination, capture := range target.CaptureMappings { if !gameClientBridgeFieldPattern.MatchString(destination) || !gameClientBridgeCaptureNamePattern.MatchString(capture) { violations = append(violations, prefix+".captureMappings contains an invalid field or capture") } if _, exists := captures[capture]; !exists { violations = append(violations, prefix+".captureMappings references undeclared capture "+capture) } projectedFields[destination] = struct{}{} } if len(target.FixedValues) > 64 { violations = append(violations, prefix+".fixedValues contains too many fields") } for destination, value := range target.FixedValues { if !gameClientBridgeFieldPattern.MatchString(destination) || len([]rune(value)) > 4096 { violations = append(violations, prefix+".fixedValues contains an invalid field or oversized value") } if _, exists := projectedFields[destination]; exists { violations = append(violations, prefix+" declares field "+destination+" more than once") } projectedFields[destination] = struct{}{} } if target.ObservedAtField != "" { if !gameClientBridgeFieldPattern.MatchString(target.ObservedAtField) { violations = append(violations, prefix+".observedAtField is invalid") } if _, exists := projectedFields[target.ObservedAtField]; exists { violations = append(violations, prefix+" declares field "+target.ObservedAtField+" more than once") } } return violations } func gameClientBridgeLogProjectionTargetDeclaresField(target domain.GameClientBridgeLogProjectionTargetDeclaration, field string) bool { if target.ObservedAtField == field { return true } if _, exists := target.CaptureMappings[field]; exists { return true } _, exists := target.FixedValues[field] return exists } func validCompanionProofEnvironment(value string) bool { if len(value) < 3 || len(value) > 64 || value[0] < 'A' || value[0] > 'Z' { return false } for _, character := range value[1:] { if character != '_' && (character < 'A' || character > 'Z') && (character < '0' || character > '9') { return false } } reserved := map[string]struct{}{ "COMSPEC": {}, "DYLD_INSERT_LIBRARIES": {}, "DYLD_LIBRARY_PATH": {}, "HOME": {}, "LD_LIBRARY_PATH": {}, "LD_PRELOAD": {}, "PATH": {}, "PATHEXT": {}, "SHELL": {}, "SYSTEMROOT": {}, "TEMP": {}, "TMP": {}, "USERPROFILE": {}, "WINDIR": {}, } if _, exists := reserved[value]; exists { return false } return true } func unsafeGameClientBridgeCommandType(value string) bool { tokens := gameClientBridgePayloadKeyTokens(value) tokenSet := make(map[string]struct{}, len(tokens)) for _, token := range tokens { tokenSet[token] = struct{}{} } has := func(values ...string) bool { for _, candidate := range values { if _, ok := tokenSet[candidate]; ok { return true } } return false } if has("sql") || has("database", "db") && has("execute", "exec", "eval", "run", "query", "statement") || has("query") && has("execute", "exec", "eval", "raw", "statement") { return true } return has("shell", "powershell", "script", "terminal", "execute", "exec", "eval") || has("command", "cmd", "process", "system", "os", "executor") && has("run") } func emptyGameClientBridgeOperationMutation(value domain.GameClientBridgeOperationMutationDeclaration) bool { return value.FieldKey == "" && value.TableKey == "" && value.IdentityKey == "" && value.ValueKey == "" && value.ConfirmationQueryKey == "" && value.AllowedValueType == "" && value.MinValue == 0 && value.MaxValue == 0 } func validateGameClientBridgeOperationMutation(prefix string, value domain.GameClientBridgeOperationMutationDeclaration, queryTemplates map[string]domain.GameClientBridgeQueryTemplateDeclaration) []string { var violations []string for field, item := range map[string]string{"fieldKey": value.FieldKey, "tableKey": value.TableKey, "identityKey": value.IdentityKey, "valueKey": value.ValueKey, "confirmationQueryKey": value.ConfirmationQueryKey} { if !validDistributionLogicalKey(item) || unsafeGameClientBridgePayloadKey(item) { violations = append(violations, prefix+"."+field+" must be a safe logical key") } } if !oneOf(value.AllowedValueType, "integer", "number", "string", "boolean") { violations = append(violations, prefix+".allowedValueType is invalid") } if value.MaxValue != 0 && value.MinValue > value.MaxValue { violations = append(violations, prefix+".minValue must not exceed maxValue") } if value.ConfirmationQueryKey != "" { if _, exists := queryTemplates[value.ConfirmationQueryKey]; !exists { violations = append(violations, prefix+".confirmationQueryKey must reference a declared query template") } } return violations } func ValidatePluginBridgeAuthorizeRequest(request domain.PluginBridgeAuthorizeRequest) error { var violations []string violations = appendRequired(violations, "pluginId", request.PluginID) violations = appendRequired(violations, "routeKey", request.RouteKey) violations = appendRequired(violations, "action", string(request.Action)) if !validPluginBridgeAction(request.Action) { violations = append(violations, "action is not supported") } if request.Action == domain.PluginBridgeActionAIInvoke { violations = appendRequired(violations, "aiPurpose", request.AIPurpose) if strings.TrimSpace(request.AIPurpose) != "" && !validAIPurpose(request.AIPurpose) { violations = append(violations, "aiPurpose is not allowed") } } return finish(violations) } func ValidatePluginBridgeExecuteRequest(request domain.PluginBridgeExecuteRequest) error { request = domain.CopyPluginBridgeExecuteRequest(request) var violations []string violations = appendRequired(violations, "requestId", request.RequestID) violations = appendRequired(violations, "pluginId", request.PluginID) violations = appendRequired(violations, "routeKey", request.RouteKey) violations = appendRequired(violations, "action", string(request.Action)) if !validPluginBridgeAction(request.Action) { violations = append(violations, "action is not supported") } if request.Action != domain.PluginBridgeActionAIInvoke { violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID) } if request.Action == domain.PluginBridgeActionAIInvoke { violations = appendRequired(violations, "aiPurpose", request.AIPurpose) if strings.TrimSpace(request.AIPurpose) != "" && !validAIPurpose(request.AIPurpose) { violations = append(violations, "aiPurpose is not allowed") } } if len(request.Payload) > maxPluginBridgePayloadKeys { violations = append(violations, "payload has too many keys") } payloadSize := 0 for key, value := range request.Payload { payloadSize += len(key) + len(value) if strings.TrimSpace(key) == "" || strings.TrimSpace(key) != key || len([]rune(key)) > 80 { violations = append(violations, "payload key is invalid") } if len([]rune(value)) > 1024 { violations = append(violations, "payload value is too long") } for _, reason := range unsafePluginStringReasons(key) { violations = append(violations, "payload key: "+reason) } for _, reason := range unsafePluginStringReasons(value) { violations = append(violations, "payload."+key+": "+reason) } if containsUnsafeRuntimeSecret(value) || strings.Contains(strings.ToLower(value), "unix://") || strings.Contains(strings.ToLower(value), "tcp://") { violations = append(violations, "payload contains unsafe content") } } if payloadSize > maxPluginBridgePayloadSize { violations = append(violations, "payload is too large") } return finish(violations) } func ValidatePluginMarketplaceFilter(filter domain.PluginMarketplaceFilter) error { var violations []string if filter.Status != "" && !validGamePluginStatus(filter.Status) { violations = append(violations, "status is invalid") } if strings.TrimSpace(filter.ServerType) != filter.ServerType { violations = append(violations, "serverType must not have surrounding whitespace") } if strings.TrimSpace(filter.Capability) != filter.Capability { violations = append(violations, "capability must not have surrounding whitespace") } if len([]rune(filter.Keyword)) > maxMarketplaceKeywordSize { violations = append(violations, "keyword is too long") } for _, value := range []fieldString{ {field: "serverType", value: filter.ServerType}, {field: "capability", value: filter.Capability}, {field: "keyword", value: filter.Keyword}, } { for _, reason := range unsafePluginStringReasons(value.value) { violations = append(violations, "filter."+value.field+": "+reason) } } return finish(violations) } func ValidatePluginMarketplaceStateAction(action domain.PluginMarketplaceStateAction) error { if validPluginMarketplaceStateAction(action) { return nil } return ValidationError{Violations: []string{"action is not supported"}} } func ValidatePluginMarketplacePlugins(plugins []domain.PluginMarketplacePlugin) error { var violations []string if len(plugins) > maxMarketplaceListSize { violations = append(violations, "items is too long") } for i, plugin := range plugins { prefix := fmt.Sprintf("items[%d]", i) violations = append(violations, validatePluginMarketplacePlugin(prefix, plugin)...) } return finish(violations) } func ValidatePluginMarketplacePlugin(plugin domain.PluginMarketplacePlugin) error { return finish(validatePluginMarketplacePlugin("plugin", plugin)) } func validatePluginMarketplacePlugin(prefix string, plugin domain.PluginMarketplacePlugin) []string { var violations []string violations = appendRequired(violations, prefix+".id", plugin.ID) violations = appendRequired(violations, prefix+".name", plugin.Name) violations = appendRequired(violations, prefix+".version", plugin.Version) violations = appendRequired(violations, prefix+".serverType", plugin.ServerType) violations = appendRequired(violations, prefix+".manifestRef", plugin.ManifestRef) violations = appendRequired(violations, prefix+".createFormSchemaRef", plugin.CreateFormSchemaRef) violations = appendRequired(violations, prefix+".source", plugin.Source) if !validGamePluginStatus(plugin.Status) { violations = append(violations, prefix+".status is invalid") } if len(plugin.Description) > maxPluginDescriptionLength { violations = append(violations, prefix+".description is too long") } if len(plugin.Capabilities) == 0 { violations = append(violations, prefix+".capabilities must not be empty") } for i, capability := range plugin.Capabilities { if !validPluginRunCapability(capability) { violations = append(violations, fmt.Sprintf("%s.capabilities[%d] is not allowed", prefix, i)) } } for i, osName := range plugin.SupportedOS { if !validPluginSupportedOS(osName) { violations = append(violations, fmt.Sprintf("%s.supportedOs[%d] is not allowed", prefix, i)) } } violations = append(violations, duplicateViolations(prefix+".supportedOs", plugin.SupportedOS)...) violations = append(violations, duplicateViolations(prefix+".capabilities", plugin.Capabilities)...) violations = append(violations, validateDeclaredPluginPermissions(plugin.DeclaredPermissions)...) violations = append(violations, validateBridgeActions(prefix+".bridgeActions", plugin.BridgeActions)...) violations = append(violations, validatePluginPages(plugin.Pages)...) violations = append(violations, duplicateViolations(prefix+".tags", plugin.Tags)...) violations = append(violations, validateAIPurposes(plugin.AIPurposes)...) violations = append(violations, validateRemoteAccess(prefix+".remoteAccess", plugin.RemoteAccess, plugin.Capabilities)...) violations = append(violations, validateSafePluginStrings(prefix, marketplacePluginSafeStrings(plugin))...) return violations } func AuthorizePluginBridgeAction(plugin domain.GamePlugin, request domain.PluginBridgeAuthorizeRequest) (domain.PluginBridgeAuthorization, error) { result := domain.PluginBridgeAuthorization{ PluginID: request.PluginID, RouteKey: request.RouteKey, ServerInstanceID: request.ServerInstanceID, Action: request.Action, RequiredPermissions: requiredBridgePermissions(request.Action), EffectivePermissions: effectivePagePermissions(plugin, request.RouteKey), } if err := ValidateGamePlugin(plugin); err != nil { return result, err } if err := ValidatePluginBridgeAuthorizeRequest(request); err != nil { return result, err } if plugin.ID != request.PluginID { result.Reason = "pluginId must match installed plugin" return result, nil } if plugin.Status != domain.GamePluginStatusInstalled { result.Reason = "plugin must be installed" return result, nil } if !containsString(plugin.BridgeActions, string(request.Action)) { result.Reason = "bridge action is not declared by plugin" return result, nil } page, pageFound := findPluginPage(plugin.Pages, request.RouteKey) if !pageFound { result.Reason = "routeKey is not declared by plugin" return result, nil } if len(page.BridgeActions) > 0 && !containsString(page.BridgeActions, string(request.Action)) { result.Reason = "bridge action is not declared by page" return result, nil } if !containsAll(result.EffectivePermissions, result.RequiredPermissions) { result.Reason = "required permission is missing" return result, nil } if request.Action == domain.PluginBridgeActionAIInvoke && !containsString(plugin.AIPurposes, request.AIPurpose) { result.Reason = "ai purpose is not declared by plugin" return result, nil } result.Allowed = true return result, nil } func ValidateServerInstance(instance domain.ServerInstance) error { return validateServerInstance(instance, false) } func ValidateStoredServerInstance(instance domain.ServerInstance) error { return validateServerInstance(instance, true) } func validateServerInstance(instance domain.ServerInstance, allowDeleted bool) error { var violations []string violations = appendRequired(violations, "id", instance.ID) violations = appendRequired(violations, "pluginId", instance.PluginID) violations = appendRequired(violations, "pluginVersion", instance.PluginVersion) if instance.State != domain.ServerInstanceStateDraft && instance.State != domain.ServerInstanceStateDeleted { violations = appendRequired(violations, "runEndpointId", instance.RunEndpointID) } violations = appendRequired(violations, "name", instance.Name) if strings.TrimSpace(instance.OwnerUserID) != instance.OwnerUserID { violations = append(violations, "ownerUserId must not have surrounding whitespace") } for i, userID := range instance.AdminUserIDs { if strings.TrimSpace(userID) == "" { violations = append(violations, fmt.Sprintf("adminUserIds[%d] is required", i)) } if strings.TrimSpace(userID) != userID { violations = append(violations, fmt.Sprintf("adminUserIds[%d] must not have surrounding whitespace", i)) } if userID == instance.OwnerUserID { violations = append(violations, fmt.Sprintf("adminUserIds[%d] must not duplicate ownerUserId", i)) } } violations = append(violations, duplicateViolations("adminUserIds", instance.AdminUserIDs)...) if !validServerInstanceState(instance.State) { violations = append(violations, "state is invalid") } if instance.State == domain.ServerInstanceStateDeleted && !allowDeleted { violations = append(violations, "state must not be deleted on create") } if instance.ConfigVersion < 0 { violations = append(violations, "configVersion must not be negative") } if err := ValidateServerDeploymentDefinition(instance.Deployment); err != nil { violations = append(violations, err.Error()) } return finish(violations) } func ValidateServerDeploymentDefinition(definition domain.ServerDeploymentDefinition) error { if definition.Mode == "" { return nil } var violations []string if definition.Mode != domain.ServerDeploymentModeGuided && definition.Mode != domain.ServerDeploymentModeExisting && definition.Mode != domain.ServerDeploymentModeCustom { violations = append(violations, "deployment mode is invalid") } if definition.Shell != domain.ServerCommandShellNone && definition.Shell != domain.ServerCommandShellPosix && definition.Shell != domain.ServerCommandShellPowerShell && definition.Shell != domain.ServerCommandShellCmd { violations = append(violations, "deployment shell is invalid") } if definition.Revision < 0 { violations = append(violations, "deployment revision must not be negative") } for key, value := range definition.CreateInputs { if !validDistributionLogicalKey(key) || len(value) > 1024 || strings.TrimSpace(value) != value || containsUnsafeRuntimeSecret(value) { violations = append(violations, "deployment createInputs are invalid") break } } for key, value := range definition.RuntimeBindings { if !validDistributionLogicalKey(key) || strings.TrimSpace(value) != value || looksLikeRawHostPath(value) || containsUnsafeRuntimeSecret(value) { violations = append(violations, "deployment runtimeBindings are invalid") break } } for _, value := range []string{definition.ServerRoot, definition.WorkingDirectory} { if value != "" && (len(value) > 1024 || strings.TrimSpace(value) != value || !looksLikeAbsoluteHostPath(value)) { violations = append(violations, "deployment path must be an absolute host path") break } } for _, value := range []string{definition.InstallCommand, definition.StartCommand, definition.StopCommand, definition.StatusCommand} { if value != "" && (len(value) > 4096 || strings.TrimSpace(value) != value || strings.ContainsAny(value, "\x00\r\n") || containsUnsafeRuntimeSecret(value)) { violations = append(violations, "deployment command is invalid or contains a secret") break } } if definition.Mode == domain.ServerDeploymentModeCustom && strings.TrimSpace(definition.StartCommand) == "" { violations = append(violations, "custom deployment requires a start command") } return finish(violations) } func looksLikeAbsoluteHostPath(value string) bool { trimmed := strings.TrimSpace(value) return strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, `\\`) || (len(trimmed) >= 3 && trimmed[1] == ':' && (trimmed[2] == '\\' || trimmed[2] == '/')) } func ValidateServerInstanceUpdate(update domain.ServerInstanceUpdate) error { var violations []string if update.Name != nil { name := strings.TrimSpace(*update.Name) if name == "" { violations = append(violations, "name is required") } if name != *update.Name { violations = append(violations, "name must not have surrounding whitespace") } } return finish(violations) } func ValidateServerInstanceDependencies(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint) error { return ValidateServerInstanceDependenciesForCapabilities(instance, plugin, endpoint, plugin.RequiredRunCapabilities) } func ValidateServerInstanceDependenciesForCapabilities(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint, requiredRunCapabilities []string) error { var violations []string if plugin.ID == "" { violations = append(violations, "plugin is required") } else { if plugin.ID != instance.PluginID { violations = append(violations, "pluginId must match plugin") } if plugin.Status != domain.GamePluginStatusInstalled { violations = append(violations, "plugin must be installed") } if plugin.Version != instance.PluginVersion { violations = append(violations, "pluginVersion must match plugin") } } if endpoint.ID == "" { violations = append(violations, "run endpoint is required") } else { if endpoint.ID != instance.RunEndpointID { violations = append(violations, "runEndpointId must match run endpoint") } if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded { violations = append(violations, "run endpoint must be online or degraded") } missing := MissingCapabilities(endpoint.Capabilities, requiredRunCapabilities) if len(missing) > 0 { violations = append(violations, "run endpoint missing required capabilities: "+strings.Join(missing, ", ")) } } return finish(violations) } func ValidatePlatformResourceUsage(usage domain.PlatformResourceUsage) error { var violations []string violations = appendPercentViolation(violations, "cpuPercent", usage.CPUPercent) violations = appendPercentViolation(violations, "memoryPercent", usage.MemoryPercent) violations = appendPercentViolation(violations, "diskPercent", usage.DiskPercent) violations = appendRequired(violations, "source", usage.Source) if usage.CollectedAt.IsZero() { violations = append(violations, "collectedAt is required") } return finish(violations) } func ValidateServerMetricsList(items []domain.ServerMetrics) error { var violations []string if len(items) > 1000 { violations = append(violations, "items is too long") } for i, item := range items { field := fmt.Sprintf("items[%d]", i) if strings.TrimSpace(item.ServerInstanceID) == "" { violations = append(violations, field+".serverInstanceId is required") } if item.CollectedAt.IsZero() { violations = append(violations, field+".collectedAt is required") } if strings.TrimSpace(item.Source) == "" { violations = append(violations, field+".source is required") } violations = appendOptionalPercentViolation(violations, field+".cpuPercent", item.CPUPercent) violations = appendOptionalPercentViolation(violations, field+".memoryPercent", item.MemoryPercent) violations = appendOptionalPercentViolation(violations, field+".diskPercent", item.DiskPercent) if item.PlayerCount != nil && *item.PlayerCount < 0 { violations = append(violations, field+".playerCount must not be negative") } if item.MaxPlayers != nil && *item.MaxPlayers < 0 { violations = append(violations, field+".maxPlayers must not be negative") } if item.TPS != nil && (*item.TPS < 0 || *item.TPS > 100) { violations = append(violations, field+".tps must be between 0 and 100") } if item.LatencyMS != nil && *item.LatencyMS < 0 { violations = append(violations, field+".latencyMs must not be negative") } } return finish(violations) } func ValidateServerConfig(config domain.ServerConfig) error { var violations []string violations = appendRequired(violations, "serverInstanceId", config.ServerInstanceID) violations = appendRequired(violations, "format", config.Format) violations = appendRequired(violations, "key", config.Key) if config.ConfigVersion <= 0 { violations = append(violations, "configVersion must be positive") } if len([]byte(config.Content)) > maxServerConfigContentSize { violations = append(violations, "content is too large") } if config.Checksum != "" && !validSHA256Checksum(config.Checksum) { violations = append(violations, "checksum must be sha256:") } if config.UpdatedAt.IsZero() { violations = append(violations, "updatedAt is required") } if containsUnsafeRuntimeSecret(config.Content) { violations = append(violations, "content must not expose raw secrets, host paths, or direct sockets") } return finish(violations) } func ValidateServerConfigDiffRequest(request domain.ServerConfigDiffRequest) error { var violations []string violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID) violations = appendRequired(violations, "key", request.Key) if request.ExpectedConfigVersion <= 0 { violations = append(violations, "expectedConfigVersion must be positive") } if !validLogicalFileKey(request.Key) || !validConfigFileKey(request.Key) { violations = append(violations, "key is not allowed") } if len([]byte(request.ProposedContent)) > maxServerConfigContentSize { violations = append(violations, "proposedContent is too large") } if containsUnsafeRuntimeSecret(request.ProposedContent) { violations = append(violations, "proposedContent must not expose raw secrets, host paths, or direct sockets") } if request.ProposedContentInputRef != "" && !validScopedInputRef(request.ProposedContentInputRef) { violations = append(violations, "proposedContentInputRef is not allowed") } return finish(violations) } func ValidateServerConfigWriteApproval(approval domain.ServerConfigWriteApproval) error { request := domain.ServerConfigDiffRequest{ ServerInstanceID: approval.ServerInstanceID, ExpectedConfigVersion: approval.ExpectedConfigVersion, Key: approval.Key, ProposedContent: approval.ProposedContent, ProposedContentInputRef: approval.ProposedContentInputRef, } var violations []string if err := ValidateServerConfigDiffRequest(request); err != nil { if validationErr, ok := err.(ValidationError); ok { violations = append(violations, validationErr.Violations...) } else { violations = append(violations, err.Error()) } } violations = appendRequired(violations, "idempotencyKey", approval.IdempotencyKey) if containsUnsafeRuntimeSecret(approval.IdempotencyKey) || looksLikeRawHostPath(approval.IdempotencyKey) { violations = append(violations, "idempotencyKey is not allowed") } return finish(violations) } func ValidateFileOperationDispatchRequest(request domain.FileOperationDispatchRequest) error { var violations []string violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID) violations = appendRequired(violations, "key", request.Key) violations = appendRequired(violations, "idempotencyKey", request.IdempotencyKey) if !validFileOperationKind(request.Operation) { violations = append(violations, "operation is invalid") } if !validLogicalFileKey(request.Key) { violations = append(violations, "key is not allowed") } if request.Operation == domain.FileOperationWrite && request.InputRef == "" { violations = append(violations, "inputRef is required for writes") } if request.InputRef != "" && !validScopedInputRef(request.InputRef) { violations = append(violations, "inputRef is not allowed") } if len([]byte(request.Content)) > maxJobExecutionContentSize { violations = append(violations, "content is too large") } if containsUnsafeRuntimeSecret(request.Content) { violations = append(violations, "content must not expose raw secrets, host paths, or direct sockets") } if request.ExpectedConfigVersion < 0 { violations = append(violations, "expectedConfigVersion must not be negative") } for _, value := range []string{request.PluginID, request.IdempotencyKey} { if containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(strings.ToLower(value), "unix://") { violations = append(violations, "request contains unsafe content") } } return finish(violations) } func appendPercentViolation(violations []string, field string, value float64) []string { if value < 0 || value > 100 { return append(violations, field+" must be between 0 and 100") } return violations } func appendOptionalPercentViolation(violations []string, field string, value *float64) []string { if value == nil { return violations } return appendPercentViolation(violations, field, *value) } func containsUnsafeRuntimeSecret(value string) bool { lower := strings.ToLower(value) unsafeFragments := []string{ "/users/", "/var/run/", "unix://", "tcp://", "bearer ", "api_key=", "apikey=", "password=", "secret=", "sk-", } for _, fragment := range unsafeFragments { if strings.Contains(lower, fragment) { return true } } return false } func ValidateRunEndpoint(endpoint domain.RunEndpoint) error { var violations []string violations = appendRequired(violations, "id", endpoint.ID) violations = appendRequired(violations, "displayName", endpoint.DisplayName) violations = appendRequired(violations, "version", endpoint.Version) if endpoint.Architecture != "" { if !validDistributionTargetOS(endpoint.Platform) { violations = append(violations, "platform is invalid") } if !validDistributionTargetArch(endpoint.Architecture) { violations = append(violations, "architecture is invalid") } } if !validRunEndpointStatus(endpoint.Status) { violations = append(violations, "status is invalid") } violations = appendCapacityViolations(violations, endpoint.Capacity) for i, capability := range endpoint.Capabilities { if strings.TrimSpace(capability) == "" { violations = append(violations, fmt.Sprintf("capabilities[%d] is required", i)) } } return finish(violations) } func ValidateJob(job domain.Job) error { var violations []string violations = appendRequired(violations, "id", job.ID) violations = appendRequired(violations, "runEndpointId", job.RunEndpointID) violations = appendRequired(violations, "capability", job.Capability) violations = appendRequired(violations, "idempotencyKey", job.IdempotencyKey) if !validJobState(job.State) { violations = append(violations, "state is invalid") } if job.Progress.Percent < 0 || job.Progress.Percent > 100 { violations = append(violations, "progress.percent must be between 0 and 100") } if job.Progress.Phase != "" && !validDeploymentProgressPhase(job.Progress.Phase) { violations = append(violations, "progress.phase is invalid") } if len(job.Progress.Message) > maxProgressMessageLength { violations = append(violations, "progress.message is too long") } if job.Attempt < 0 { violations = append(violations, "attempt must not be negative") } if job.RetryPolicy.MaxAttempts <= 0 { violations = append(violations, "retryPolicy.maxAttempts must be positive") } if job.RetryPolicy.InitialBackoffSeconds <= 0 || job.RetryPolicy.MaxBackoffSeconds < job.RetryPolicy.InitialBackoffSeconds { violations = append(violations, "retryPolicy backoff must be positive and bounded") } if job.Attempt > job.RetryPolicy.MaxAttempts { violations = append(violations, "attempt must not exceed retryPolicy.maxAttempts") } if job.LeaseTokenHash != "" && len(job.LeaseTokenHash) != 64 { violations = append(violations, "leaseTokenHash must be a SHA-256 hash") } if len(job.CancelReason) > maxProgressMessageLength { violations = append(violations, "cancelReason is too long") } if len(job.ReconcileOutcome) > maxProgressMessageLength { violations = append(violations, "reconcileOutcome is too long") } if job.TargetKey != "" && !validLogicalFileKey(job.TargetKey) { violations = append(violations, "targetKey is not allowed") } if job.InputRef != "" && !validScopedInputRef(job.InputRef) { violations = append(violations, "inputRef is not allowed") } if len([]byte(job.ExecutionInput.Content)) > maxJobExecutionContentSize { violations = append(violations, "executionInput.content is too large") } if job.ExecutionInput.MaxReadBytes < 0 || job.ExecutionInput.MaxReadBytes > maxJobExecutionContentSize { violations = append(violations, "executionInput.maxReadBytes is out of bounds") } if job.ExecutionInput.ExpectedChecksum != "" && !validSHA256Checksum(job.ExecutionInput.ExpectedChecksum) { violations = append(violations, "executionInput.expectedChecksum must be sha256:") } if len(job.ExecutionInput.DLLExtensions) > 16 { violations = append(violations, "executionInput.dllExtensions must not exceed 16") } if len(job.ExecutionInput.DLLExtensions) > 0 && (job.Capability != domain.LifecycleCapabilityStart || job.ExecutionInput.LifecycleOperation != "start" || job.ServerInstanceID == "") { violations = append(violations, "executionInput.dllExtensions are allowed only for scoped process.start jobs") } for i, plan := range job.ExecutionInput.DLLExtensions { violations = append(violations, validateRuntimeDLLExtensionPlan(fmt.Sprintf("executionInput.dllExtensions[%d]", i), plan)...) } if job.ExecutionInput.LogSource != nil { violations = append(violations, validateRuntimeLogSourcePlanForJob("executionInput.logSource", job.ExecutionInput.LogSource)...) if job.Capability != domain.JobCapabilityLogsBackfill || job.ServerInstanceID == "" { violations = append(violations, "executionInput.logSource is allowed only for logs.backfill jobs") } } if len(job.ExecutionInput.LogSources) > 0 { if job.Capability != domain.LifecycleCapabilityStart || job.ExecutionInput.LifecycleOperation != "start" || job.ServerInstanceID == "" { violations = append(violations, "executionInput.logSources are allowed only for scoped process.start jobs") } seenKinds := map[string]struct{}{} for i, source := range job.ExecutionInput.LogSources { prefix := fmt.Sprintf("executionInput.logSources[%d]", i) violations = append(violations, validateRuntimeProcessLogSourcePlanForJob(prefix, source)...) if _, exists := seenKinds[source.Kind]; exists { violations = append(violations, "executionInput.logSources kind is duplicated") } seenKinds[source.Kind] = struct{}{} } } if job.ExecutionInput.SourceRCON != nil { violations = append(violations, validateRuntimeSourceRCONPlan("executionInput.sourceRcon", job.ExecutionInput.SourceRCON)...) isSourceCommand := job.Capability == domain.JobCapabilityRemoteRunRCONCommand if !isSourceCommand || job.ExecutionInput.RemoteAdapterKind != "rcon" { violations = append(violations, "executionInput.sourceRcon is allowed only for rcon jobs") } if job.RetryPolicy.MaxAttempts != 1 { violations = append(violations, "executionInput.sourceRcon jobs must have one attempt") } if len(job.ExecutionInput.Inputs) != 0 { violations = append(violations, "executionInput.sourceRcon must not persist adapter inputs") } } violations = append(violations, validateRemoteAdapterInputs("executionInput.inputs", job.ExecutionInput.Inputs)...) if job.ExecutionResult.Checksum != "" && !validSHA256Checksum(job.ExecutionResult.Checksum) { violations = append(violations, "executionResult.checksum must be sha256:") } if len([]byte(job.ExecutionResult.Content)) > maxJobExecutionContentSize { violations = append(violations, "executionResult.content is too large") } if len(job.ExecutionResult.Summary) > maxSummaryLength { violations = append(violations, "executionResult.summary is too long") } if job.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesRead || job.Capability == domain.JobCapabilityFilesWrite { if job.ServerInstanceID == "" { violations = append(violations, "serverInstanceId is required for scoped file jobs") } if job.TargetKey == "" { violations = append(violations, "targetKey is required for scoped file jobs") } } if job.Capability == domain.JobCapabilityConfigWrite || job.Capability == domain.JobCapabilityFilesWrite { if job.InputRef == "" { violations = append(violations, "inputRef is required for scoped write jobs") } } if isRemoteRunCapability(job.Capability) { if job.ServerInstanceID == "" { violations = append(violations, "serverInstanceId is required for remote access jobs") } if remoteCapabilityRequiresTargetKey(job.Capability) && job.TargetKey == "" { violations = append(violations, "targetKey is required for remote access jobs") } if remoteCapabilityRequiresInputRef(job.Capability) && job.InputRef == "" { violations = append(violations, "inputRef is required for remote access jobs") } } return finish(violations) } func validateRuntimeLogSourcePlanForJob(prefix string, source *domain.RuntimeLogSource) []string { if source == nil { return nil } var violations []string violations = append(violations, validateProfileKey(prefix+".key", source.Key)...) violations = append(violations, validateProfileKey(prefix+".targetKey", source.TargetKey)...) violations = append(violations, validateProfileKey(prefix+".streamKey", source.StreamKey)...) if source.Kind != "file.tail" { violations = append(violations, prefix+".kind must be file.tail") } if source.CursorKind != "" && !oneOf(source.CursorKind, "offset", "fingerprint") { violations = append(violations, prefix+".cursorKind is invalid") } if source.RetentionDays < 0 || source.RetentionDays > 365 { violations = append(violations, prefix+".retentionDays is invalid") } return violations } func validateRuntimeProcessLogSourcePlanForJob(prefix string, source domain.RuntimeLogSource) []string { var violations []string violations = append(violations, validateProfileKey(prefix+".key", source.Key)...) if source.TargetKey != "" { violations = append(violations, validateProfileKey(prefix+".targetKey", source.TargetKey)...) } violations = append(violations, validateProfileKey(prefix+".streamKey", source.StreamKey)...) if source.Kind != "process.stdout" && source.Kind != "process.stderr" { violations = append(violations, prefix+".kind must be process.stdout or process.stderr") } if source.CursorKind != "" && source.CursorKind != "sequence" { violations = append(violations, prefix+".cursorKind is invalid") } if source.RetentionDays < 0 || source.RetentionDays > 365 { violations = append(violations, prefix+".retentionDays is invalid") } return violations } func ValidateArtifact(artifact domain.Artifact) error { var violations []string violations = appendRequired(violations, "id", artifact.ID) violations = appendRequired(violations, "ownerId", artifact.OwnerID) violations = appendRequired(violations, "checksum", artifact.Checksum) if !validArtifactOwnerKind(artifact.OwnerKind) { violations = append(violations, "ownerKind is invalid") } if !validArtifactState(artifact.State) { violations = append(violations, "state is invalid") } if artifact.SizeBytes < 0 { violations = append(violations, "sizeBytes must not be negative") } return finish(violations) } func ValidateLogStream(stream domain.LogStream) error { var violations []string violations = appendRequired(violations, "id", stream.ID) violations = appendRequired(violations, "serverInstanceId", stream.ServerInstanceID) violations = appendRequired(violations, "streamKey", stream.StreamKey) violations = appendRequired(violations, "retentionPolicy", stream.RetentionPolicy) if !validLogStreamSource(stream.Source) { violations = append(violations, "source is invalid") } if !validLogStorageBackend(stream.StorageBackend) { violations = append(violations, "storageBackend is invalid") } hasSessionID := strings.TrimSpace(stream.LogSessionID) != "" hasSessionStart := !stream.SessionStartedAt.IsZero() if hasSessionID != hasSessionStart { violations = append(violations, "logSessionId and sessionStartedAt must be provided together") } if (hasSessionID || hasSessionStart) && stream.Source != domain.LogStreamSourceProcess { violations = append(violations, "log session metadata is only valid for process streams") } return finish(violations) } func MissingCapabilities(actual []string, required []string) []string { actualSet := make(map[string]struct{}, len(actual)) for _, capability := range actual { actualSet[capability] = struct{}{} } var missing []string for _, capability := range required { if _, exists := actualSet[capability]; !exists { missing = append(missing, capability) } } return missing } type fieldString struct { field string value string } func duplicateViolations(field string, values []string) []string { seen := map[string]struct{}{} var violations []string for i, value := range values { trimmed := strings.TrimSpace(value) if trimmed == "" { violations = append(violations, fmt.Sprintf("%s[%d] is required", field, i)) continue } if _, exists := seen[trimmed]; exists { violations = append(violations, fmt.Sprintf("%s[%d] duplicates an earlier value", field, i)) } seen[trimmed] = struct{}{} } return violations } func validateDeclaredPluginPermissions(permissions []string) []string { var violations []string for i, permission := range permissions { if !validPluginPermission(permission) { violations = append(violations, fmt.Sprintf("permissions[%d] is not allowed", i)) } } violations = append(violations, duplicateViolations("permissions", permissions)...) return violations } func validateLifecycleActions(actions domain.PluginLifecycleActions) []string { var violations []string required := []fieldString{ {field: "actions.install", value: actions.Install}, {field: "actions.start", value: actions.Start}, {field: "actions.stop", value: actions.Stop}, } for _, action := range required { violations = appendRequired(violations, action.field, action.value) } for _, action := range []fieldString{ {field: "actions.install", value: actions.Install}, {field: "actions.start", value: actions.Start}, {field: "actions.stop", value: actions.Stop}, {field: "actions.restart", value: actions.Restart}, {field: "actions.status", value: actions.Status}, } { if strings.TrimSpace(action.value) != "" && !safeRelativeJSONRef(action.value) { violations = append(violations, action.field+" must be a safe relative JSON reference") } } return violations } func validatePluginPages(pages []domain.GamePluginPage) []string { var violations []string seenKeys := map[string]struct{}{} for i, page := range pages { prefix := fmt.Sprintf("pages[%d]", i) violations = appendRequired(violations, prefix+".key", page.Key) violations = appendRequired(violations, prefix+".title", page.Title) violations = appendRequired(violations, prefix+".path", page.Path) if len(page.Title) > maxPluginPageTitleLength { violations = append(violations, prefix+".title is too long") } if !validPluginPagePath(page.Path) { violations = append(violations, prefix+".path is invalid") } if page.Bundle.Key != "" || page.Bundle.Version != "" || page.Bundle.IntegritySHA256 != "" { violations = appendRequired(violations, prefix+".bundle.key", page.Bundle.Key) violations = appendRequired(violations, prefix+".bundle.version", page.Bundle.Version) violations = appendRequired(violations, prefix+".bundle.integritySha256", page.Bundle.IntegritySHA256) if !validDistributionLogicalKey(page.Bundle.Key) || !validPluginPageBundleVersion(page.Bundle.Version) || !validPluginPageBundleIntegrity(page.Bundle.IntegritySHA256) { violations = append(violations, prefix+".bundle is invalid") } } if page.Key != "" { if _, exists := seenKeys[page.Key]; exists { violations = append(violations, prefix+".key duplicates another page") } seenKeys[page.Key] = struct{}{} } for permissionIndex, permission := range page.Permissions { if !validPluginPermission(permission) { violations = append(violations, fmt.Sprintf("%s.permissions[%d] is not allowed", prefix, permissionIndex)) } } violations = append(violations, duplicateViolations(prefix+".permissions", page.Permissions)...) violations = append(violations, validateBridgeActions(prefix+".bridgeActions", page.BridgeActions)...) for _, key := range page.FeatureKeys { if !clientManagerIdentifierPattern.MatchString(key) { violations = append(violations, prefix+".featureKeys contains an invalid feature key") } } violations = append(violations, duplicateViolations(prefix+".featureKeys", page.FeatureKeys)...) } return violations } func validPluginPageBundleVersion(value string) bool { if len(value) == 0 || len(value) > 80 { return false } for _, item := range value { if !(item >= 'a' && item <= 'z' || item >= 'A' && item <= 'Z' || item >= '0' && item <= '9' || item == '.' || item == '_' || item == '-') { return false } } return true } func validPluginPageBundleIntegrity(value string) bool { if len(value) != len("sha256:")+64 || !strings.HasPrefix(value, "sha256:") { return false } for _, item := range value[len("sha256:"):] { if !(item >= 'a' && item <= 'f' || item >= '0' && item <= '9') { return false } } return true } func validatePluginFileWorkspace(prefix string, workspace domain.PluginFileWorkspace) []string { if workspace.DefaultDirectoryKey == "" && len(workspace.Directories) == 0 && len(workspace.Files) == 0 && len(workspace.ConfigFields) == 0 { return nil } var violations []string directories := map[string]bool{} files := map[string]domain.PluginLogicalFile{} for i, item := range workspace.Directories { field := fmt.Sprintf("%s.directories[%d]", prefix, i) if !validDistributionLogicalKey(item.Key) || item.Label == "" || !oneOf(item.Scope, "config", "logs") { violations = append(violations, field+" is invalid") } if directories[item.Key] { violations = append(violations, field+".key duplicates another directory") } directories[item.Key] = true } if !directories[workspace.DefaultDirectoryKey] { violations = append(violations, prefix+".defaultDirectoryKey must reference a declared directory") } for i, item := range workspace.Files { field := fmt.Sprintf("%s.files[%d]", prefix, i) if !validDistributionLogicalKey(item.Key) || !directories[item.DirectoryKey] || item.Label == "" || !oneOf(item.Kind, "config", "log") || (item.Kind == "log" && item.StreamKey == "") { violations = append(violations, field+" is invalid") } if _, exists := files[item.Key]; exists { violations = append(violations, field+".key duplicates another file") } files[item.Key] = item } for i, item := range workspace.ConfigFields { field := fmt.Sprintf("%s.configFields[%d]", prefix, i) file, exists := files[item.FileKey] if !validDistributionLogicalKey(item.Key) || !exists || file.Kind != "config" || !file.Editable || item.ConfigKey == "" || item.Label == "" || item.Description == "" || !oneOf(item.Control, "text", "number", "boolean", "port") || !oneOf(item.RestartImpact, "none", "restart-required") || (item.Minimum > 0 && item.Maximum > 0 && item.Minimum > item.Maximum) { violations = append(violations, field+" is invalid") } } return violations } func validateBridgeActions(field string, actions []string) []string { var violations []string for i, action := range actions { if !validPluginBridgeAction(domain.PluginBridgeAction(action)) { violations = append(violations, fmt.Sprintf("%s[%d] is not supported", field, i)) } } violations = append(violations, duplicateViolations(field, actions)...) return violations } func validateAIPurposes(purposes []string) []string { var violations []string for i, purpose := range purposes { if !validAIPurpose(purpose) { violations = append(violations, fmt.Sprintf("aiPurposes[%d] is not allowed", i)) } } violations = append(violations, duplicateViolations("aiPurposes", purposes)...) return violations } func validateRemoteAccess(field string, remote domain.GamePluginRemoteAccess, declaredCapabilities []string) []string { var violations []string if len(remote.Methods) == 0 && len(remote.RunCapabilities) == 0 && len(remote.DatabaseEngines) == 0 && !remote.RCON && !remote.LogTransfer { return violations } if len(remote.Methods) == 0 { violations = append(violations, field+".methods must not be empty when remote access is declared") } for i, method := range remote.Methods { if !validRemoteAccessMethod(method) { violations = append(violations, fmt.Sprintf("%s.methods[%d] is not allowed", field, i)) } } violations = append(violations, duplicateViolations(field+".methods", remote.Methods)...) for i, capability := range remote.RunCapabilities { if !validPluginRunCapability(capability) || !isRemoteRunCapability(capability) { violations = append(violations, fmt.Sprintf("%s.runCapabilities[%d] is not allowed", field, i)) continue } if !containsString(declaredCapabilities, capability) { violations = append(violations, fmt.Sprintf("%s.runCapabilities[%d] must also be declared in capabilities", field, i)) } } violations = append(violations, duplicateViolations(field+".runCapabilities", remote.RunCapabilities)...) for i, engine := range remote.DatabaseEngines { if !validRemoteDatabaseEngine(engine) { violations = append(violations, fmt.Sprintf("%s.databaseEngines[%d] is not allowed", field, i)) } } violations = append(violations, duplicateViolations(field+".databaseEngines", remote.DatabaseEngines)...) if containsString(remote.Methods, "run") && len(remote.RunCapabilities) == 0 { violations = append(violations, field+".runCapabilities must not be empty when run access is declared") } if containsString(remote.Methods, "ftp") && !containsAny(declaredCapabilities, []string{domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite}) { violations = append(violations, field+" requires remote.ftp.read or remote.ftp.write when ftp is declared") } if containsString(remote.Methods, "rsync") && !containsAny(declaredCapabilities, []string{domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite}) { violations = append(violations, field+" requires remote.rsync.read or remote.rsync.write when rsync is declared") } if remote.RCON && !containsString(remote.RunCapabilities, domain.JobCapabilityRemoteRunRCONCommand) { violations = append(violations, field+".rcon requires remote.run.rcon.command") } if remote.LogTransfer && !containsString(remote.RunCapabilities, domain.JobCapabilityRemoteRunLogsTransfer) { violations = append(violations, field+".logTransfer requires remote.run.logs.transfer") } for _, engine := range remote.DatabaseEngines { required := domain.JobCapabilityRemoteRunDBMySQLQuery if engine == "sqlite" { required = domain.JobCapabilityRemoteRunDBSQLiteQuery } if !containsString(remote.RunCapabilities, required) { violations = append(violations, fmt.Sprintf("%s.databaseEngines requires %s", field, required)) } } return violations } func validateSafePluginStrings(prefix string, values []fieldString) []string { var violations []string for _, value := range values { for _, reason := range unsafePluginStringReasons(value.value) { violations = append(violations, prefix+"."+value.field+": "+reason) } } return violations } func pluginSafeStrings(plugin domain.GamePlugin) []fieldString { values := []fieldString{ {field: "id", value: plugin.ID}, {field: "name", value: plugin.Name}, {field: "description", value: plugin.Description}, {field: "serverType", value: plugin.ServerType}, {field: "serverDisplayName", value: plugin.ServerDisplayName}, {field: "manifestRef", value: plugin.ManifestRef}, {field: "createFormSchemaRef", value: plugin.CreateFormSchemaRef}, {field: "actions.install", value: plugin.LifecycleActions.Install}, {field: "actions.start", value: plugin.LifecycleActions.Start}, {field: "actions.stop", value: plugin.LifecycleActions.Stop}, {field: "actions.restart", value: plugin.LifecycleActions.Restart}, {field: "actions.status", value: plugin.LifecycleActions.Status}, } values = appendStringSliceFields(values, "supportedOs", plugin.SupportedOS) values = appendStringSliceFields(values, "requiredRunCapabilities", plugin.RequiredRunCapabilities) values = appendStringSliceFields(values, "declaredPermissions", plugin.DeclaredPermissions) values = appendStringSliceFields(values, "tags", plugin.Tags) values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes) values = appendStringSliceFields(values, "productionLifecycle.operations", plugin.ProductionLifecycle.Operations) values = appendStringSliceFields(values, "productionLifecycle.approvalRequired", plugin.ProductionLifecycle.ApprovalRequired) values = append(values, fieldString{field: "productionLifecycle.dependencyPolicy", value: plugin.ProductionLifecycle.DependencyPolicy}) values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions) values = appendStringSliceFields(values, "remoteAccess.methods", plugin.RemoteAccess.Methods) values = appendStringSliceFields(values, "remoteAccess.runCapabilities", plugin.RemoteAccess.RunCapabilities) values = appendStringSliceFields(values, "remoteAccess.databaseEngines", plugin.RemoteAccess.DatabaseEngines) for i, file := range plugin.LifecycleAssets { values = append(values, fieldString{field: fmt.Sprintf("lifecycleAssets[%d].path", i), value: file.Path}) } for i, page := range plugin.Pages { prefix := fmt.Sprintf("pages[%d]", i) values = append(values, fieldString{field: prefix + ".key", value: page.Key}, fieldString{field: prefix + ".title", value: page.Title}, fieldString{field: prefix + ".path", value: page.Path}, ) values = appendStringSliceFields(values, prefix+".permissions", page.Permissions) values = appendStringSliceFields(values, prefix+".bridgeActions", page.BridgeActions) } return values } func manifestSafeStrings(registration domain.GamePluginManifestRegistration) []fieldString { manifest := registration.Manifest values := []fieldString{ {field: "manifestRef", value: registration.ManifestRef}, {field: "id", value: manifest.ID}, {field: "name", value: manifest.Name}, {field: "description", value: manifest.Description}, {field: "kind", value: manifest.Kind}, {field: "server.type", value: manifest.Server.Type}, {field: "server.displayName", value: manifest.Server.DisplayName}, {field: "server.createFormSchema", value: manifest.Server.CreateFormSchema}, {field: "actions.install", value: manifest.Actions.Install}, {field: "actions.start", value: manifest.Actions.Start}, {field: "actions.stop", value: manifest.Actions.Stop}, {field: "actions.restart", value: manifest.Actions.Restart}, {field: "actions.status", value: manifest.Actions.Status}, } values = appendStringSliceFields(values, "tags", manifest.Tags) values = appendStringSliceFields(values, "server.supportedOs", manifest.Server.SupportedOS) values = appendStringSliceFields(values, "bridge.actions", manifest.Bridge.Actions) values = appendStringSliceFields(values, "capabilities", manifest.Capabilities) values = appendStringSliceFields(values, "permissions", manifest.Permissions) values = appendStringSliceFields(values, "ai.purposes", manifest.AI.Purposes) values = append(values, fieldString{field: "ai.mediation", value: manifest.AI.Mediation}, fieldString{field: "ai.configWritePolicy", value: manifest.AI.ConfigWritePolicy}, fieldString{field: "productionLifecycle.dependencyPolicy", value: manifest.ProductionLifecycle.DependencyPolicy}) values = appendStringSliceFields(values, "productionLifecycle.operations", manifest.ProductionLifecycle.Operations) values = appendStringSliceFields(values, "productionLifecycle.approvalRequired", manifest.ProductionLifecycle.ApprovalRequired) values = appendStringSliceFields(values, "remoteAccess.methods", manifest.RemoteAccess.Methods) values = appendStringSliceFields(values, "remoteAccess.runCapabilities", manifest.RemoteAccess.RunCapabilities) values = appendStringSliceFields(values, "remoteAccess.databaseEngines", manifest.RemoteAccess.DatabaseEngines) for i, file := range manifest.AssetFiles { values = append(values, fieldString{field: fmt.Sprintf("assetFiles[%d].path", i), value: file.Path}) } for i, page := range manifest.Pages { prefix := fmt.Sprintf("pages[%d]", i) values = append(values, fieldString{field: prefix + ".key", value: page.Key}, fieldString{field: prefix + ".title", value: page.Title}, fieldString{field: prefix + ".path", value: page.Path}, ) values = appendStringSliceFields(values, prefix+".permissions", page.Permissions) values = appendStringSliceFields(values, prefix+".bridgeActions", page.BridgeActions) } return values } func marketplacePluginSafeStrings(plugin domain.PluginMarketplacePlugin) []fieldString { values := []fieldString{ {field: "id", value: plugin.ID}, {field: "name", value: plugin.Name}, {field: "description", value: plugin.Description}, {field: "serverType", value: plugin.ServerType}, {field: "serverDisplayName", value: plugin.ServerDisplayName}, {field: "manifestRef", value: plugin.ManifestRef}, {field: "createFormSchemaRef", value: plugin.CreateFormSchemaRef}, {field: "actions.install", value: plugin.LifecycleActions.Install}, {field: "actions.start", value: plugin.LifecycleActions.Start}, {field: "actions.stop", value: plugin.LifecycleActions.Stop}, {field: "actions.restart", value: plugin.LifecycleActions.Restart}, {field: "actions.status", value: plugin.LifecycleActions.Status}, {field: "source", value: plugin.Source}, } values = appendStringSliceFields(values, "supportedOs", plugin.SupportedOS) values = appendStringSliceFields(values, "capabilities", plugin.Capabilities) values = appendStringSliceFields(values, "declaredPermissions", plugin.DeclaredPermissions) values = appendStringSliceFields(values, "tags", plugin.Tags) values = appendStringSliceFields(values, "aiPurposes", plugin.AIPurposes) values = appendStringSliceFields(values, "bridgeActions", plugin.BridgeActions) values = appendStringSliceFields(values, "remoteAccess.methods", plugin.RemoteAccess.Methods) values = appendStringSliceFields(values, "remoteAccess.runCapabilities", plugin.RemoteAccess.RunCapabilities) values = appendStringSliceFields(values, "remoteAccess.databaseEngines", plugin.RemoteAccess.DatabaseEngines) for i, page := range plugin.Pages { prefix := fmt.Sprintf("pages[%d]", i) values = append(values, fieldString{field: prefix + ".key", value: page.Key}, fieldString{field: prefix + ".title", value: page.Title}, fieldString{field: prefix + ".path", value: page.Path}, ) values = appendStringSliceFields(values, prefix+".permissions", page.Permissions) values = appendStringSliceFields(values, prefix+".bridgeActions", page.BridgeActions) } return values } func appendStringSliceFields(values []fieldString, field string, items []string) []fieldString { for i, item := range items { values = append(values, fieldString{field: fmt.Sprintf("%s[%d]", field, i), value: item}) } return values } func unsafePluginStringReasons(value string) []string { trimmed := strings.TrimSpace(value) lowered := strings.ToLower(trimmed) if trimmed == "" { return nil } var reasons []string if looksLikeRawSecret(trimmed) || strings.Contains(lowered, "raw api key") || strings.Contains(lowered, "provider key") || strings.Contains(lowered, "ai key") || strings.Contains(lowered, "raw credential") { reasons = append(reasons, "raw credential or AI/provider key content is not allowed") } if strings.Contains(lowered, "direct run") || strings.Contains(lowered, "run socket") || strings.Contains(lowered, "run credential") || strings.Contains(lowered, "run token") || strings.Contains(lowered, "direct socket") { reasons = append(reasons, "direct run access request is not allowed") } if strings.HasPrefix(lowered, "file://") || strings.HasPrefix(trimmed, `\\`) || looksLikeRawHostPath(trimmed) || strings.Contains(lowered, "host path") || strings.Contains(lowered, "raw host path") { reasons = append(reasons, "raw host path access is not allowed") } return reasons } func looksLikeRawHostPath(value string) bool { if len(value) >= 3 && ((value[1] == ':' && value[2] == '\\') || (value[1] == ':' && value[2] == '/')) { return true } lowered := strings.ToLower(value) for _, prefix := range []string{"/users/", "/etc/", "/var/", "/tmp/", "/home/", "/root/", "/private/", "/volumes/", "/opt/"} { if strings.HasPrefix(lowered, prefix) { return true } } return false } func safeRef(value string) bool { trimmed := strings.TrimSpace(value) return strings.HasPrefix(trimmed, "artifact://") || strings.HasPrefix(trimmed, "manifest://") || safeRelativeJSONRef(trimmed) } func safeRelativeJSONRef(value string) bool { trimmed := strings.TrimSpace(value) lowered := strings.ToLower(trimmed) if trimmed == "" || strings.HasPrefix(trimmed, "/") || strings.Contains(trimmed, "..") || strings.Contains(trimmed, "://") || strings.Contains(trimmed, `\`) || !strings.HasSuffix(lowered, ".json") { return false } if len(trimmed) >= 2 && trimmed[1] == ':' { return false } return true } func safeRelativeSQLRef(value string) bool { trimmed := strings.TrimSpace(value) lowered := strings.ToLower(trimmed) if trimmed == "" || strings.HasPrefix(trimmed, "/") || strings.Contains(trimmed, "..") || strings.Contains(trimmed, "://") || strings.Contains(trimmed, `\`) || !strings.HasSuffix(lowered, ".sql") { return false } return len(trimmed) < 2 || trimmed[1] != ':' } func looksLikeRawSecret(value string) bool { trimmed := strings.TrimSpace(strings.ToLower(value)) if trimmed == "" { return false } if strings.HasPrefix(trimmed, "secret://") || strings.HasPrefix(trimmed, "vault://") || strings.HasPrefix(trimmed, "env://") { return false } return strings.HasPrefix(trimmed, "sk-") || strings.HasPrefix(trimmed, "sk_") || strings.Contains(trimmed, "api_key=") || strings.Contains(trimmed, "apikey=") || strings.Contains(trimmed, "bearer ") } func validUserStatus(status domain.UserStatus) bool { switch status { case domain.UserStatusActive, domain.UserStatusDisabled, domain.UserStatusPending: return true default: return false } } func validUserRole(role string) bool { switch strings.ToLower(strings.TrimSpace(role)) { case "admin", "platform-admin", "platformadmin", "server-owner", "owner", "server-admin", "operator": return true default: return false } } func validAIProviderKind(kind domain.AIProviderKind) bool { switch kind { case domain.AIProviderKindOpenAICompatible, domain.AIProviderKindOpenAI, domain.AIProviderKindClaude, domain.AIProviderKindGemini, domain.AIProviderKindOllama, domain.AIProviderKindCustom: return true default: return false } } func validAIRelayMode(mode domain.AIRelayMode) bool { switch mode { case domain.AIRelayModeDirect, domain.AIRelayModeRelay, domain.AIRelayModeLocal: return true default: return false } } func validAIProviderStatus(status domain.AIProviderStatus) bool { switch status { case domain.AIProviderStatusActive, domain.AIProviderStatusDisabled, domain.AIProviderStatusError: return true default: return false } } func validGamePluginStatus(status domain.GamePluginStatus) bool { switch status { case domain.GamePluginStatusInstalled, domain.GamePluginStatusDisabled, domain.GamePluginStatusInvalid, domain.GamePluginStatusUpdating: return true default: return false } } func validPluginMarketplaceStateAction(action domain.PluginMarketplaceStateAction) bool { switch action { case domain.PluginMarketplaceStateActionInstall, domain.PluginMarketplaceStateActionEnable, domain.PluginMarketplaceStateActionDisable: return true default: return false } } func validPluginRunCapability(capability string) bool { switch capability { case "process.install", "process.start", "process.stop", "process.restart", "process.status", "config.write", "files.list", "files.read", "files.write", "files.patch", "file.list", "file.read", "file.write", "file.patch", "logs.read", "log.query", domain.JobCapabilityLogsBackfill, domain.JobCapabilityRemoteFTPRead, domain.JobCapabilityRemoteFTPWrite, domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite, domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite, domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop, domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProgram, domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall, domain.JobCapabilityDeploymentPlan, domain.JobCapabilityDeploymentShellPosix, domain.JobCapabilityDeploymentShellPowerShell, domain.JobCapabilityDeploymentShellCmd, domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate, domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall, "artifacts.read", "artifacts.write", "artifact.read", "artifact.write", "ai.invoke": return true default: return false } } func isRemoteRunCapability(capability string) bool { return strings.HasPrefix(capability, "remote.") } func remoteCapabilityRequiresTargetKey(capability string) bool { switch capability { case domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop: return false default: return isRemoteRunCapability(capability) } } func remoteCapabilityRequiresInputRef(capability string) bool { switch capability { case domain.JobCapabilityRemoteFTPWrite, domain.JobCapabilityRemoteRsyncWrite, domain.JobCapabilityRemoteRunFilesWrite, domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProgram: return true default: return false } } func validRemoteAccessMethod(method string) bool { switch method { case "ftp", "rsync", "run": return true default: return false } } func validRemoteDatabaseEngine(engine string) bool { switch engine { case "mysql", "sqlite": return true default: return false } } func validFileOperationKind(operation domain.FileOperationKind) bool { switch operation { case domain.FileOperationRead, domain.FileOperationWrite: return true default: return false } } func validConfigFileKey(key string) bool { switch key { case "server.properties", "config/server.properties": return true default: return strings.HasPrefix(key, "config/") && (strings.HasSuffix(key, ".properties") || strings.HasSuffix(key, ".json")) } } func validLogicalFileKey(key string) bool { trimmed := strings.TrimSpace(key) if trimmed == "" || trimmed != key || len([]rune(key)) > maxLogicalFileKeyLength { return false } if strings.HasPrefix(key, "/") || strings.Contains(key, "..") || strings.Contains(key, `\`) || strings.Contains(key, "://") || looksLikeRawHostPath(key) || containsUnsafeRuntimeSecret(key) { return false } for _, char := range key { if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' || char == '/' { continue } return false } return true } func validScopedInputRef(ref string) bool { trimmed := strings.TrimSpace(ref) if trimmed == "" || trimmed != ref || containsUnsafeRuntimeSecret(ref) || looksLikeRawHostPath(ref) { return false } return strings.HasPrefix(ref, "input://") || strings.HasPrefix(ref, "artifact://") } func validPluginPermission(permission string) bool { switch permission { case "server.create", "server.read", "server.lifecycle", "server.files.read", "server.files.write", "server.logs.read", "server.artifacts.read", "server.artifacts.write", "server.remote.access", "server.run.distribution", "server.dependencies.manage", "server.client-manager.manage", "server.game-client.read", "server.game-client.command", "server.game-client.maintenance", "ai.invoke": return true default: return false } } func validPluginBridgeAction(action domain.PluginBridgeAction) bool { switch action { case domain.PluginBridgeActionServerInstancesRead, domain.PluginBridgeActionJobsDispatch, domain.PluginBridgeActionLogsQuery, domain.PluginBridgeActionArtifactsOpen, domain.PluginBridgeActionFilesRequest, domain.PluginBridgeActionRemoteAccessRequest, domain.PluginBridgeActionRunDistribution, domain.PluginBridgeActionDependenciesRequest, domain.PluginBridgeActionLogsBackfillRequest, domain.PluginBridgeActionClientManager, domain.PluginBridgeActionPluginLifecycle, domain.PluginBridgeActionAIInvoke: return true default: return false } } func requiredBridgePermissions(action domain.PluginBridgeAction) []string { switch action { case domain.PluginBridgeActionServerInstancesRead: return []string{"server.read"} case domain.PluginBridgeActionJobsDispatch: return []string{"server.lifecycle"} case domain.PluginBridgeActionLogsQuery: return []string{"server.logs.read"} case domain.PluginBridgeActionArtifactsOpen: return []string{"server.artifacts.read"} case domain.PluginBridgeActionFilesRequest: return []string{"server.files.read"} case domain.PluginBridgeActionRemoteAccessRequest: return []string{"server.remote.access"} case domain.PluginBridgeActionRunDistribution: return []string{"server.run.distribution"} case domain.PluginBridgeActionDependenciesRequest: return []string{"server.dependencies.manage"} case domain.PluginBridgeActionLogsBackfillRequest: return []string{"server.logs.read"} case domain.PluginBridgeActionClientManager: return []string{"server.client-manager.manage"} case domain.PluginBridgeActionPluginLifecycle: return []string{"server.lifecycle"} case domain.PluginBridgeActionAIInvoke: return []string{"ai.invoke"} default: return nil } } func validateProductionLifecycle(field string, lifecycle domain.GamePluginProductionLifecycle, required bool) []string { var violations []string if required && len(lifecycle.Operations) == 0 { violations = append(violations, field+".operations must not be empty") } for i, operation := range lifecycle.Operations { switch domain.PluginLifecycleOperation(operation) { case domain.PluginLifecycleOperationInstall, domain.PluginLifecycleOperationEnable, domain.PluginLifecycleOperationDisable, domain.PluginLifecycleOperationUpgrade, domain.PluginLifecycleOperationRollback, domain.PluginLifecycleOperationRetire, domain.PluginLifecycleOperationDependencyCheck: default: violations = append(violations, fmt.Sprintf("%s.operations[%d] is invalid", field, i)) } } violations = append(violations, duplicateViolations(field+".operations", lifecycle.Operations)...) if lifecycle.DependencyPolicy != "required" && lifecycle.DependencyPolicy != "optional" { violations = append(violations, field+".dependencyPolicy must be required or optional") } for i, operation := range lifecycle.ApprovalRequired { if operation != string(domain.PluginLifecycleOperationDisable) && operation != string(domain.PluginLifecycleOperationRollback) && operation != string(domain.PluginLifecycleOperationRetire) { violations = append(violations, fmt.Sprintf("%s.approvalRequired[%d] is invalid", field, i)) } } for _, operation := range []string{string(domain.PluginLifecycleOperationDisable), string(domain.PluginLifecycleOperationRollback), string(domain.PluginLifecycleOperationRetire)} { if containsString(lifecycle.Operations, operation) && !containsString(lifecycle.ApprovalRequired, operation) { violations = append(violations, field+".approvalRequired must include "+operation) } } return violations } func effectivePagePermissions(plugin domain.GamePlugin, routeKey string) []string { declared := plugin.DeclaredPermissions page, found := findPluginPage(plugin.Pages, routeKey) if !found || len(page.Permissions) == 0 { return domain.CopyStringSlice(declared) } var effective []string for _, permission := range page.Permissions { if containsString(declared, permission) { effective = append(effective, permission) } } return effective } func findPluginPage(pages []domain.GamePluginPage, routeKey string) (domain.GamePluginPage, bool) { for _, page := range pages { if page.Key == routeKey { return page, true } } return domain.GamePluginPage{}, false } func containsAll(values []string, required []string) bool { for _, value := range required { if !containsString(values, value) { return false } } return true } func containsAny(values []string, candidates []string) bool { for _, candidate := range candidates { if containsString(values, candidate) { return true } } return false } func containsString(values []string, target string) bool { for _, value := range values { if value == target { return true } } return false } func validPluginSupportedOS(osName string) bool { switch osName { case "windows", "linux", "darwin": return true default: return false } } func validPluginPagePath(path string) bool { if !strings.HasPrefix(path, "/") || strings.Contains(path, "..") || strings.Contains(path, `\`) || strings.Contains(path, "://") { return false } for _, char := range path[1:] { if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '_' || char == '.' || char == '/' || char == '-' { continue } return false } return true } func validAIPurpose(purpose string) bool { switch purpose { case "config.read", "config.generate", "config.suggest", "logs.diagnose", "files.suggest": return true default: return false } } func validServerInstanceState(state domain.ServerInstanceState) bool { switch state { case domain.ServerInstanceStateDraft, domain.ServerInstanceStateInstalling, domain.ServerInstanceStateReady, domain.ServerInstanceStateRunning, domain.ServerInstanceStateStopped, domain.ServerInstanceStateFailed, domain.ServerInstanceStateDeleted: return true default: return false } } func validRunEndpointStatus(status domain.RunEndpointStatus) bool { switch status { case domain.RunEndpointStatusOnline, domain.RunEndpointStatusOffline, domain.RunEndpointStatusDegraded, domain.RunEndpointStatusDisabled: return true default: return false } } func validJobState(state domain.JobState) bool { switch state { case domain.JobStateQueued, domain.JobStateAccepted, domain.JobStateRunning, domain.JobStateRetrying, domain.JobStateSucceeded, domain.JobStateFailed, domain.JobStateCancelled: return true default: return false } } func validArtifactOwnerKind(kind domain.ArtifactOwnerKind) bool { switch kind { case domain.ArtifactOwnerKindPlatform, domain.ArtifactOwnerKindPlugin, domain.ArtifactOwnerKindServerInstance, domain.ArtifactOwnerKindJob: return true default: return false } } func validArtifactState(state domain.ArtifactState) bool { switch state { case domain.ArtifactStateUploading, domain.ArtifactStateAvailable, domain.ArtifactStateExpired, domain.ArtifactStateFailed: return true default: return false } } func validLogStreamSource(source domain.LogStreamSource) bool { switch source { case domain.LogStreamSourceProcess, domain.LogStreamSourceFile, domain.LogStreamSourcePlugin, domain.LogStreamSourceManagementProgram: return true default: return strings.TrimSpace(string(source)) != "" } } func validLogStorageBackend(backend domain.LogStorageBackend) bool { switch backend { case domain.LogStorageBackendLocalSegments, domain.LogStorageBackendLoki, domain.LogStorageBackendClickHouse, domain.LogStorageBackendOpenSearch, domain.LogStorageBackendElasticsearch: return true default: return false } }