package validator import ( "fmt" "strings" "browser.local/platform/domain" ) const ( maxAuditSummaryLength = 512 maxContactNoteLength = 160 maxMarketplaceKeywordSize = 80 maxMarketplaceListSize = 500 maxPluginDescriptionLength = 240 maxPluginPageTitleLength = 40 maxPluginBridgePayloadKeys = 16 maxPluginBridgePayloadSize = 4096 maxProgressMessageLength = 256 maxServerConfigContentSize = 64 * 1024 maxLogicalFileKeyLength = 160 ) 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, duplicateViolations("tags", plugin.Tags)...) violations = append(violations, validateAIPurposes(plugin.AIPurposes)...) 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") } 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, duplicateViolations("manifest.tags", manifest.Tags)...) violations = append(violations, validateAIPurposes(manifest.AI.Purposes)...) violations = append(violations, validateSafePluginStrings("manifest", manifestSafeStrings(registration))...) return finish(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, 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 { var violations []string violations = appendRequired(violations, "id", instance.ID) violations = appendRequired(violations, "pluginId", instance.PluginID) violations = appendRequired(violations, "pluginVersion", instance.PluginVersion) 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 { violations = append(violations, "state must not be deleted on create") } if instance.ConfigVersion < 0 { violations = append(violations, "configVersion must not be negative") } return finish(violations) } func ValidateServerInstanceDependencies(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint) 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, plugin.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.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 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 !validRunEndpointStatus(endpoint.Status) { violations = append(violations, "status is invalid") } if endpoint.Capacity.MaxJobs < 0 || endpoint.Capacity.RunningJobs < 0 || endpoint.Capacity.QueuedJobs < 0 { violations = append(violations, "capacity counts must not be negative") } if endpoint.Capacity.MaxJobs > 0 && endpoint.Capacity.RunningJobs > endpoint.Capacity.MaxJobs { violations = append(violations, "runningJobs must not exceed maxJobs") } 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 len(job.Progress.Message) > maxProgressMessageLength { violations = append(violations, "progress.message 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 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") } } return finish(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") } return finish(violations) } func ValidateAuditEvent(event domain.AuditEvent) error { var violations []string violations = appendRequired(violations, "id", event.ID) violations = appendRequired(violations, "actorId", event.ActorID) violations = appendRequired(violations, "action", event.Action) violations = appendRequired(violations, "resourceKind", event.ResourceKind) violations = appendRequired(violations, "resourceId", event.ResourceID) violations = appendRequired(violations, "summary", event.Summary) if !validAuditResult(event.Result) { violations = append(violations, "result is invalid") } if len(event.Summary) > maxAuditSummaryLength { violations = append(violations, "summary is too long") } if looksLikeRawSecret(event.Summary) { violations = append(violations, "summary must be redacted") } 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.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)...) } 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 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, "bridgeActions", plugin.BridgeActions) 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) 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) 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 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", "artifacts.read", "artifacts.write", "artifact.read", "artifact.write", "ai.invoke": 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", "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.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.PluginBridgeActionAIInvoke: return []string{"ai.invoke"} default: return nil } } 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 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.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: 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 } } func validAuditResult(result domain.AuditResult) bool { switch result { case domain.AuditResultSuccess, domain.AuditResultDenied, domain.AuditResultFailed, domain.AuditResultQueued: return true default: return false } }