xolo-server 1.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/data/client/xolo +182 -82
  3. data/lib/xolo/core/base_classes/title.rb +261 -20
  4. data/lib/xolo/core/base_classes/version.rb +53 -8
  5. data/lib/xolo/core/constants.rb +7 -3
  6. data/lib/xolo/core/security_cmd.rb +128 -0
  7. data/lib/xolo/core/version.rb +1 -1
  8. data/lib/xolo/core.rb +1 -0
  9. data/lib/xolo/server/app.rb +7 -0
  10. data/lib/xolo/server/configuration.rb +243 -37
  11. data/lib/xolo/server/constants.rb +10 -0
  12. data/lib/xolo/server/helpers/auth.rb +19 -2
  13. data/lib/xolo/server/helpers/autopkg.rb +170 -0
  14. data/lib/xolo/server/helpers/client_data.rb +91 -61
  15. data/lib/xolo/server/helpers/file_transfers.rb +412 -82
  16. data/lib/xolo/server/helpers/jamf_pro.rb +30 -7
  17. data/lib/xolo/server/helpers/log.rb +2 -0
  18. data/lib/xolo/server/helpers/maintenance.rb +1 -0
  19. data/lib/xolo/server/helpers/notification.rb +4 -3
  20. data/lib/xolo/server/helpers/pkg_signing.rb +16 -12
  21. data/lib/xolo/server/helpers/progress_streaming.rb +9 -12
  22. data/lib/xolo/server/helpers/subscriptions.rb +119 -0
  23. data/lib/xolo/server/helpers/titles.rb +27 -3
  24. data/lib/xolo/server/helpers/versions.rb +23 -11
  25. data/lib/xolo/server/mixins/changelog.rb +9 -16
  26. data/lib/xolo/server/mixins/title_jamf_access.rb +395 -383
  27. data/lib/xolo/server/mixins/title_ted_access.rb +29 -3
  28. data/lib/xolo/server/mixins/version_jamf_access.rb +185 -159
  29. data/lib/xolo/server/mixins/version_ted_access.rb +25 -0
  30. data/lib/xolo/server/object_locks.rb +2 -1
  31. data/lib/xolo/server/routes/auth.rb +2 -2
  32. data/lib/xolo/server/routes/jamf_pro.rb +11 -1
  33. data/lib/xolo/server/routes/maint.rb +2 -1
  34. data/lib/xolo/server/routes/subscriptions.rb +126 -0
  35. data/lib/xolo/server/routes/title_editor.rb +1 -1
  36. data/lib/xolo/server/routes/titles.rb +30 -17
  37. data/lib/xolo/server/routes/uploads.rb +0 -14
  38. data/lib/xolo/server/routes/versions.rb +22 -17
  39. data/lib/xolo/server/routes.rb +9 -0
  40. data/lib/xolo/server/title.rb +107 -78
  41. data/lib/xolo/server/tmp_overrides.rb +38 -0
  42. data/lib/xolo/server/version.rb +193 -15
  43. data/lib/xolo/server.rb +12 -0
  44. metadata +8 -9
@@ -223,12 +223,89 @@ module Xolo
223
223
  }
224
224
  end
225
225
 
