Files
browser/platform/validator/resources.go
T
2026-07-11 14:56:10 +08:00

1373 lines
52 KiB
Go

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
}
}