package validator import ( "regexp" "strings" "browser.local/platform/domain" ) var clientManagerIdentifierPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$`) func ValidateClientManagerInstallation(value domain.ClientManagerInstallation) error { var violations []string violations = appendRequired(violations, "id", value.ID) violations = appendRequired(violations, "serverInstanceId", value.ServerInstanceID) violations = appendRequired(violations, "pluginId", value.PluginID) violations = appendRequired(violations, "profileKey", value.ProfileKey) violations = appendRequired(violations, "runEndpointId", value.RunEndpointID) if !validDistributionLogicalKey(value.ProfileKey) { violations = append(violations, "profileKey is invalid") } if value.TargetOS != "" || value.TargetArch != "" { violations = appendDistributionTargetViolations(violations, value.TargetOS, value.TargetArch) } if !validClientManagerLifecycleStatus(value.Status) { violations = append(violations, "status is invalid") } if !validClientManagerHealth(value.Health) { violations = append(violations, "health is invalid") } if value.KeyGeneration < 0 || value.DeploymentGeneration < 0 { violations = append(violations, "keyGeneration and deploymentGeneration must not be negative") } if value.Checksum != "" && !validSHA256Checksum(value.Checksum) { violations = append(violations, "checksum must be sha256:") } for field, content := range map[string]string{"phase": value.Phase, "healthReason": value.HealthReason} { if len(content) > 240 || unsafeLifecycleText(content) { violations = append(violations, field+" must be bounded and redacted") } } if value.CreatedAt.IsZero() || value.UpdatedAt.IsZero() { violations = append(violations, "createdAt and updatedAt are required") } return finish(violations) } func ValidateClientManagerLifecycleTransition(from, to domain.ClientManagerLifecycleStatus) error { if from == to { return nil } allowed := map[domain.ClientManagerLifecycleStatus][]domain.ClientManagerLifecycleStatus{ domain.ClientManagerLifecycleRequested: {domain.ClientManagerLifecycleBuilding, domain.ClientManagerLifecycleAvailable, domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleBuilding: {domain.ClientManagerLifecycleAvailable, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleAvailable: {domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleDeploying: {domain.ClientManagerLifecycleInstalled, domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleFailed, domain.ClientManagerLifecycleUninstalled}, domain.ClientManagerLifecycleInstalled: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleUninstalled, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleRegistering: {domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleOnline: {domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleDegraded: {domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleOffline: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleUninstalled, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleUpdating: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleRollingBack: {domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleOnline, domain.ClientManagerLifecycleDegraded, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleStopping: {domain.ClientManagerLifecycleInstalled, domain.ClientManagerLifecycleOffline, domain.ClientManagerLifecycleUninstalled, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleUninstalled: {domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleFailed}, domain.ClientManagerLifecycleFailed: {domain.ClientManagerLifecycleDeploying, domain.ClientManagerLifecycleRegistering, domain.ClientManagerLifecycleUpdating, domain.ClientManagerLifecycleRollingBack, domain.ClientManagerLifecycleStopping, domain.ClientManagerLifecycleUninstalled}, } for _, candidate := range allowed[from] { if candidate == to { return nil } } return finish([]string{"client-manager lifecycle transition is invalid"}) } func ValidateClientManagerSession(value domain.ClientManagerSession) error { var violations []string for field, content := range map[string]string{"id": value.ID, "installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID, "profileKey": value.ProfileKey, "runEndpointId": value.RunEndpointID, "artifactId": value.ArtifactID, "tokenHash": value.TokenHash} { violations = appendRequired(violations, field, content) } if value.KeyGeneration <= 0 || value.DeploymentGeneration <= 0 { violations = append(violations, "keyGeneration and deploymentGeneration must be positive") } if len(value.TokenHash) != 64 || !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(value.TokenHash) { violations = append(violations, "tokenHash must be a SHA-256 digest") } if !oneOf(string(value.Status), string(domain.ClientManagerSessionActive), string(domain.ClientManagerSessionRevoked), string(domain.ClientManagerSessionExpired)) { violations = append(violations, "status is invalid") } if value.ExpiresAt.IsZero() || value.CreatedAt.IsZero() || value.UpdatedAt.IsZero() { violations = append(violations, "session timestamps are required") } return finish(violations) } func ValidateClientManagerNonce(value domain.ClientManagerRegistrationNonce) error { var violations []string violations = appendRequired(violations, "id", value.ID) violations = appendRequired(violations, "installationId", value.InstallationID) if len(value.ID) != 64 || !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(value.ID) { violations = append(violations, "id must be a nonce SHA-256 digest") } if value.ExpiresAt.IsZero() || value.CreatedAt.IsZero() || !value.ExpiresAt.After(value.CreatedAt) { violations = append(violations, "nonce expiry must follow creation") } return finish(violations) } func ValidateClientManagerDeployRequest(value domain.ClientManagerDeployRequest) error { return validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, value.DistributionID, value.IdempotencyKey, value.ExpectedDeploymentGeneration, true) } func ValidateClientManagerControlRequest(value domain.ClientManagerControlRequest) error { if !oneOf(string(value.Operation), "start", "stop", "restart", "status", "rollback") { return finish([]string{"operation is invalid"}) } return validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, "", value.IdempotencyKey, value.ExpectedDeploymentGeneration, false) } func ValidateClientManagerUpdateRequest(value domain.ClientManagerUpdateRequest) error { var violations []string if !value.Approved { violations = append(violations, "approved must be true") } if err := validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, value.DistributionID, value.IdempotencyKey, value.ExpectedDeploymentGeneration, true); err != nil { violations = append(violations, err.Error()) } return finish(violations) } func ValidateClientManagerUninstallRequest(value domain.ClientManagerUninstallRequest) error { var violations []string if !value.Confirmed { violations = append(violations, "confirmed must be true") } if err := validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, "", value.IdempotencyKey, value.ExpectedDeploymentGeneration, false); err != nil { violations = append(violations, err.Error()) } return finish(violations) } func ValidateClientManagerRetryRequest(value domain.ClientManagerRetryRequest) error { return validateClientManagerOperationRequest(value.ServerInstanceID, value.ProfileKey, "", value.IdempotencyKey, value.ExpectedDeploymentGeneration, false) } func ValidateClientManagerLifecycleInputRequest(value domain.ClientManagerLifecycleInputRequest) error { var violations []string for field, content := range map[string]string{"runEndpointId": value.RunEndpointID, "sessionToken": value.SessionToken, "jobId": value.JobID, "leaseToken": value.LeaseToken} { violations = appendRequired(violations, field, content) } if value.Attempt <= 0 { violations = append(violations, "attempt must be positive") } return finish(violations) } func ValidateClientManagerCompanionConfigInput(value domain.ClientManagerCompanionConfigInput) error { var violations []string if value.SchemaVersion != domain.ClientManagerCompanionConfigSchemaVersion { violations = append(violations, "schemaVersion is invalid") } for field, content := range map[string]string{ "configTemplateKey": value.ConfigTemplateKey, "configTemplateRef": value.ConfigTemplateRef, "configOutputRef": value.ConfigOutputRef, "configSchemaRef": value.ConfigSchemaRef, "installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID, "pluginId": value.PluginID, "profileKey": value.ProfileKey, "artifactId": value.ArtifactID, "version": value.Version, "sourceRevision": value.SourceRevision, "targetOs": value.TargetOS, "targetArch": value.TargetArch, "proofMaterialEnv": value.ProofMaterialEnv, } { violations = appendRequired(violations, field, content) } if !clientManagerIdentifierPattern.MatchString(value.ConfigTemplateKey) { violations = append(violations, "configTemplateKey is invalid") } violations = append(violations, validateSafeRelativeRuntimePath("configTemplateRef", value.ConfigTemplateRef)...) if value.ConfigOutputRef != "config.yaml" { violations = append(violations, "configOutputRef must be config.yaml") } if !safeRelativeJSONRef(value.ConfigSchemaRef) { violations = append(violations, "configSchemaRef must be a safe relative JSON reference") } if value.ConfigFormat != "yaml" || value.PlatformBaseURLSource != "run-control" || value.RegistrationProof != "hmac-sha256" || value.ProofMaterialSource != "component-package" || value.SessionMode != "component-session" || value.TLSPolicy != "verify-system-roots" { violations = append(violations, "companion bootstrap security policy is invalid") } if !validCompanionProofEnvironment(value.ProofMaterialEnv) { violations = append(violations, "proofMaterialEnv is invalid") } for field, identifier := range map[string]string{"installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID, "pluginId": value.PluginID, "profileKey": value.ProfileKey, "artifactId": value.ArtifactID} { if !clientManagerIdentifierPattern.MatchString(identifier) { violations = append(violations, field+" is invalid") } } violations = appendDistributionTargetViolations(violations, value.TargetOS, value.TargetArch) if value.KeyGeneration <= 0 || value.DeploymentGeneration <= 0 { violations = append(violations, "keyGeneration and deploymentGeneration must be positive") } if len(value.Capabilities) == 0 || len(value.Capabilities) > 32 { violations = append(violations, "capabilities must be bounded") } capabilities := make(map[string]struct{}, len(value.Capabilities)) for _, capability := range value.Capabilities { if !clientManagerIdentifierPattern.MatchString(capability) { violations = append(violations, "capability is invalid") } if _, exists := capabilities[capability]; exists { violations = append(violations, "capabilities must be unique") } capabilities[capability] = struct{}{} } for _, required := range []string{"component.register", "component.heartbeat", "component.health", "game-client.bridge"} { if _, exists := capabilities[required]; !exists { violations = append(violations, "capabilities must include "+required) } } if value.HeartbeatIntervalSeconds < 5 || value.HeartbeatIntervalSeconds > 300 || value.CommandPollIntervalSeconds < 1 || value.CommandPollIntervalSeconds > 60 || value.RequestTimeoutSeconds < 1 || value.RequestTimeoutSeconds > 60 { violations = append(violations, "companion timing policy is invalid") } return finish(violations) } func ValidateClientManagerRegisterRequest(value domain.ClientManagerRegisterRequest) error { var violations []string for field, content := range map[string]string{"installationId": value.InstallationID, "serverInstanceId": value.ServerInstanceID, "profileKey": value.ProfileKey, "artifactId": value.ArtifactID, "version": value.Version, "sourceRevision": value.SourceRevision, "targetOs": value.TargetOS, "targetArch": value.TargetArch, "nonce": value.Nonce, "signature": value.Signature} { violations = appendRequired(violations, field, content) } violations = appendDistributionTargetViolations(violations, value.TargetOS, value.TargetArch) if value.KeyGeneration <= 0 || value.DeploymentGeneration <= 0 { violations = append(violations, "keyGeneration and deploymentGeneration must be positive") } if value.Timestamp.IsZero() { violations = append(violations, "timestamp is required") } if !regexp.MustCompile(`^[A-Za-z0-9_-]{16,128}$`).MatchString(value.Nonce) { violations = append(violations, "nonce is invalid") } if !regexp.MustCompile(`^sha256:[a-f0-9]{64}$`).MatchString(value.Signature) { violations = append(violations, "signature is invalid") } if len(value.Capabilities) == 0 { violations = append(violations, "capabilities must not be empty") } return finish(violations) } func ValidateClientManagerHeartbeat(value domain.ClientManagerHeartbeat) error { var violations []string violations = appendRequired(violations, "installationId", value.InstallationID) violations = appendRequired(violations, "sessionToken", value.SessionToken) if value.Sequence == 0 { violations = append(violations, "sequence must be positive") } if !validClientManagerHealth(value.Health) || value.Health == domain.ClientManagerHealthUnknown { violations = append(violations, "health is invalid") } if len(value.HealthReason) > 240 || unsafeLifecycleText(value.HealthReason) { violations = append(violations, "healthReason must be bounded and redacted") } if value.SentAt.IsZero() { violations = append(violations, "sentAt is required") } return finish(violations) } func validateClientManagerOperationRequest(serverID, profileKey, distributionID, idempotencyKey string, generation int, distributionRequired bool) error { var violations []string violations = appendRequired(violations, "serverInstanceId", serverID) violations = appendRequired(violations, "profileKey", profileKey) violations = appendRequired(violations, "idempotencyKey", idempotencyKey) if distributionRequired { violations = appendRequired(violations, "distributionId", distributionID) } if !validDistributionLogicalKey(profileKey) { violations = append(violations, "profileKey is invalid") } if !clientManagerIdentifierPattern.MatchString(idempotencyKey) { violations = append(violations, "idempotencyKey is invalid") } if generation < 0 { violations = append(violations, "expectedDeploymentGeneration must not be negative") } return finish(violations) } func validClientManagerLifecycleStatus(value domain.ClientManagerLifecycleStatus) bool { return oneOf(string(value), "requested", "building", "available", "deploying", "installed", "registering", "online", "degraded", "offline", "updating", "rolling_back", "stopping", "uninstalled", "failed") } func validClientManagerHealth(value domain.ClientManagerHealthStatus) bool { return oneOf(string(value), "unknown", "healthy", "degraded", "unhealthy", "offline") } func unsafeLifecycleText(value string) bool { lowered := strings.ToLower(strings.TrimSpace(value)) return strings.Contains(lowered, "secret://") || strings.Contains(lowered, "bearer ") || strings.Contains(lowered, "password=") || strings.Contains(lowered, "token=") || strings.Contains(lowered, "unix://") || strings.Contains(lowered, "tcp://") || looksLikeRawHostPath(value) }