226
+ # add a new version in response to a patch title update webhook event.
227
+ # This doesn't upload a pkg - it just creates the version in Xolo, and then
228
+ # someone can upload a pkg to it via xadm or autopkg will do it if configured.
229
+ #
230
+ # @param title_object [Xolo::Server::Title] the title object for the subscribed title
231
+ # @param new_version [String] the new version to add
232
+ # @return [void]
233
+ ######################
234
+ def self.add_version_via_subscription(title_object:, new_version:)
235
+ title_object.log_info "Adding new version '#{new_version}' for subscribed title '#{title_object.title}'"
236
+
237
+ # get more details about this version from the JPAPI
238
+ patch_version_data = title_object.patch_versions(version: new_version).first
239
+ unless patch_version_data
240
+ msg = "Could not get patch version data from JPAPI for version '#{new_version}' of subscribed title '#{title_object.title}'. Cannot create new version in Xolo without this data. Aborting."
241
+ title_object.log_error msg, alert: true
242
+ return
243
+ end
244
+
245
+ title_object.log_debug "Got patch version data from JPAPI for version '#{new_version}': #{patch_version_data}"
246
+
247
+ # put the data into a hash for creating a new version object
248
+ vobj_data = {
249
+ publish_date: Time.parse(patch_version_data[:releaseDate]),
250
+ standalone: patch_version_data[:standalone],
251
+ min_os: patch_version_data[:minimumOperatingSystem],
252
+ reboot: patch_version_data[:rebootRequired],
253
+ killapps: []
254
+ }
255
+
256
+ # Killapps for subscribed titles? The API only shows app names without the .app, e.g.
257
+ # "killApps": [
258
+ # {
259
+ # "appName": "ChrislTestHelper"
260
+ # },
261
+ # {
262
+ # "appName": "Chrisl Test"
263
+ # }
264
+ # ]
265
+ # Since we don't manage them, we'll just record them in the data like this...
266
+ unless patch_version_data[:killApps].pix_empty?
267
+ patch_version_data[:killApps].each do |ka|
268
+ vobj_data[:killapps] << "#{ka[:appName]}.app;unknown.from.subscription"
269
+ end
270
+ end
271
+
272
+ # instantiate the version object
273
+ title_object.log_debug "Instantiating version via subscription '#{new_version}' of title '#{title_object.title}' (#{title_object.class}) with data: #{vobj_data}"
274
+
275
+ vobj = title_object.server_app_instance.instantiate_version(
276
+ title: title_object,
277
+ version: new_version,
278
+ **vobj_data
279
+ )
280
+
281
+ # create it in xolo
282
+ vobj.create
283
+
284
+ # moved to vobj.create:
285
+ # vobj.server_app_instance.update_client_data
286
+
287
+ # tell someone
288
+ msg = "ACTION REQUIRED: New pilot for subscribed title '#{title_object.title}', version '#{new_version}' has been created in Xolo via subscription."
289
+
290
+ # if not autopkg enabled, we need to tell someone to upload a pkg for this new version
291
+ unless title_object.autopkg_enabled?
292
+ # update general alert msg
293
+ msg = "#{msg}\nPlease upload a .pkg for it ASAP using this command:\n xadm edit-version #{title_object.title} #{new_version} --pkg-to-upload /path/to/installer.pkg"
294
+
295
+ # email to title contact
296
+ vobj.server_app_instance.send_email to: title_object.contact_email, subject: 'Need manual upload of xolo pkg', msg: msg
297
+ end # if title_object.autopkg_enabled?
298
+
299
+ # send general alert
300
+ vobj.log_info msg, alert: true
301
+ end
302
+
226
303
  # Attributes
227
304
  ######################
228
305
  ######################
229
306
 
230
307
  # The instance of Xolo::Server::App that instantiated this
231
- # title object. This is how we access things that are available in routes
308
+ # version object. This is how we access things that are available in routes
232
309
  # and helpers, like the single Jamf and TEd
233
310
  # connections for this App instance.
234
311
  attr_accessor :server_app_instance
@@ -297,6 +374,9 @@ module Xolo
297
374
  # one of :creating, :updating, :deleting
298
375
  attr_accessor :current_action
299
376
 
377
+ # @return [Boolean] is the pkg being processed now from an autopkg recipe?
378
+ attr_accessor :pkg_is_from_autopkg
379
+
300
380
  # Constructor
301
381
  ######################
302
382
  ######################
@@ -314,15 +394,15 @@ module Xolo
314
394
  @jamf_pkg_id ||= data_hash[:jamf_pkg_id]
315
395
 
316
396
  # and these can be generated now
317
- @jamf_obj_name_pfx = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{title}-#{version}"
397
+ @jamf_obj_name_pfx = "#{jamf_obj_name_pfx_base}#{title}-#{version}"
318
398
 
319
399
  @jamf_pkg_name ||= @jamf_obj_name_pfx
320
400
 
321
- @jamf_installed_group_name = "#{jamf_obj_name_pfx}#{JAMF_SMART_GROUP_NAME_INSTALLED_SFX}"
401
+ @jamf_installed_group_name = "#{jamf_obj_name_pfx}-#{JAMF_SMART_GROUP_NAME_INSTALLED_SFX}"
322
402
 
323
- @jamf_auto_install_policy_name = "#{jamf_obj_name_pfx}#{JAMF_POLICY_NAME_AUTO_INSTALL_SFX}"
324
- @jamf_manual_install_policy_name = "#{jamf_obj_name_pfx}#{JAMF_POLICY_NAME_MANUAL_INSTALL_SFX}"
325
- @jamf_auto_reinstall_policy_name = "#{jamf_obj_name_pfx}#{JAMF_POLICY_NAME_AUTO_REINSTALL_SFX}"
403
+ @jamf_auto_install_policy_name = "#{jamf_obj_name_pfx}-#{JAMF_POLICY_NAME_AUTO_INSTALL_SFX}"
404
+ @jamf_manual_install_policy_name = "#{jamf_obj_name_pfx}-#{JAMF_POLICY_NAME_MANUAL_INSTALL_SFX}"
405
+ @jamf_auto_reinstall_policy_name = "#{jamf_obj_name_pfx}-#{JAMF_POLICY_NAME_AUTO_REINSTALL_SFX}"
326
406
 
327
407
  @jamf_patch_policy_name = @jamf_obj_name_pfx
328
408
 
@@ -391,7 +471,17 @@ module Xolo
391
471
  def pilot_groups_to_use
392
472
  return @pilot_groups_to_use if @pilot_groups_to_use
393
473
 
474
+ # any defined in the version override any in the title
394
475
  @pilot_groups_to_use = changes_for_update&.key?(:pilot_groups) ? changes_for_update[:pilot_groups][:new] : pilot_groups
476
+ return @pilot_groups_to_use unless @pilot_groups_to_use.empty?
477
+
478
+ # if none defined in the version, look in the title
479
+ @pilot_groups_to_use =
480
+ if title_object.changes_for_update&.key?(:pilot_groups)
481
+ title_object.changes_for_update[:pilot_groups][:new]
482
+ else
483
+ title_object.pilot_groups
484
+ end
395
485
  end
396
486
 
397
487
  # The scope excluded groups to use in policies and patch policies for all versions of
@@ -456,10 +546,12 @@ module Xolo
456
546
  # @session ||= {}
457
547
  end
458
548
 
549
+ # This can be manually set earlier in the request handling to use a non-standard
550
+ # admin username
459
551
  # @return [String]
460
552
  ###################
461
553
  def admin
462
- session[:admin]
554
+ @admin ||= session[:admin]
463
555
  end
464
556
 
465
557
  # Append a message to the progress stream file,
@@ -472,8 +564,8 @@ module Xolo
472
564
  #
473
565
  # @return [void]
474
566
  ###################
475
- def progress(msg, log: :debug)
476
- server_app_instance.progress msg, log: log
567
+ def progress(msg, log: :debug, alert: false)
568
+ server_app_instance.progress msg, log: log, alert: alert
477
569
  end
478
570
 
479
571
  # This might have been set already if we were instantiated via our title
@@ -549,6 +641,10 @@ module Xolo
549
641
  lock
550
642
  @current_action = :creating
551
643
 
644
+ # NOTE: this for later in this method,
645
+ # since uploads are processed in separate threads
646
+ pkg_upload_given = !pkg_to_upload.pix_empty?
647
+
552
648
  self.creation_date = Time.now
553
649
  self.created_by = admin
554
650
  self.status = STATUS_PENDING
@@ -559,7 +655,7 @@ module Xolo
559
655
  progress 'Saving version data to Xolo server'
560
656
  save_local_data
561
657
 
562
- create_patch_in_ted
658
+ create_patch_in_ted unless subscribed?
563
659
 
564
660
  create_in_jamf
565
661
 
@@ -576,10 +672,71 @@ module Xolo
576
672
  log_change msg: 'Version Created'
577
673
 
578
674
  progress "Version '#{version}' of Title '#{title}' has been created in Xolo.", log: :info
675
+
676
+ # all done unless we need to get a pkg via autopkg
677
+ # pkg upload from xadm will happen in a separate process,
678
+ # so we don't want to do it here in the create method
679
+
680
+ # do we have an uploaded pkg?
681
+ if pkg_upload_given
682
+
683
+ progress "Pkg will be uploaded to xolo via xadm shortly, from path '#{pkg_to_upload}'", log: :info
684
+
685
+ # if we have an autopkg recipe and dir, get the .pkg and upload it to Jamf
686
+ elsif title_object.autopkg_enabled?
687
+ handle_autopkg_during_create
688
+
689
+ # otherwise tell someone we need a .pkg
690
+ else
691
+ msg = "No --pkg-to-upload given for version '#{version}' of title #{title}, and no autopkg recipe enabled. Please upload a pkg via xadm or enable autopkg for this title."
692
+
693
+ # no alert when subscribed because a better alert mesg is sent from add_version_via_subscription
694
+ subscribed? ? progress(msg, log: :warn) : progress(msg, log: :warn, alert: true)
695
+ end
696
+
697
+ server_app_instance.update_client_data
579
698
  ensure
580
699
  unlock
581
700
  end
582
701
 
702
+ # Do autopkg stuff during creation
703
+ #
704
+ ############################
705
+ def handle_autopkg_during_create
706
+ return unless title_object.autopkg_enabled?
707
+
708
+ pkg_src = title_object.run_autopkg_recipe
709
+
710
+ if pkg_src.nil?
711
+ msg = 'AutoPkg recipe is enabled for this title, but no pkg was found after running the recipe. Please check the AutoPkg recipe and the server log for details.'
712
+ progress msg, log: :warn, alert: true
713
+ return
714
+ end
715
+
716
+ oldest_allowed = Time.now - 1200 # 20 minutes ago
717
+ if pkg_src.mtime < oldest_allowed
718
+ msg = "AutoPkg recipe is enabled for this title, and a pkg was found after running the recipe, but it was last modified at #{pkg_src.mtime}, which is more than 20 minutes ago. To avoid accidentally uploading an old pkg, the server will not upload this pkg. Please check the AutoPkg recipe and the server log for details."
719
+ progress msg, log: :warn, alert: true
720
+ return
721
+ end
722
+
723
+ # this lets future code know that the pkg we're working with came from autopkg
724
+ self.pkg_is_from_autopkg = true
725
+
726
+ # Upload the pkg to Jamf, and associate it with this version
727
+ server_app_instance.process_and_upload_autopkg_pkg(title, self, pkg_src)
728
+ end
729
+
730
+ # Is this version part of a subscribed title?
731
+ def subscribed?
732
+ title_object.subscribed?
733
+ end
734
+
735
+ # or a managed title?
736
+ def managed?
737
+ !subscribed?
738
+ end
739
+
583
740
  # Update a this version, updating to the
584
741
  # local filesystem, Jamf Pro, and the Title Editor as needed
585
742
  #
@@ -610,6 +767,7 @@ module Xolo
610
767
  update_local_instance_values
611
768
  save_local_data
612
769
 
770
+ server_app_instance.update_client_data
613
771
  # new pkg uploads happen in a separate process
614
772
  rescue => e
615
773
  log_change msg: "ERROR: The update failed and the changes didn't all go through!\n#{e.class}: #{e.message}\nSee server log for details."
@@ -666,13 +824,20 @@ module Xolo
666
824
  progress "Fixing original upload date: #{new_date}, by: #{new_by}", log: :debug
667
825
  self.upload_date = new_date
668
826
  self.uploaded_by = new_by
669
- save_local_data
827
+
670
828
  end
829
+ save_local_data
830
+ # no need to update client data now if we're being repaired along with our title
831
+ return if title_object.current_action == :repairing
832
+
833
+ server_app_instance.update_client_data
671
834
  ensure
672
835
  unlock
673
836
  end
674
837
 
675
838
  # Release this version, possibly rolling back from a previously newer version
839
+ # This should only be called by the title. The initial 'release' action starts in the title,
840
+ # and then calls this method on the version to do the version-specific release steps.
676
841
  #
677
842
  # @param rollback [Boolean] If true, this version is being released as a rollback
678
843
  #
@@ -823,15 +988,25 @@ module Xolo
823
988
  # know the version is gone. Set this to false when the title itself
824
989
  # is being deleted and calling this method.
825
990
  #
991
+ # @param deleting_title [Boolean] Is the title itself being deleted?
992
+ #
826
993
  # @return [void]
827
994
  ##########################
828
- def delete(update_title: true)
995
+ def delete(update_title: true, deleting_title: false)
829
996
  lock
830
997
  @current_action = :deleting
831
998
 
832
- delete_patch_from_ted
833
999
  delete_version_from_jamf
834
1000
 
1001
+ # NOTE: we no longer delete the patch from the Title Editor
1002
+ # unless the whole title is being deleted, because
1003
+ # patches may be needed for reporting purposes.
1004
+ # When the title is deleted, the title's delete method
1005
+ # will delete all patches for all versions.
1006
+ # If other situations arise where we need to delete
1007
+ # ted patches individually, set deleting_title to true.
1008
+ delete_patch_from_ted if deleting_title && managed?
1009
+
835
1010
  # remove from the title's list of versions
836
1011
  progress 'Deleting version from title data on the Xolo server', log: :debug
837
1012
  title_object.remove_version(version) if update_title
@@ -842,6 +1017,7 @@ module Xolo
842
1017
  log_change msg: 'Version Deleted'
843
1018
 
844
1019
  progress "Version '#{version}' of Title '#{title}' has been deleted from Xolo.", log: :info
1020
+ server_app_instance.update_client_data
845
1021
  ensure
846
1022
  unlock
847
1023
  end
@@ -858,14 +1034,16 @@ module Xolo
858
1034
  raise Xolo::ServerError, 'Server is shutting down' if Xolo::Server.shutting_down?
859
1035
 
860
1036
  while locked?
861
- log_debug "Waiting for update lock on Version '#{version}' of title '#{title}'..." if (Time.now.to_i % 5).zero?
1037
+ if (Time.now.to_i % 5).zero?
1038
+ log_debug "Method #{caller_locations.first.label} is waiting for update lock on Version '#{version}' of title '#{title}'..."
1039
+ end
862
1040
  sleep 0.33
863
1041
  end
864
1042
  Xolo::Server.object_locks[title] ||= { versions: {} }
865
1043
 
866
1044
  exp = Time.now + Xolo::Server::ObjectLocks::OBJECT_LOCK_LIMIT
867
1045
  Xolo::Server.object_locks[title][:versions][version] = exp
868
- log_debug "Locked version '#{version}' of title '#{title}' for updates until #{exp}"
1046
+ log_debug "Locked version '#{version}' of title '#{title}' for updates until #{exp}, by method #{caller_locations.first.label}"
869
1047
  end
870
1048
 
871
1049
  # Unlock this version for updates
data/lib/xolo/server.rb CHANGED
@@ -27,6 +27,7 @@ require 'base64'
27
27
  require 'resolv'
28
28
  require 'shellwords'
29
29
  require 'net/smtp'
30
+ require 'etc'
30
31
 
31
32
  # Gems
32
33
  ######
@@ -42,6 +43,10 @@ require 'concurrent/atomic/reentrant_read_write_lock'
42
43
  require 'concurrent/executor/thread_pool_executor'
43
44
  require 'concurrent/timer_task'
44
45
 
46
+ # Temp fixes or overrides, esp for ruby-jss or windoo
47
+ #############
48
+ require 'xolo/server/tmp_overrides'
49
+
45
50
  # Xolo Server - order matters
46
51
  ######
47
52
  require 'xolo/server/constants'
@@ -102,6 +107,11 @@ module Xolo
102
107
  ##############################
103
108
  ##############################
104
109
 
110
+ ################
111
+ def self.test_server?
112
+ Xolo::Server.config.test_server
113
+ end
114
+
105
115
  ################
106
116
  def self.start_time
107
117
  @start_time
@@ -194,6 +204,8 @@ require 'xolo/server/helpers/versions'
194
204
  require 'xolo/server/helpers/client_data'
195
205
  require 'xolo/server/helpers/file_transfers'
196
206
  require 'xolo/server/helpers/maintenance'
207
+ require 'xolo/server/helpers/subscriptions'
208
+ require 'xolo/server/helpers/autopkg'
197
209
 
198
210
  require 'xolo/server/configuration'
199
211
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xolo-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Lasell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-03 00:00:00.000000000 Z
11
+ date: 2026-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -152,14 +152,8 @@ dependencies:
152
152
  version: '1.0'
153
153
  description: |
154
154
  == Xolo
155
- Xolo (sorta pronounced 'show-low') is an HTTPS server and set of command-line tools for macOS that provide automatable access to the software deployment and patch management aspects of {Jamf Pro}[https://www.jamf.com/products/jamf-pro/] and the {Jamf Title Editor}[https://learn.jamf.com/en-US/bundle/title-editor/page/About_Title_Editor.html]. It enhances Jamf Pro's abilities in many ways:
155
+ Xolo (sorta pronounced 'show-low') is an HTTPS server and set of command-line tools for macOS that provide automatable access to the software deployment and patch management aspects of {Jamf Pro}[https://www.jamf.com/products/jamf-pro/] and the {Jamf Title Editor}[https://learn.jamf.com/en-US/bundle/title-editor/page/About_Title_Editor.html]. It enhances Jamf Pro's abilities in many ways.
156
156
 
157
- * Management of titles and versions/patches is scriptable and automatable, allowing developers and admins to integrate with CI/CD workflows.
158
- * Simplifies and standardizes the complex, multistep manual process of managing titles and patches using the Title Editor and Patch Management web interfaces.
159
- * Client installs can be performed by remotely via ssh and/or MDM
160
- * Automated pre-release piloting of new versions/patches
161
- * Titles can be expired (auto-uninstalled) after a period of disuse, reclaiming unused licenses.
162
- * And more!
163
157
 
164
158
  The xolo-server gem packages the code needed to run `xoloserver`, the sinatra-based HTTPS server at the heart of Xolo.
165
159
  email: xolo@pixar.com
@@ -188,6 +182,7 @@ files:
188
182
  - lib/xolo/core/json_wrappers.rb
189
183
  - lib/xolo/core/loading.rb
190
184
  - lib/xolo/core/output.rb
185
+ - lib/xolo/core/security_cmd.rb
191
186
  - lib/xolo/core/version.rb
192
187
  - lib/xolo/server.rb
193
188
  - lib/xolo/server/app.rb
@@ -195,6 +190,7 @@ files:
195
190
  - lib/xolo/server/configuration.rb
196
191
  - lib/xolo/server/constants.rb
197
192
  - lib/xolo/server/helpers/auth.rb
193
+ - lib/xolo/server/helpers/autopkg.rb
198
194
  - lib/xolo/server/helpers/client_data.rb
199
195
  - lib/xolo/server/helpers/file_transfers.rb
200
196
  - lib/xolo/server/helpers/jamf_pro.rb
@@ -203,6 +199,7 @@ files:
203
199
  - lib/xolo/server/helpers/notification.rb
204
200
  - lib/xolo/server/helpers/pkg_signing.rb
205
201
  - lib/xolo/server/helpers/progress_streaming.rb
202
+ - lib/xolo/server/helpers/subscriptions.rb
206
203
  - lib/xolo/server/helpers/title_editor.rb
207
204
  - lib/xolo/server/helpers/titles.rb
208
205
  - lib/xolo/server/helpers/versions.rb
@@ -217,11 +214,13 @@ files:
217
214
  - lib/xolo/server/routes/auth.rb
218
215
  - lib/xolo/server/routes/jamf_pro.rb
219
216
  - lib/xolo/server/routes/maint.rb
217
+ - lib/xolo/server/routes/subscriptions.rb
220
218
  - lib/xolo/server/routes/title_editor.rb
221
219
  - lib/xolo/server/routes/titles.rb
222
220
  - lib/xolo/server/routes/uploads.rb
223
221
  - lib/xolo/server/routes/versions.rb
224
222
  - lib/xolo/server/title.rb
223
+ - lib/xolo/server/tmp_overrides.rb
225
224
  - lib/xolo/server/version.rb
226
225
  homepage: https://pixaranimationstudios.github.io/xolo-home/
227
226
  licenses: