├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .sbtopts ├── .scalafmt.conf ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assets ├── README.md ├── icons │ ├── stasis.logo.1024.png │ ├── stasis.logo.128.png │ ├── stasis.logo.16.png │ ├── stasis.logo.256.png │ ├── stasis.logo.32.png │ ├── stasis.logo.512.png │ └── stasis.logo.64.png ├── identity.logo.svg ├── launchers │ ├── stasis.logo.144.png │ ├── stasis.logo.192.png │ ├── stasis.logo.48.png │ ├── stasis.logo.72.png │ ├── stasis.logo.96.png │ ├── stasis.logo.round.144.png │ ├── stasis.logo.round.192.png │ ├── stasis.logo.round.48.png │ ├── stasis.logo.round.72.png │ ├── stasis.logo.round.96.png │ └── stasis.logo.xml ├── refresh_assets.py ├── screenshots │ ├── client_android_screenshot_1.png │ ├── client_android_screenshot_2.png │ ├── client_android_screenshot_3.png │ ├── client_android_screenshot_4.png │ ├── client_android_screenshot_5.png │ ├── client_android_screenshot_6.png │ ├── client_ui_screenshot_1.png │ ├── client_ui_screenshot_2.png │ ├── client_ui_screenshot_3.png │ └── client_ui_screenshot_4.png └── stasis.logo.svg ├── build.sbt ├── client-android ├── .gitignore ├── README.md ├── app │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── stasis │ │ │ └── client_android │ │ │ ├── Fixtures.kt │ │ │ ├── api │ │ │ ├── DatasetsViewModelSpec.kt │ │ │ ├── DeviceStatusViewModelSpec.kt │ │ │ └── UserStatusViewModelSpec.kt │ │ │ ├── eventually.kt │ │ │ ├── extensions.kt │ │ │ ├── mocks │ │ │ ├── Generators.kt │ │ │ ├── MockBackupTracker.kt │ │ │ ├── MockCommandProcessor.kt │ │ │ ├── MockCredentialsManagementBridge.kt │ │ │ ├── MockOAuthClient.kt │ │ │ ├── MockOperationExecutor.kt │ │ │ ├── MockRecoveryTracker.kt │ │ │ ├── MockSearch.kt │ │ │ ├── MockServerApiEndpointClient.kt │ │ │ ├── MockServerCoreEndpointClient.kt │ │ │ ├── MockServerMonitor.kt │ │ │ └── MockServerTracker.kt │ │ │ ├── persistence │ │ │ ├── config │ │ │ │ ├── ConfigRepositorySpec.kt │ │ │ │ └── ConfigViewModelSpec.kt │ │ │ ├── credentials │ │ │ │ ├── CredentialsRepositorySpec.kt │ │ │ │ └── CredentialsViewModelSpec.kt │ │ │ ├── rules │ │ │ │ ├── RuleEntityDaoSpec.kt │ │ │ │ ├── RuleEntityDatabaseSpec.kt │ │ │ │ ├── RuleRepositorySpec.kt │ │ │ │ └── RuleViewModelSpec.kt │ │ │ └── schedules │ │ │ │ ├── ActiveScheduleEntityDaoSpec.kt │ │ │ │ ├── ActiveScheduleEntityDatabaseSpec.kt │ │ │ │ ├── ActiveScheduleRepositorySpec.kt │ │ │ │ ├── ActiveScheduleViewModelSpec.kt │ │ │ │ ├── LocalScheduleEntityDaoSpec.kt │ │ │ │ ├── LocalScheduleEntityDatabaseSpec.kt │ │ │ │ ├── LocalScheduleRepositorySpec.kt │ │ │ │ └── LocalScheduleViewModelSpec.kt │ │ │ ├── tracking │ │ │ ├── DefaultBackupsTrackerSpec.kt │ │ │ ├── DefaultRecoveryTrackerSpec.kt │ │ │ └── DefaultServerTrackerSpec.kt │ │ │ └── utils │ │ │ ├── DynamicArgumentsSpec.kt │ │ │ └── LiveDataExtensionsSpec.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── stasis │ │ │ │ └── client_android │ │ │ │ ├── ServiceComponent.kt │ │ │ │ ├── StasisClientApplication.kt │ │ │ │ ├── StasisClientDependencies.kt │ │ │ │ ├── activities │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── WelcomeActivity.kt │ │ │ │ ├── fragments │ │ │ │ │ ├── AboutFragment.kt │ │ │ │ │ ├── HomeFragment.kt │ │ │ │ │ ├── SettingsFragment.kt │ │ │ │ │ ├── WelcomeFragment.kt │ │ │ │ │ ├── backup │ │ │ │ │ │ ├── DatasetDefinitionDetailsFragment.kt │ │ │ │ │ │ ├── DatasetDefinitionFormFragment.kt │ │ │ │ │ │ ├── DatasetDefinitionListFragment.kt │ │ │ │ │ │ ├── DatasetDefinitionListItemAdapter.kt │ │ │ │ │ │ ├── DatasetEntryDetailsFragment.kt │ │ │ │ │ │ ├── DatasetEntryListItemAdapter.kt │ │ │ │ │ │ └── DatasetMetadataEntryListItemAdapter.kt │ │ │ │ │ ├── bootstrap │ │ │ │ │ │ ├── BootstrapIntroFragment.kt │ │ │ │ │ │ ├── BootstrapProvideCodeFragment.kt │ │ │ │ │ │ ├── BootstrapProvidePasswordFragment.kt │ │ │ │ │ │ ├── BootstrapProvideSecretFragment.kt │ │ │ │ │ │ ├── BootstrapProvideServerFragment.kt │ │ │ │ │ │ └── BootstrapProvideUsernameFragment.kt │ │ │ │ │ ├── login │ │ │ │ │ │ ├── LoginFragment.kt │ │ │ │ │ │ ├── MoreOptionsDialogFragment.kt │ │ │ │ │ │ └── MoreOptionsReEncryptDialogFragment.kt │ │ │ │ │ ├── operations │ │ │ │ │ │ ├── OperationDetailsFragment.kt │ │ │ │ │ │ ├── OperationFailureListItemAdapter.kt │ │ │ │ │ │ ├── OperationListItemAdapter.kt │ │ │ │ │ │ ├── OperationStageListItemAdapter.kt │ │ │ │ │ │ ├── OperationStageStepsDialogFragment.kt │ │ │ │ │ │ └── OperationsFragment.kt │ │ │ │ │ ├── recover │ │ │ │ │ │ └── RecoverFragment.kt │ │ │ │ │ ├── rules │ │ │ │ │ │ ├── DefinitionRulesFragment.kt │ │ │ │ │ │ ├── RuleFormDialogFragment.kt │ │ │ │ │ │ ├── RuleSuggestion.kt │ │ │ │ │ │ ├── RuleSuggestionListItemAdapter.kt │ │ │ │ │ │ ├── RuleTreeDialogFragment.kt │ │ │ │ │ │ ├── RuleTreeEntryContextDialogFragment.kt │ │ │ │ │ │ ├── RulesFragment.kt │ │ │ │ │ │ └── RulesListItemAdapter.kt │ │ │ │ │ ├── schedules │ │ │ │ │ │ ├── ActiveScheduleListItemAdapter.kt │ │ │ │ │ │ ├── LocalScheduleFormDialogFragment.kt │ │ │ │ │ │ ├── NewScheduleAssignmentDialogFragment.kt │ │ │ │ │ │ ├── ScheduleListItemAdapter.kt │ │ │ │ │ │ └── SchedulesFragment.kt │ │ │ │ │ ├── search │ │ │ │ │ │ ├── SearchFragment.kt │ │ │ │ │ │ ├── SearchResultListItemAdapter.kt │ │ │ │ │ │ └── SearchResultMatchListItemAdapter.kt │ │ │ │ │ ├── settings │ │ │ │ │ │ ├── CommandsDialogFragment.kt │ │ │ │ │ │ ├── ExportDialogFragment.kt │ │ │ │ │ │ ├── ImportDialogFragment.kt │ │ │ │ │ │ ├── PullDialogFragment.kt │ │ │ │ │ │ ├── PushDialogFragment.kt │ │ │ │ │ │ ├── UpdatePasswordFragment.kt │ │ │ │ │ │ └── UpdateSaltFragment.kt │ │ │ │ │ └── status │ │ │ │ │ │ ├── ConnectionsFragment.kt │ │ │ │ │ │ ├── DeviceDetailsFragment.kt │ │ │ │ │ │ ├── ServerListItemAdapter.kt │ │ │ │ │ │ ├── StatusEntryListItemAdapter.kt │ │ │ │ │ │ ├── StatusFragment.kt │ │ │ │ │ │ └── UserDetailsFragment.kt │ │ │ │ ├── helpers │ │ │ │ │ ├── Backups.kt │ │ │ │ │ ├── Common.kt │ │ │ │ │ ├── DateTimeExtensions.kt │ │ │ │ │ ├── RecoveryConfig.kt │ │ │ │ │ ├── TextInputExtensions.kt │ │ │ │ │ └── Transitions.kt │ │ │ │ ├── receivers │ │ │ │ │ ├── LogoutReceiver.kt │ │ │ │ │ ├── SystemConfigurationChangedReceiver.kt │ │ │ │ │ └── SystemStartedReceiver.kt │ │ │ │ └── views │ │ │ │ │ ├── ExpandingListView.kt │ │ │ │ │ ├── context │ │ │ │ │ ├── EntryAction.kt │ │ │ │ │ ├── EntryActionsContextDialogFragment.kt │ │ │ │ │ └── EntryActionsListItemAdapter.kt │ │ │ │ │ ├── dialogs │ │ │ │ │ ├── ConfirmationDialogFragment.kt │ │ │ │ │ └── InformationDialogFragment.kt │ │ │ │ │ └── tree │ │ │ │ │ └── FileTreeNode.kt │ │ │ │ ├── api │ │ │ │ ├── DatasetsViewModel.kt │ │ │ │ ├── DeviceStatusViewModel.kt │ │ │ │ ├── UserStatusViewModel.kt │ │ │ │ └── clients │ │ │ │ │ ├── MockConfig.kt │ │ │ │ │ ├── MockOAuthClient.kt │ │ │ │ │ ├── MockServerApiEndpointClient.kt │ │ │ │ │ ├── MockServerBootstrapEndpointClient.kt │ │ │ │ │ ├── MockServerCoreEndpointClient.kt │ │ │ │ │ └── MockServiceDiscoveryClient.kt │ │ │ │ ├── persistence │ │ │ │ ├── Converters.kt │ │ │ │ ├── cache │ │ │ │ │ ├── DatasetEntryCacheFileSerdes.kt │ │ │ │ │ └── DatasetMetadataCacheFileSerdes.kt │ │ │ │ ├── config │ │ │ │ │ ├── Config.kt │ │ │ │ │ ├── ConfigRepository.kt │ │ │ │ │ └── ConfigViewModel.kt │ │ │ │ ├── credentials │ │ │ │ │ ├── CredentialsRepository.kt │ │ │ │ │ └── CredentialsViewModel.kt │ │ │ │ ├── rules │ │ │ │ │ ├── RuleEntity.kt │ │ │ │ │ ├── RuleEntityDao.kt │ │ │ │ │ ├── RuleEntityDatabase.kt │ │ │ │ │ ├── RuleRepository.kt │ │ │ │ │ ├── RuleViewModel.kt │ │ │ │ │ └── RulesConfig.kt │ │ │ │ └── schedules │ │ │ │ │ ├── ActiveScheduleEntity.kt │ │ │ │ │ ├── ActiveScheduleEntityDao.kt │ │ │ │ │ ├── ActiveScheduleEntityDatabase.kt │ │ │ │ │ ├── ActiveScheduleRepository.kt │ │ │ │ │ ├── ActiveScheduleViewModel.kt │ │ │ │ │ ├── LocalScheduleEntity.kt │ │ │ │ │ ├── LocalScheduleEntityDao.kt │ │ │ │ │ ├── LocalScheduleEntityDatabase.kt │ │ │ │ │ ├── LocalScheduleRepository.kt │ │ │ │ │ └── LocalScheduleViewModel.kt │ │ │ │ ├── providers │ │ │ │ └── ProviderContext.kt │ │ │ │ ├── scheduling │ │ │ │ ├── AlarmManagerExtensions.kt │ │ │ │ ├── SchedulerService.kt │ │ │ │ └── Schedules.kt │ │ │ │ ├── security │ │ │ │ ├── DefaultCredentialsManagementBridge.kt │ │ │ │ └── Secrets.kt │ │ │ │ ├── serialization │ │ │ │ ├── ByteStrings.kt │ │ │ │ └── Extras.kt │ │ │ │ ├── settings │ │ │ │ └── Settings.kt │ │ │ │ ├── tracking │ │ │ │ ├── BackupTrackerManage.kt │ │ │ │ ├── BackupTrackerView.kt │ │ │ │ ├── DefaultBackupTracker.kt │ │ │ │ ├── DefaultRecoveryTracker.kt │ │ │ │ ├── DefaultServerTracker.kt │ │ │ │ ├── DefaultTrackers.kt │ │ │ │ ├── RecoveryTrackerManage.kt │ │ │ │ ├── RecoveryTrackerView.kt │ │ │ │ ├── ServerTrackerView.kt │ │ │ │ └── TrackerViews.kt │ │ │ │ └── utils │ │ │ │ ├── DynamicArguments.kt │ │ │ │ ├── LiveDataExtensions.kt │ │ │ │ ├── NotificationManagerExtensions.kt │ │ │ │ └── Permissions.kt │ │ └── res │ │ │ ├── drawable │ │ │ ├── dot_selector.xml │ │ │ ├── ic_about.xml │ │ │ ├── ic_action_delete.xml │ │ │ ├── ic_action_details.xml │ │ │ ├── ic_action_edit.xml │ │ │ ├── ic_actions_expand.xml │ │ │ ├── ic_add.xml │ │ │ ├── ic_backup.xml │ │ │ ├── ic_check.xml │ │ │ ├── ic_close.xml │ │ │ ├── ic_commands.xml │ │ │ ├── ic_debug.xml │ │ │ ├── ic_dot_default.xml │ │ │ ├── ic_dot_selected.xml │ │ │ ├── ic_entity_state_existing.xml │ │ │ ├── ic_entity_state_new.xml │ │ │ ├── ic_entity_state_updated.xml │ │ │ ├── ic_export.xml │ │ │ ├── ic_filter_off.xml │ │ │ ├── ic_filter_on.xml │ │ │ ├── ic_home.xml │ │ │ ├── ic_import.xml │ │ │ ├── ic_info.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── ic_logout.xml │ │ │ ├── ic_menu.xml │ │ │ ├── ic_mock.xml │ │ │ ├── ic_operations.xml │ │ │ ├── ic_play.xml │ │ │ ├── ic_pull.xml │ │ │ ├── ic_push.xml │ │ │ ├── ic_recover.xml │ │ │ ├── ic_reset.xml │ │ │ ├── ic_restrictions.xml │ │ │ ├── ic_rule_operation_exclude.xml │ │ │ ├── ic_rule_operation_include.xml │ │ │ ├── ic_rules.xml │ │ │ ├── ic_schedules.xml │ │ │ ├── ic_search.xml │ │ │ ├── ic_settings.xml │ │ │ ├── ic_status.xml │ │ │ ├── ic_status_collapse.xml │ │ │ ├── ic_status_expand.xml │ │ │ ├── ic_stop.xml │ │ │ ├── ic_tree.xml │ │ │ ├── ic_tree_arrow_down.xml │ │ │ ├── ic_tree_arrow_right.xml │ │ │ ├── ic_tree_directory.xml │ │ │ ├── ic_tree_file.xml │ │ │ ├── ic_update_password.xml │ │ │ ├── ic_update_salt.xml │ │ │ └── ic_warning.xml │ │ │ ├── layout-land │ │ │ ├── fragment_dataset_definition_details.xml │ │ │ ├── fragment_device_details.xml │ │ │ ├── fragment_home.xml │ │ │ ├── fragment_login.xml │ │ │ ├── fragment_operation_details.xml │ │ │ ├── fragment_search.xml │ │ │ ├── fragment_user_details.xml │ │ │ ├── list_item_dataset_definition_details.xml │ │ │ └── list_item_operation.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ ├── activity_welcome.xml │ │ │ ├── context_dialog_entry_actions.xml │ │ │ ├── context_dialog_rule_tree_entry.xml │ │ │ ├── dialog_commands.xml │ │ │ ├── dialog_device_secret_export.xml │ │ │ ├── dialog_device_secret_import.xml │ │ │ ├── dialog_device_secret_pull.xml │ │ │ ├── dialog_device_secret_push.xml │ │ │ ├── dialog_local_schedule_form.xml │ │ │ ├── dialog_login_more_options.xml │ │ │ ├── dialog_login_more_options_reencrypt.xml │ │ │ ├── dialog_new_schedule_assignment.xml │ │ │ ├── dialog_operation_stage_steps.xml │ │ │ ├── dialog_rule_form.xml │ │ │ ├── dialog_rules_tree.xml │ │ │ ├── dialog_user_credentials_update_password.xml │ │ │ ├── dialog_user_credentials_update_salt.xml │ │ │ ├── dropdown_duration_type_item.xml │ │ │ ├── dropdown_policy_type_item.xml │ │ │ ├── dropdown_schedule_assignment_definition.xml │ │ │ ├── dropdown_schedule_assignment_type_item.xml │ │ │ ├── fragment_about.xml │ │ │ ├── fragment_bootstrap_intro.xml │ │ │ ├── fragment_bootstrap_provide_code.xml │ │ │ ├── fragment_bootstrap_provide_password.xml │ │ │ ├── fragment_bootstrap_provide_secret.xml │ │ │ ├── fragment_bootstrap_provide_server.xml │ │ │ ├── fragment_bootstrap_provide_username.xml │ │ │ ├── fragment_connections.xml │ │ │ ├── fragment_dataset_definition_details.xml │ │ │ ├── fragment_dataset_definition_form.xml │ │ │ ├── fragment_dataset_definition_list.xml │ │ │ ├── fragment_dataset_entry_details.xml │ │ │ ├── fragment_definition_rules.xml │ │ │ ├── fragment_device_details.xml │ │ │ ├── fragment_home.xml │ │ │ ├── fragment_login.xml │ │ │ ├── fragment_operation_details.xml │ │ │ ├── fragment_operations.xml │ │ │ ├── fragment_recover.xml │ │ │ ├── fragment_rules.xml │ │ │ ├── fragment_schedules.xml │ │ │ ├── fragment_search.xml │ │ │ ├── fragment_status.xml │ │ │ ├── fragment_user_details.xml │ │ │ ├── fragment_welcome.xml │ │ │ ├── input_dataset_definition_retention.xml │ │ │ ├── input_duration.xml │ │ │ ├── input_timestamp.xml │ │ │ ├── layout_dataset_metadata_entry_details_row.xml │ │ │ ├── layout_limits_row.xml │ │ │ ├── list_item_command.xml │ │ │ ├── list_item_dataset_definition_details.xml │ │ │ ├── list_item_dataset_definition_summary.xml │ │ │ ├── list_item_dataset_entry.xml │ │ │ ├── list_item_dataset_entry_summary.xml │ │ │ ├── list_item_dataset_metadata_entry.xml │ │ │ ├── list_item_entry_action.xml │ │ │ ├── list_item_operation.xml │ │ │ ├── list_item_operation_failure.xml │ │ │ ├── list_item_operation_stage.xml │ │ │ ├── list_item_operation_stage_step.xml │ │ │ ├── list_item_permission_chip.xml │ │ │ ├── list_item_rule.xml │ │ │ ├── list_item_rule_suggestion.xml │ │ │ ├── list_item_rules_tree.xml │ │ │ ├── list_item_schedule.xml │ │ │ ├── list_item_schedule_assignment.xml │ │ │ ├── list_item_search_result.xml │ │ │ ├── list_item_search_result_match.xml │ │ │ ├── list_item_server_connection.xml │ │ │ ├── list_item_status_entry.xml │ │ │ └── navigation_drawer_header.xml │ │ │ ├── menu │ │ │ ├── main_top_bar.xml │ │ │ └── navigation_drawer.xml │ │ │ ├── mipmap-anydpi │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── navigation │ │ │ ├── main_nav_graph.xml │ │ │ └── welcome_nav_graph.xml │ │ │ ├── values-night │ │ │ └── themes.xml │ │ │ ├── values │ │ │ ├── arrays.xml │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ │ └── xml │ │ │ ├── network_security_config.xml │ │ │ └── preferences.xml │ │ └── test │ │ └── java │ │ └── stasis │ │ └── test │ │ └── client_android │ │ ├── Fixtures.kt │ │ ├── activities │ │ ├── helpers │ │ │ ├── CommonSpec.kt │ │ │ ├── DateTimeExtensionsSpec.kt │ │ │ ├── RecoveryConfigSpec.kt │ │ │ └── TransitionsSpec.kt │ │ └── receivers │ │ │ ├── LogoutReceiverSpec.kt │ │ │ ├── SystemConfigurationChangedReceiverSpec.kt │ │ │ └── SystemStartedReceiverSpec.kt │ │ ├── extensions.kt │ │ ├── mocks │ │ └── MockServerApiEndpointClient.kt │ │ ├── persistence │ │ ├── ConvertersSpec.kt │ │ └── cache │ │ │ ├── DatasetEntryCacheFileSerdesSpec.kt │ │ │ └── DatasetMetadataCacheFileSerdesSpec.kt │ │ ├── scheduling │ │ ├── AlarmManagerExtensionsSpec.kt │ │ └── SchedulerServiceSpec.kt │ │ ├── security │ │ ├── DefaultCredentialsManagementBridgeSpec.kt │ │ └── SecretsSpec.kt │ │ ├── serialization │ │ ├── ByteStringsSpec.kt │ │ └── ExtrasSpec.kt │ │ ├── settings │ │ └── SettingsSpec.kt │ │ └── utils │ │ └── NotificationManagerExtensionsSpec.kt ├── build.gradle.kts ├── config │ └── detekt │ │ └── detekt.yml ├── dev │ └── get_bootstrap_code.sh ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ ├── kotlin │ │ │ └── stasis │ │ │ │ └── client_android │ │ │ │ └── lib │ │ │ │ ├── analysis │ │ │ │ ├── Checksum.kt │ │ │ │ └── Metadata.kt │ │ │ │ ├── api │ │ │ │ └── clients │ │ │ │ │ ├── CachedServerApiEndpointClient.kt │ │ │ │ │ ├── Clients.kt │ │ │ │ │ ├── DefaultServerApiEndpointClient.kt │ │ │ │ │ ├── DefaultServerBootstrapEndpointClient.kt │ │ │ │ │ ├── DefaultServerCoreEndpointClient.kt │ │ │ │ │ ├── ServerApiEndpointClient.kt │ │ │ │ │ ├── ServerBootstrapEndpointClient.kt │ │ │ │ │ ├── ServerCoreEndpointClient.kt │ │ │ │ │ ├── ServiceApiClientFactory.kt │ │ │ │ │ ├── exceptions │ │ │ │ │ ├── AccessDeniedFailure.kt │ │ │ │ │ ├── EndpointFailure.kt │ │ │ │ │ ├── InvalidBootstrapCodeFailure.kt │ │ │ │ │ └── ResourceMissingFailure.kt │ │ │ │ │ └── internal │ │ │ │ │ ├── Adapters.kt │ │ │ │ │ └── ClientExtensions.kt │ │ │ │ ├── collection │ │ │ │ ├── BackupCollector.kt │ │ │ │ ├── BackupMetadataCollector.kt │ │ │ │ ├── DefaultBackupCollector.kt │ │ │ │ ├── DefaultRecoveryCollector.kt │ │ │ │ ├── RecoveryCollector.kt │ │ │ │ ├── RecoveryMetadataCollector.kt │ │ │ │ └── rules │ │ │ │ │ ├── Rule.kt │ │ │ │ │ ├── Specification.kt │ │ │ │ │ ├── exceptions │ │ │ │ │ └── RuleMatchingFailure.kt │ │ │ │ │ └── internal │ │ │ │ │ └── FilesWalker.kt │ │ │ │ ├── compression │ │ │ │ ├── Compression.kt │ │ │ │ ├── Compressor.kt │ │ │ │ ├── Decoder.kt │ │ │ │ ├── Deflate.kt │ │ │ │ ├── Encoder.kt │ │ │ │ ├── Gzip.kt │ │ │ │ ├── Identity.kt │ │ │ │ └── internal │ │ │ │ │ └── BaseCompressor.kt │ │ │ │ ├── discovery │ │ │ │ ├── ClientDiscoveryAttributes.kt │ │ │ │ ├── ServiceApiClient.kt │ │ │ │ ├── ServiceApiEndpoint.kt │ │ │ │ ├── ServiceDiscoveryClient.kt │ │ │ │ ├── ServiceDiscoveryRequest.kt │ │ │ │ ├── ServiceDiscoveryResult.kt │ │ │ │ ├── exceptions │ │ │ │ │ └── DiscoveryFailure.kt │ │ │ │ ├── http │ │ │ │ │ └── HttpServiceDiscoveryClient.kt │ │ │ │ └── providers │ │ │ │ │ └── client │ │ │ │ │ └── ServiceDiscoveryProvider.kt │ │ │ │ ├── encryption │ │ │ │ ├── Aes.kt │ │ │ │ ├── Decoder.kt │ │ │ │ ├── Encoder.kt │ │ │ │ ├── secrets │ │ │ │ │ ├── DeviceFileSecret.kt │ │ │ │ │ ├── DeviceMetadataSecret.kt │ │ │ │ │ ├── DeviceSecret.kt │ │ │ │ │ ├── Secret.kt │ │ │ │ │ ├── UserAuthenticationPassword.kt │ │ │ │ │ ├── UserHashedEncryptionPassword.kt │ │ │ │ │ ├── UserKeyStoreEncryptionSecret.kt │ │ │ │ │ ├── UserLocalEncryptionSecret.kt │ │ │ │ │ └── UserPassword.kt │ │ │ │ └── stream │ │ │ │ │ ├── CipherSink.kt │ │ │ │ │ ├── CipherSource.kt │ │ │ │ │ └── CipherTransformation.kt │ │ │ │ ├── model │ │ │ │ ├── DatasetMetadata.kt │ │ │ │ ├── EntityMetadata.kt │ │ │ │ ├── FilesystemMetadata.kt │ │ │ │ ├── SourceEntity.kt │ │ │ │ ├── TargetEntity.kt │ │ │ │ ├── core │ │ │ │ │ ├── CrateStorageRequest.kt │ │ │ │ │ ├── CrateStorageReservation.kt │ │ │ │ │ ├── Identifiers.kt │ │ │ │ │ ├── Manifest.kt │ │ │ │ │ └── networking │ │ │ │ │ │ └── EndpointAddress.kt │ │ │ │ └── server │ │ │ │ │ ├── api │ │ │ │ │ ├── requests │ │ │ │ │ │ ├── CreateDatasetDefinition.kt │ │ │ │ │ │ ├── CreateDatasetEntry.kt │ │ │ │ │ │ ├── ResetUserPassword.kt │ │ │ │ │ │ └── UpdateDatasetDefinition.kt │ │ │ │ │ └── responses │ │ │ │ │ │ ├── CommandAsJson.kt │ │ │ │ │ │ ├── CreatedDatasetDefinition.kt │ │ │ │ │ │ ├── CreatedDatasetEntry.kt │ │ │ │ │ │ ├── Ping.kt │ │ │ │ │ │ └── UpdatedUserSalt.kt │ │ │ │ │ ├── datasets │ │ │ │ │ ├── DatasetDefinition.kt │ │ │ │ │ └── DatasetEntry.kt │ │ │ │ │ ├── devices │ │ │ │ │ ├── Device.kt │ │ │ │ │ └── DeviceBootstrapParameters.kt │ │ │ │ │ ├── schedules │ │ │ │ │ └── Schedule.kt │ │ │ │ │ └── users │ │ │ │ │ └── User.kt │ │ │ │ ├── ops │ │ │ │ ├── Operation.kt │ │ │ │ ├── backup │ │ │ │ │ ├── Backup.kt │ │ │ │ │ ├── Providers.kt │ │ │ │ │ └── stages │ │ │ │ │ │ ├── EntityCollection.kt │ │ │ │ │ │ ├── EntityDiscovery.kt │ │ │ │ │ │ ├── EntityProcessing.kt │ │ │ │ │ │ ├── MetadataCollection.kt │ │ │ │ │ │ ├── MetadataPush.kt │ │ │ │ │ │ └── internal │ │ │ │ │ │ └── PartitionedSource.kt │ │ │ │ ├── commands │ │ │ │ │ ├── CommandProcessor.kt │ │ │ │ │ └── DefaultCommandProcessor.kt │ │ │ │ ├── exceptions │ │ │ │ │ ├── EntityProcessingFailure.kt │ │ │ │ │ └── OperationRestrictedFailure.kt │ │ │ │ ├── monitoring │ │ │ │ │ ├── DefaultServerMonitor.kt │ │ │ │ │ └── ServerMonitor.kt │ │ │ │ ├── recovery │ │ │ │ │ ├── Providers.kt │ │ │ │ │ ├── Recovery.kt │ │ │ │ │ └── stages │ │ │ │ │ │ ├── EntityCollection.kt │ │ │ │ │ │ ├── EntityProcessing.kt │ │ │ │ │ │ ├── MetadataApplication.kt │ │ │ │ │ │ └── internal │ │ │ │ │ │ ├── DecompressedSource.kt │ │ │ │ │ │ ├── DecryptedCrates.kt │ │ │ │ │ │ ├── DestagedByteStringSource.kt │ │ │ │ │ │ └── MergedCrates.kt │ │ │ │ ├── scheduling │ │ │ │ │ ├── ActiveSchedule.kt │ │ │ │ │ ├── DefaultOperationExecutor.kt │ │ │ │ │ ├── OperationExecutor.kt │ │ │ │ │ └── OperationScheduleAssignment.kt │ │ │ │ └── search │ │ │ │ │ ├── DefaultSearch.kt │ │ │ │ │ └── Search.kt │ │ │ │ ├── persistence │ │ │ │ └── state │ │ │ │ │ └── StateStore.kt │ │ │ │ ├── security │ │ │ │ ├── AccessTokenResponse.kt │ │ │ │ ├── CredentialsManagementBridge.kt │ │ │ │ ├── CredentialsProvider.kt │ │ │ │ ├── DefaultOAuthClient.kt │ │ │ │ ├── HttpCredentials.kt │ │ │ │ ├── OAuthClient.kt │ │ │ │ ├── OAuthTokenManager.kt │ │ │ │ └── exceptions │ │ │ │ │ ├── ExplicitLogout.kt │ │ │ │ │ ├── InvalidUserCredentials.kt │ │ │ │ │ ├── MissingDeviceSecret.kt │ │ │ │ │ └── TokenExpired.kt │ │ │ │ ├── staging │ │ │ │ ├── DefaultFileStaging.kt │ │ │ │ └── FileStaging.kt │ │ │ │ ├── tracking │ │ │ │ ├── BackupTracker.kt │ │ │ │ ├── RecoveryTracker.kt │ │ │ │ ├── ServerTracker.kt │ │ │ │ └── state │ │ │ │ │ ├── BackupState.kt │ │ │ │ │ ├── OperationState.kt │ │ │ │ │ ├── RecoveryState.kt │ │ │ │ │ └── serdes │ │ │ │ │ ├── BackupStateSerdes.kt │ │ │ │ │ └── RecoveryStateSerdes.kt │ │ │ │ └── utils │ │ │ │ ├── AsyncOps.kt │ │ │ │ ├── Cache.kt │ │ │ │ ├── ConcatSource.kt │ │ │ │ ├── Either.kt │ │ │ │ ├── FlatMapSource.kt │ │ │ │ ├── NonFatal.kt │ │ │ │ ├── Reference.kt │ │ │ │ └── Try.kt │ │ └── protobuf │ │ │ ├── metadata.proto │ │ │ └── state.proto │ │ └── test │ │ ├── kotlin │ │ └── stasis │ │ │ └── test │ │ │ └── client_android │ │ │ └── lib │ │ │ ├── Fixtures.kt │ │ │ ├── ResourceHelpers.kt │ │ │ ├── analysis │ │ │ ├── ChecksumSpec.kt │ │ │ └── MetadataSpec.kt │ │ │ ├── api │ │ │ └── clients │ │ │ │ ├── CachedServerApiEndpointClientSpec.kt │ │ │ │ ├── ClientsSpec.kt │ │ │ │ ├── DefaultServerApiEndpointClientSpec.kt │ │ │ │ ├── DefaultServerBootstrapEndpointClientSpec.kt │ │ │ │ ├── DefaultServerCoreEndpointClientSpec.kt │ │ │ │ ├── ServiceApiClientFactorySpec.kt │ │ │ │ └── internal │ │ │ │ ├── AdaptersSpec.kt │ │ │ │ ├── ClientExtensionsSpec.kt │ │ │ │ ├── TestClient.kt │ │ │ │ └── TestDataClass.kt │ │ │ ├── await.kt │ │ │ ├── collect.kt │ │ │ ├── collection │ │ │ ├── BackupMetadataCollectorSpec.kt │ │ │ ├── DefaultBackupCollectorSpec.kt │ │ │ ├── DefaultRecoveryCollectorSpec.kt │ │ │ ├── RecoveryMetadataCollectorSpec.kt │ │ │ └── rules │ │ │ │ ├── RuleSpec.kt │ │ │ │ ├── SpecificationSpec.kt │ │ │ │ └── internal │ │ │ │ └── FilesWalkerSpec.kt │ │ │ ├── compression │ │ │ ├── CompressionSpec.kt │ │ │ ├── DeflateSpec.kt │ │ │ ├── GzipSpec.kt │ │ │ └── IdentitySpec.kt │ │ │ ├── discovery │ │ │ ├── ClientDiscoveryAttributesSpec.kt │ │ │ ├── ServiceApiEndpointSpec.kt │ │ │ ├── ServiceDiscoveryRequestSpec.kt │ │ │ ├── ServiceDiscoveryResultSpec.kt │ │ │ ├── http │ │ │ │ └── HttpServiceDiscoveryClientSpec.kt │ │ │ └── providers │ │ │ │ └── client │ │ │ │ └── ServiceDiscoveryProviderSpec.kt │ │ │ ├── encryption │ │ │ ├── AesSpec.kt │ │ │ ├── secrets │ │ │ │ ├── DeviceFileSecretSpec.kt │ │ │ │ ├── DeviceMetadataSecretSpec.kt │ │ │ │ ├── DeviceSecretSpec.kt │ │ │ │ ├── SecretSpec.kt │ │ │ │ ├── SecretsConfig.kt │ │ │ │ ├── UserAuthenticationPasswordSpec.kt │ │ │ │ ├── UserHashedEncryptionPasswordSpec.kt │ │ │ │ ├── UserKeyStoreEncryptionSecretSpec.kt │ │ │ │ ├── UserLocalEncryptionSecretSpec.kt │ │ │ │ └── UserPasswordSpec.kt │ │ │ └── stream │ │ │ │ ├── CipherSinkSpec.kt │ │ │ │ ├── CipherSourceSpec.kt │ │ │ │ └── CipherTransformationSpec.kt │ │ │ ├── eventually.kt │ │ │ ├── mocks │ │ │ ├── MockBackupCollector.kt │ │ │ ├── MockBackupTracker.kt │ │ │ ├── MockChecksum.kt │ │ │ ├── MockCompression.kt │ │ │ ├── MockCredentialsManagementBridge.kt │ │ │ ├── MockEncryption.kt │ │ │ ├── MockFileStaging.kt │ │ │ ├── MockRecoveryCollector.kt │ │ │ ├── MockRecoveryMetadataCollector.kt │ │ │ ├── MockRecoveryTracker.kt │ │ │ ├── MockServerApiEndpointClient.kt │ │ │ ├── MockServerCoreEndpointClient.kt │ │ │ ├── MockServerTracker.kt │ │ │ └── MockServiceDiscoveryClient.kt │ │ │ ├── model │ │ │ ├── DatasetMetadataSpec.kt │ │ │ ├── EntityMetadataSpec.kt │ │ │ ├── FilesystemMetadataSpec.kt │ │ │ ├── Generators.kt │ │ │ ├── SourceEntitySpec.kt │ │ │ ├── TargetEntitySpec.kt │ │ │ └── server │ │ │ │ ├── api │ │ │ │ └── responses │ │ │ │ │ └── CommandAsJsonSpec.kt │ │ │ │ ├── datasets │ │ │ │ └── DatasetEntrySpec.kt │ │ │ │ └── schedules │ │ │ │ └── ScheduleSpec.kt │ │ │ ├── ops │ │ │ ├── OperationSpec.kt │ │ │ ├── backup │ │ │ │ ├── BackupSpec.kt │ │ │ │ └── stages │ │ │ │ │ ├── EntityCollectionSpec.kt │ │ │ │ │ ├── EntityDiscoverySpec.kt │ │ │ │ │ ├── EntityProcessingSpec.kt │ │ │ │ │ ├── MetadataCollectionSpec.kt │ │ │ │ │ ├── MetadataPushSpec.kt │ │ │ │ │ └── internal │ │ │ │ │ └── PartitionedSourceSpec.kt │ │ │ ├── commands │ │ │ │ └── DefaultCommandProcessorSpec.kt │ │ │ ├── monitoring │ │ │ │ └── DefaultServerMonitorSpec.kt │ │ │ ├── recovery │ │ │ │ ├── RecoverySpec.kt │ │ │ │ └── stages │ │ │ │ │ ├── EntityCollectionSpec.kt │ │ │ │ │ ├── EntityProcessingSpec.kt │ │ │ │ │ ├── MetadataApplicationSpec.kt │ │ │ │ │ └── internal │ │ │ │ │ ├── DecompressedSourceSpec.kt │ │ │ │ │ ├── DecryptedCratesSpec.kt │ │ │ │ │ ├── DestagedByteStringSourceSpec.kt │ │ │ │ │ └── MergedCratesSpec.kt │ │ │ ├── scheduling │ │ │ │ └── DefaultOperationExecutorSpec.kt │ │ │ └── search │ │ │ │ └── DefaultSearchSpec.kt │ │ │ ├── persistence │ │ │ └── state │ │ │ │ └── StateStoreSpec.kt │ │ │ ├── security │ │ │ ├── AccessTokenResponseSpec.kt │ │ │ ├── CredentialsProviderSpec.kt │ │ │ ├── DefaultOAuthClientSpec.kt │ │ │ ├── HttpCredentialsSpec.kt │ │ │ └── OAuthTokenManagerSpec.kt │ │ │ ├── staging │ │ │ └── DefaultFileStagingSpec.kt │ │ │ ├── tracking │ │ │ └── state │ │ │ │ ├── BackupStateSpec.kt │ │ │ │ ├── RecoveryStateSpec.kt │ │ │ │ └── serdes │ │ │ │ ├── BackupStateSerdesSpec.kt │ │ │ │ └── RecoveryStateSerdesSpec.kt │ │ │ └── utils │ │ │ ├── AsyncOpsSpec.kt │ │ │ ├── CacheSpec.kt │ │ │ ├── ConcatSourceSpec.kt │ │ │ ├── EitherSpec.kt │ │ │ ├── FlatMapSourceSpec.kt │ │ │ ├── NonFatalSpec.kt │ │ │ ├── ReferenceSpec.kt │ │ │ ├── TrySpec.kt │ │ │ └── mock │ │ │ └── MockSource.kt │ │ └── resources │ │ ├── analysis │ │ ├── digest-source-file │ │ └── metadata-source-file │ │ ├── collection │ │ ├── file-1 │ │ ├── file-2 │ │ ├── file-3 │ │ ├── other-file-1 │ │ └── other-file-2 │ │ ├── encryption │ │ ├── encrypted-file │ │ └── plaintext-file │ │ └── ops │ │ ├── large-source-file-1 │ │ ├── large-source-file-2 │ │ ├── nested │ │ ├── source-file-4 │ │ └── source-file-5 │ │ ├── processing │ │ ├── .gitignore │ │ └── empty │ │ ├── recovery │ │ ├── .gitignore │ │ └── empty │ │ ├── scheduling │ │ ├── test.file │ │ ├── test.rules │ │ └── test.schedules │ │ ├── source-file-1 │ │ ├── source-file-2 │ │ ├── source-file-3 │ │ └── temp-file-1 └── settings.gradle.kts ├── client-cli ├── .coveragerc ├── .dockerignore ├── .gitignore ├── .pylintrc ├── README.md ├── client_cli │ ├── __init__.py │ ├── __main__.py │ ├── api │ │ ├── __init__.py │ │ ├── client_api.py │ │ ├── default_client_api.py │ │ ├── default_init_api.py │ │ ├── endpoint_context.py │ │ ├── inactive_client_api.py │ │ ├── inactive_init_api.py │ │ └── init_api.py │ ├── cli │ │ ├── __init__.py │ │ ├── backup.py │ │ ├── common │ │ │ ├── __init__.py │ │ │ ├── filtering.py │ │ │ └── sorting.py │ │ ├── context.py │ │ ├── operations.py │ │ ├── recover.py │ │ ├── schedules.py │ │ └── service.py │ └── render │ │ ├── __init__.py │ │ ├── default │ │ ├── __init__.py │ │ ├── backup_rules.py │ │ ├── dataset_definitions.py │ │ ├── dataset_entries.py │ │ ├── dataset_metadata.py │ │ ├── devices.py │ │ ├── operations.py │ │ ├── schedules.py │ │ └── users.py │ │ ├── default_writer.py │ │ ├── flatten │ │ ├── __init__.py │ │ ├── backup_rules.py │ │ ├── dataset_definitions.py │ │ ├── dataset_entries.py │ │ ├── dataset_metadata.py │ │ └── init_state.py │ │ ├── json_writer.py │ │ └── writer.py ├── qa.py ├── setup.py └── tests │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── test_default_client_api.py │ ├── test_default_init_api.py │ ├── test_endpoint_context.py │ ├── test_inactive_client_api.py │ ├── test_inactive_init_api.py │ └── test_package.py │ ├── cli │ ├── __init__.py │ ├── cli_runner.py │ ├── common │ │ ├── __init__.py │ │ ├── test_filtering.py │ │ ├── test_package.py │ │ └── test_sorting.py │ ├── test_backup.py │ ├── test_operations.py │ ├── test_package.py │ ├── test_recover.py │ ├── test_schedules.py │ └── test_service.py │ ├── mocks │ ├── __init__.py │ ├── mock_client_api.py │ ├── mock_data.py │ └── mock_init_api.py │ ├── render │ ├── __init__.py │ ├── default │ │ ├── __init__.py │ │ ├── test_backup_rules.py │ │ ├── test_dataset_definitions.py │ │ ├── test_dataset_entries.py │ │ ├── test_dataset_metadata.py │ │ ├── test_devices.py │ │ ├── test_operations.py │ │ ├── test_schedules.py │ │ └── test_users.py │ ├── flatten │ │ ├── __init__.py │ │ ├── test_backup_rules.py │ │ ├── test_dataset_definitions.py │ │ ├── test_dataset_entries.py │ │ ├── test_dataset_metadata.py │ │ └── test_init_state.py │ ├── test_default_writer.py │ ├── test_json_writer.py │ └── test_package.py │ └── test_main.py ├── client-ui ├── .gitignore ├── .metadata ├── AppImageBuilder.yml ├── README.md ├── analysis_options.yaml ├── assets │ └── logo.svg ├── deployment │ └── dev │ │ └── build.py ├── lib │ ├── api │ │ ├── api_client.dart │ │ ├── app_processes.dart │ │ ├── default_client_api.dart │ │ ├── default_init_api.dart │ │ ├── endpoint_context.dart │ │ └── mock_api_client.dart │ ├── client_app.dart │ ├── color_schemes.dart │ ├── config │ │ ├── api_token.dart │ │ ├── app_dirs.dart │ │ ├── app_files.dart │ │ └── config.dart │ ├── main.dart │ ├── model │ │ ├── api │ │ │ ├── requests │ │ │ │ ├── create_dataset_definition.dart │ │ │ │ ├── update_dataset_definition.dart │ │ │ │ ├── update_user_password.dart │ │ │ │ └── update_user_salt.dart │ │ │ └── responses │ │ │ │ ├── created_dataset_definition.dart │ │ │ │ └── operation_started.dart │ │ ├── datasets │ │ │ ├── dataset_definition.dart │ │ │ ├── dataset_entry.dart │ │ │ ├── dataset_metadata.dart │ │ │ ├── dataset_metadata_search_result.dart │ │ │ └── entity_metadata.dart │ │ ├── devices │ │ │ ├── device.dart │ │ │ └── server_state.dart │ │ ├── formats.dart │ │ ├── operations │ │ │ ├── operation.dart │ │ │ ├── operation_progress.dart │ │ │ ├── operation_state.dart │ │ │ ├── rule.dart │ │ │ └── specification_rules.dart │ │ ├── schedules │ │ │ ├── active_schedule.dart │ │ │ └── schedule.dart │ │ ├── service │ │ │ ├── init_state.dart │ │ │ └── ping.dart │ │ └── users │ │ │ ├── permission.dart │ │ │ └── user.dart │ ├── pages │ │ ├── about.dart │ │ ├── backup.dart │ │ ├── backup_entries.dart │ │ ├── common │ │ │ └── components.dart │ │ ├── components │ │ │ ├── background_processes.dart │ │ │ ├── backup_entry_metadata.dart │ │ │ ├── client_not_configured_card.dart │ │ │ ├── context │ │ │ │ ├── context_menu.dart │ │ │ │ └── entry_action.dart │ │ │ ├── credentials_form.dart │ │ │ ├── dataset_definition_specification.dart │ │ │ ├── dataset_definition_summary.dart │ │ │ ├── dataset_entry_summary.dart │ │ │ ├── dataset_metadata_entity_summary.dart │ │ │ ├── entity_form.dart │ │ │ ├── extensions.dart │ │ │ ├── file_tree.dart │ │ │ ├── forms │ │ │ │ ├── boolean_field.dart │ │ │ │ ├── dataset_entry_field.dart │ │ │ │ ├── date_time_field.dart │ │ │ │ ├── duration_field.dart │ │ │ │ ├── file_size_field.dart │ │ │ │ ├── policy_field.dart │ │ │ │ └── retention_field.dart │ │ │ ├── invalid_config_file_card.dart │ │ │ ├── operation_details.dart │ │ │ ├── operation_summary.dart │ │ │ ├── rendering.dart │ │ │ ├── schedule_assignment_summary.dart │ │ │ ├── schedule_summary.dart │ │ │ ├── sizing.dart │ │ │ ├── top_bar.dart │ │ │ └── update_user_credentials_form.dart │ │ ├── home.dart │ │ ├── login.dart │ │ ├── operations.dart │ │ ├── page_destinations.dart │ │ ├── page_router.dart │ │ ├── recover.dart │ │ ├── rules.dart │ │ ├── schedules.dart │ │ ├── search.dart │ │ ├── settings.dart │ │ └── status.dart │ └── utils │ │ ├── chrono_unit.dart │ │ ├── debouncer.dart │ │ ├── env.dart │ │ ├── file_size_unit.dart │ │ ├── pair.dart │ │ └── triple.dart ├── linux │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ └── Flutter-Release.xcconfig │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ ├── 128.png │ │ │ ├── 16.png │ │ │ ├── 256.png │ │ │ ├── 32.png │ │ │ ├── 512.png │ │ │ ├── 64.png │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ └── MainMenu.xib │ │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements ├── pubspec.yaml ├── qa.py └── test │ ├── api │ ├── api_client_test.dart │ ├── default_client_api_test.dart │ ├── default_init_api_test.dart │ └── endpoint_context_test.dart │ ├── config │ ├── api_token_test.dart │ ├── app_files_test.dart │ └── config_test.dart │ ├── mocks │ └── mock_api_client.dart │ ├── model │ ├── datasets │ │ ├── dataset_metadata_test.dart │ │ └── entity_metadata_test.dart │ ├── formats_test.dart │ ├── operations │ │ ├── operation_state_test.dart │ │ ├── operation_test.dart │ │ ├── rule_test.dart │ │ └── specification_rules_test.dart │ ├── schedules │ │ └── active_schedule_test.dart │ └── users │ │ ├── permission_test.dart │ │ └── user_test.dart │ ├── pages │ └── manage │ │ └── components │ │ └── rendering_test.dart │ ├── resources │ ├── api_token │ ├── app_files │ │ ├── api-token │ │ ├── client.conf │ │ ├── client.rules │ │ ├── client.schedules │ │ └── invalid │ │ │ └── client.conf │ ├── invalid.conf │ ├── invalid.json │ ├── localhost.p12 │ ├── secret.conf │ ├── valid.conf │ └── valid.json │ └── utils │ ├── pair_test.dart │ └── triple_test.dart ├── client ├── README.md └── src │ ├── main │ ├── protobuf │ │ ├── metadata.proto │ │ └── state.proto │ ├── resources │ │ ├── assets │ │ │ ├── logo.png │ │ │ └── logo.svg │ │ ├── logback.xml │ │ ├── reference.conf │ │ └── templates │ │ │ ├── client.conf.template │ │ │ ├── client.rules.linux.template │ │ │ └── client.rules.macos.template │ └── scala │ │ └── stasis │ │ └── client │ │ ├── Main.scala │ │ ├── analysis │ │ ├── Checksum.scala │ │ └── Metadata.scala │ │ ├── api │ │ ├── Context.scala │ │ ├── clients │ │ │ ├── CachedServerApiEndpointClient.scala │ │ │ ├── Clients.scala │ │ │ ├── DefaultServerApiEndpointClient.scala │ │ │ ├── DefaultServerBootstrapEndpointClient.scala │ │ │ ├── DefaultServerCoreEndpointClient.scala │ │ │ ├── ServerApiEndpointClient.scala │ │ │ ├── ServerBootstrapEndpointClient.scala │ │ │ ├── ServerCoreEndpointClient.scala │ │ │ ├── exceptions │ │ │ │ ├── ServerApiFailure.scala │ │ │ │ └── ServerBootstrapFailure.scala │ │ │ └── internal │ │ │ │ └── InsecureX509TrustManager.scala │ │ └── http │ │ │ ├── Formats.scala │ │ │ ├── HttpApiEndpoint.scala │ │ │ └── routes │ │ │ ├── ApiRoutes.scala │ │ │ ├── DatasetDefinitions.scala │ │ │ ├── DatasetEntries.scala │ │ │ ├── DatasetMetadata.scala │ │ │ ├── Device.scala │ │ │ ├── Operations.scala │ │ │ ├── Schedules.scala │ │ │ ├── Service.scala │ │ │ └── User.scala │ │ ├── collection │ │ ├── BackupCollector.scala │ │ ├── BackupMetadataCollector.scala │ │ ├── RecoveryCollector.scala │ │ ├── RecoveryMetadataCollector.scala │ │ └── rules │ │ │ ├── Rule.scala │ │ │ ├── RuleSet.scala │ │ │ ├── Specification.scala │ │ │ ├── exceptions │ │ │ ├── RuleMatchingFailure.scala │ │ │ └── RuleParsingFailure.scala │ │ │ └── internal │ │ │ ├── FilesWalker.scala │ │ │ └── IndexedRule.scala │ │ ├── compression │ │ ├── Compression.scala │ │ ├── Decoder.scala │ │ ├── Deflate.scala │ │ ├── Encoder.scala │ │ ├── Gzip.scala │ │ └── Identity.scala │ │ ├── encryption │ │ ├── Aes.scala │ │ ├── Decoder.scala │ │ ├── Encoder.scala │ │ ├── secrets │ │ │ ├── DeviceFileSecret.scala │ │ │ ├── DeviceMetadataSecret.scala │ │ │ ├── DeviceSecret.scala │ │ │ ├── Secret.scala │ │ │ ├── UserAuthenticationPassword.scala │ │ │ ├── UserHashedEncryptionPassword.scala │ │ │ ├── UserKeyStoreEncryptionSecret.scala │ │ │ ├── UserLocalEncryptionSecret.scala │ │ │ └── UserPassword.scala │ │ └── stream │ │ │ └── CipherStage.scala │ │ ├── model │ │ ├── DatasetMetadata.scala │ │ ├── EntityMetadata.scala │ │ ├── FilesystemMetadata.scala │ │ ├── SourceEntity.scala │ │ └── TargetEntity.scala │ │ ├── ops │ │ ├── Metrics.scala │ │ ├── ParallelismConfig.scala │ │ ├── backup │ │ │ ├── Backup.scala │ │ │ ├── Providers.scala │ │ │ └── stages │ │ │ │ ├── EntityCollection.scala │ │ │ │ ├── EntityDiscovery.scala │ │ │ │ ├── EntityProcessing.scala │ │ │ │ ├── MetadataCollection.scala │ │ │ │ ├── MetadataPush.scala │ │ │ │ └── internal │ │ │ │ ├── CompressedByteStringSource.scala │ │ │ │ ├── PartitionedByteStringSource.scala │ │ │ │ └── StagedSubFlow.scala │ │ ├── commands │ │ │ ├── CommandProcessor.scala │ │ │ ├── DefaultCommandProcessor.scala │ │ │ ├── DefaultCommandProcessorHandlers.scala │ │ │ ├── DefaultCommandProcessorState.scala │ │ │ └── ProcessedCommand.scala │ │ ├── exceptions │ │ │ ├── EntityDiscardFailure.scala │ │ │ ├── EntityMergeFailure.scala │ │ │ ├── EntityProcessingFailure.scala │ │ │ ├── OperationExecutionFailure.scala │ │ │ ├── OperationStopped.scala │ │ │ ├── ScheduleAssignmentParsingFailure.scala │ │ │ └── ScheduleRetrievalFailure.scala │ │ ├── monitoring │ │ │ ├── DefaultServerMonitor.scala │ │ │ └── ServerMonitor.scala │ │ ├── recovery │ │ │ ├── Providers.scala │ │ │ ├── Recovery.scala │ │ │ └── stages │ │ │ │ ├── EntityCollection.scala │ │ │ │ ├── EntityProcessing.scala │ │ │ │ ├── MetadataApplication.scala │ │ │ │ └── internal │ │ │ │ ├── DecompressedByteStringSource.scala │ │ │ │ ├── DecryptedCrates.scala │ │ │ │ ├── DestagedByteStringSource.scala │ │ │ │ └── MergedCrates.scala │ │ ├── scheduling │ │ │ ├── DefaultOperationExecutor.scala │ │ │ ├── DefaultOperationScheduler.scala │ │ │ ├── OperationExecutor.scala │ │ │ ├── OperationScheduleAssignment.scala │ │ │ └── OperationScheduler.scala │ │ └── search │ │ │ ├── DefaultSearch.scala │ │ │ └── Search.scala │ │ ├── security │ │ ├── CredentialsProvider.scala │ │ ├── DefaultCredentialsProvider.scala │ │ ├── DefaultFrontendAuthenticator.scala │ │ └── FrontendAuthenticator.scala │ │ ├── service │ │ ├── ApplicationArguments.scala │ │ ├── ApplicationConfiguration.scala │ │ ├── ApplicationDirectory.scala │ │ ├── ApplicationRuntimeRequirements.scala │ │ ├── ApplicationTemplates.scala │ │ ├── ApplicationTray.scala │ │ ├── Service.scala │ │ └── components │ │ │ ├── ApiClients.scala │ │ │ ├── ApiEndpoint.scala │ │ │ ├── Base.scala │ │ │ ├── Files.scala │ │ │ ├── Init.scala │ │ │ ├── Ops.scala │ │ │ ├── Secrets.scala │ │ │ ├── Tracking.scala │ │ │ ├── bootstrap │ │ │ ├── Base.scala │ │ │ ├── Bootstrap.scala │ │ │ ├── Init.scala │ │ │ ├── Parameters.scala │ │ │ ├── Secrets.scala │ │ │ ├── init │ │ │ │ ├── ViaCli.scala │ │ │ │ └── ViaStdIn.scala │ │ │ └── internal │ │ │ │ └── SelfSignedCertificateGenerator.scala │ │ │ ├── exceptions │ │ │ └── ServiceStartupFailure.scala │ │ │ ├── init │ │ │ ├── ViaApi.scala │ │ │ └── ViaStdIn.scala │ │ │ ├── internal │ │ │ ├── ConfigOverride.scala │ │ │ └── FutureOps.scala │ │ │ └── maintenance │ │ │ ├── Base.scala │ │ │ ├── Certificates.scala │ │ │ ├── Credentials.scala │ │ │ ├── Init.scala │ │ │ ├── Secrets.scala │ │ │ └── init │ │ │ ├── ViaCli.scala │ │ │ └── ViaStdIn.scala │ │ ├── staging │ │ ├── DefaultFileStaging.scala │ │ └── FileStaging.scala │ │ └── tracking │ │ ├── BackupTracker.scala │ │ ├── RecoveryTracker.scala │ │ ├── ServerTracker.scala │ │ ├── TrackerViews.scala │ │ ├── Trackers.scala │ │ ├── state │ │ ├── BackupState.scala │ │ ├── OperationState.scala │ │ ├── RecoveryState.scala │ │ └── serdes │ │ │ ├── BackupStateSerdes.scala │ │ │ └── RecoveryStateSerdes.scala │ │ └── trackers │ │ ├── DefaultBackupTracker.scala │ │ ├── DefaultRecoveryTracker.scala │ │ └── DefaultServerTracker.scala │ └── test │ ├── resources │ ├── analysis │ │ ├── digest-source-file │ │ └── metadata-source-file │ ├── application.conf │ ├── collection │ │ ├── file-1 │ │ ├── file-2 │ │ ├── file-3 │ │ ├── other-file-1 │ │ └── other-file-2 │ ├── encryption │ │ ├── encrypted-file │ │ └── plaintext-file │ ├── logback-test.xml │ ├── mockito-extensions │ │ └── org.mockito.plugins.MockMaker │ └── ops │ │ ├── large-source-file │ │ ├── nested │ │ ├── source-file-4 │ │ └── source-file-5 │ │ ├── processing │ │ ├── .gitignore │ │ └── empty │ │ ├── recovery │ │ ├── .gitignore │ │ └── empty │ │ ├── scheduling │ │ ├── extra.rules │ │ ├── test.file │ │ ├── test.rules │ │ └── test.schedules │ │ ├── source-file-1 │ │ ├── source-file-2 │ │ ├── source-file-3 │ │ └── temp-file-1 │ └── scala │ └── stasis │ └── test │ └── specs │ └── unit │ └── client │ ├── EncodingHelpers.scala │ ├── Fixtures.scala │ ├── ResourceHelpers.scala │ ├── analysis │ ├── ChecksumSpec.scala │ └── MetadataSpec.scala │ ├── api │ ├── clients │ │ ├── CachedServerApiEndpointClientSpec.scala │ │ ├── ClientsSpec.scala │ │ ├── DefaultServerApiEndpointClientSpec.scala │ │ ├── DefaultServerBootstrapEndpointClientSpec.scala │ │ ├── DefaultServerCoreEndpointClientSpec.scala │ │ └── internal │ │ │ └── InsecureX509TrustManagerSpec.scala │ └── http │ │ ├── FormatsSpec.scala │ │ ├── HttpApiEndpointSpec.scala │ │ └── routes │ │ ├── DatasetDefinitionsSpec.scala │ │ ├── DatasetEntriesSpec.scala │ │ ├── DatasetMetadataSpec.scala │ │ ├── DeviceSpec.scala │ │ ├── OperationsSpec.scala │ │ ├── SchedulesSpec.scala │ │ ├── ServiceSpec.scala │ │ └── UserSpec.scala │ ├── collection │ ├── BackupCollectorSpec.scala │ ├── BackupMetadataCollectorSpec.scala │ ├── RecoveryCollectorSpec.scala │ ├── RecoveryMetadataCollectorSpec.scala │ └── rules │ │ ├── RuleSetSpec.scala │ │ ├── RuleSpec.scala │ │ ├── SpecificationBehaviour.scala │ │ ├── SpecificationSpec.scala │ │ └── internal │ │ ├── FilesWalkerBehaviour.scala │ │ └── FilesWalkerSpec.scala │ ├── compression │ ├── CompressionSpec.scala │ ├── DeflateSpec.scala │ ├── GzipSpec.scala │ └── IdentitySpec.scala │ ├── encryption │ ├── AesSpec.scala │ ├── secrets │ │ ├── DeviceFileSecretSpec.scala │ │ ├── DeviceMetadataSecretSpec.scala │ │ ├── DeviceSecretSpec.scala │ │ ├── SecretsConfig.scala │ │ ├── UserAuthenticationPasswordSpec.scala │ │ ├── UserHashedEncryptionPasswordSpec.scala │ │ ├── UserKeyStoreEncryptionSecretSpec.scala │ │ ├── UserLocalEncryptionSecretSpec.scala │ │ └── UserPasswordSpec.scala │ └── stream │ │ └── CipherStageSpec.scala │ ├── mocks │ ├── MockBackupCollector.scala │ ├── MockBackupMetadataCollector.scala │ ├── MockBackupTracker.scala │ ├── MockChecksum.scala │ ├── MockClientTelemetryContext.scala │ ├── MockCommandProcessor.scala │ ├── MockCompression.scala │ ├── MockEncryption.scala │ ├── MockFileStaging.scala │ ├── MockOperationExecutor.scala │ ├── MockOperationScheduler.scala │ ├── MockOpsMetrics.scala │ ├── MockRecoveryCollector.scala │ ├── MockRecoveryMetadataCollector.scala │ ├── MockRecoveryTracker.scala │ ├── MockSearch.scala │ ├── MockServerApiEndpoint.scala │ ├── MockServerApiEndpointClient.scala │ ├── MockServerBootstrapEndpoint.scala │ ├── MockServerBootstrapEndpointClient.scala │ ├── MockServerCoreEndpoint.scala │ ├── MockServerCoreEndpointClient.scala │ ├── MockServerMonitor.scala │ ├── MockServerTracker.scala │ ├── MockTokenEndpoint.scala │ ├── MockTrackerViews.scala │ └── MockX509TrustManager.scala │ ├── model │ ├── DatasetMetadataSpec.scala │ ├── EntityMetadataSpec.scala │ ├── FilesystemMetadataSpec.scala │ ├── SourceEntitySpec.scala │ └── TargetEntitySpec.scala │ ├── ops │ ├── MetricsSpec.scala │ ├── backup │ │ ├── BackupSpec.scala │ │ └── stages │ │ │ ├── EntityCollectionSpec.scala │ │ │ ├── EntityDiscoverySpec.scala │ │ │ ├── EntityProcessingSpec.scala │ │ │ ├── MetadataCollectionSpec.scala │ │ │ ├── MetadataPushSpec.scala │ │ │ └── internal │ │ │ ├── CompressedByteStringSourceSpec.scala │ │ │ ├── PartitionedByteStringSourceSpec.scala │ │ │ └── StagedSubFlowSpec.scala │ ├── commands │ │ ├── CommandProcessorSpec.scala │ │ ├── DefaultCommandProcessorHandlersSpec.scala │ │ ├── DefaultCommandProcessorSpec.scala │ │ └── DefaultCommandProcessorStateSpec.scala │ ├── exceptions │ │ └── OperationStoppedSpec.scala │ ├── monitoring │ │ └── DefaultServerMonitorSpec.scala │ ├── recovery │ │ ├── RecoverySpec.scala │ │ └── stages │ │ │ ├── EntityCollectionSpec.scala │ │ │ ├── EntityProcessingSpec.scala │ │ │ ├── MetadataApplicationSpec.scala │ │ │ └── internal │ │ │ ├── DecompressedByteStringSourceSpec.scala │ │ │ ├── DecryptedCratesSpec.scala │ │ │ ├── DestagedByteStringSourceSpec.scala │ │ │ └── MergedCratesSpec.scala │ ├── scheduling │ │ ├── DefaultOperationExecutorSpec.scala │ │ ├── DefaultOperationSchedulerSpec.scala │ │ └── OperationScheduleAssignmentSpec.scala │ └── search │ │ ├── DefaultSearchSpec.scala │ │ └── SearchSpec.scala │ ├── security │ ├── DefaultCredentialsProviderSpec.scala │ └── DefaultFrontendAuthenticatorSpec.scala │ ├── service │ ├── ApplicationArgumentsSpec.scala │ ├── ApplicationConfigurationSpec.scala │ ├── ApplicationDirectorySpec.scala │ ├── ApplicationRuntimeRequirementsSpec.scala │ ├── ApplicationTemplatesSpec.scala │ ├── ApplicationTraySpec.scala │ ├── ServiceSpec.scala │ └── components │ │ ├── ApiClientsSpec.scala │ │ ├── ApiEndpointSpec.scala │ │ ├── BaseSpec.scala │ │ ├── InitSpec.scala │ │ ├── OpsSpec.scala │ │ ├── SecretsSpec.scala │ │ ├── TrackingSpec.scala │ │ ├── bootstrap │ │ ├── BaseSpec.scala │ │ ├── BootstrapSpec.scala │ │ ├── InitSpec.scala │ │ ├── ParametersSpec.scala │ │ ├── SecretsSpec.scala │ │ ├── init │ │ │ ├── ViaCliSpec.scala │ │ │ └── ViaStdInSpec.scala │ │ └── internal │ │ │ └── SelfSignedCertificateGeneratorSpec.scala │ │ ├── exceptions │ │ └── ServiceStartupFailureSpec.scala │ │ ├── init │ │ ├── ViaApiSpec.scala │ │ └── ViaStdInSpec.scala │ │ ├── internal │ │ ├── ConfigOverrideSpec.scala │ │ └── FutureOpsSpec.scala │ │ └── maintenance │ │ ├── BaseSpec.scala │ │ ├── CertificatesSpec.scala │ │ ├── CredentialsSpec.scala │ │ ├── InitSpec.scala │ │ ├── SecretsSpec.scala │ │ └── init │ │ ├── ViaCliSpec.scala │ │ └── ViaStdInSpec.scala │ ├── staging │ └── DefaultFileStagingSpec.scala │ └── tracking │ ├── TrackersSpec.scala │ ├── state │ ├── BackupStateSpec.scala │ ├── RecoveryStateSpec.scala │ └── serdes │ │ ├── BackupStateSerdesSpec.scala │ │ └── RecoveryStateSerdesSpec.scala │ └── trackers │ ├── DefaultBackupTrackerSpec.scala │ ├── DefaultRecoveryTrackerSpec.scala │ └── DefaultServerTrackerSpec.scala ├── core ├── README.md └── src │ ├── main │ └── scala │ │ └── stasis │ │ └── core │ │ ├── api │ │ ├── Formats.scala │ │ └── PoolClient.scala │ │ ├── discovery │ │ ├── ServiceApiClient.scala │ │ ├── ServiceApiEndpoint.scala │ │ ├── ServiceDiscoveryClient.scala │ │ ├── ServiceDiscoveryRequest.scala │ │ ├── ServiceDiscoveryResult.scala │ │ ├── exceptions │ │ │ └── DiscoveryFailure.scala │ │ ├── http │ │ │ ├── HttpServiceDiscoveryClient.scala │ │ │ └── HttpServiceDiscoveryEndpoint.scala │ │ └── providers │ │ │ ├── client │ │ │ └── ServiceDiscoveryProvider.scala │ │ │ └── server │ │ │ └── ServiceDiscoveryProvider.scala │ │ ├── networking │ │ ├── Endpoint.scala │ │ ├── EndpointAddress.scala │ │ ├── EndpointClient.scala │ │ ├── exceptions │ │ │ ├── ClientFailure.scala │ │ │ ├── CredentialsFailure.scala │ │ │ ├── EndpointFailure.scala │ │ │ ├── NetworkingFailure.scala │ │ │ └── ReservationFailure.scala │ │ ├── grpc │ │ │ ├── GrpcEndpoint.scala │ │ │ ├── GrpcEndpointAddress.scala │ │ │ ├── GrpcEndpointClient.scala │ │ │ └── internal │ │ │ │ ├── Client.scala │ │ │ │ ├── Credentials.scala │ │ │ │ ├── Implicits.scala │ │ │ │ └── Requests.scala │ │ └── http │ │ │ ├── HttpEndpoint.scala │ │ │ ├── HttpEndpointAddress.scala │ │ │ └── HttpEndpointClient.scala │ │ ├── packaging │ │ ├── Crate.scala │ │ └── Manifest.scala │ │ ├── persistence │ │ ├── CrateStorageRequest.scala │ │ ├── CrateStorageReservation.scala │ │ ├── Metrics.scala │ │ ├── backends │ │ │ ├── EventLogBackend.scala │ │ │ ├── KeyValueBackend.scala │ │ │ ├── StreamingBackend.scala │ │ │ ├── file │ │ │ │ ├── ContainerBackend.scala │ │ │ │ ├── EventLogFileBackend.scala │ │ │ │ ├── FileBackend.scala │ │ │ │ ├── container │ │ │ │ │ ├── Container.scala │ │ │ │ │ ├── CrateChunk.scala │ │ │ │ │ ├── CrateChunkDescriptor.scala │ │ │ │ │ ├── README.md │ │ │ │ │ ├── exceptions │ │ │ │ │ │ ├── ContainerFailure.scala │ │ │ │ │ │ ├── ContainerSinkFailure.scala │ │ │ │ │ │ ├── ContainerSourceFailure.scala │ │ │ │ │ │ └── ConversionFailure.scala │ │ │ │ │ ├── headers │ │ │ │ │ │ ├── ChunkHeader.scala │ │ │ │ │ │ ├── ContainerHeader.scala │ │ │ │ │ │ └── ContainerLogHeader.scala │ │ │ │ │ ├── ops │ │ │ │ │ │ ├── AutoCloseSupport.scala │ │ │ │ │ │ ├── ContainerLogOps.scala │ │ │ │ │ │ ├── ContainerOps.scala │ │ │ │ │ │ ├── ConversionOps.scala │ │ │ │ │ │ └── FileOps.scala │ │ │ │ │ └── stream │ │ │ │ │ │ ├── CrateChunkSink.scala │ │ │ │ │ │ ├── CrateChunkSource.scala │ │ │ │ │ │ └── transform │ │ │ │ │ │ ├── ChunksToCrate.scala │ │ │ │ │ │ └── CrateToChunks.scala │ │ │ │ └── state │ │ │ │ │ └── StateStore.scala │ │ │ ├── memory │ │ │ │ ├── EventLogMemoryBackend.scala │ │ │ │ └── StreamingMemoryBackend.scala │ │ │ └── slick │ │ │ │ ├── LegacyKeyValueStore.scala │ │ │ │ ├── SlickBackend.scala │ │ │ │ └── SlickProfile.scala │ │ ├── commands │ │ │ ├── CommandStore.scala │ │ │ └── DefaultCommandStore.scala │ │ ├── crates │ │ │ └── CrateStore.scala │ │ ├── events │ │ │ └── EventLog.scala │ │ ├── exceptions │ │ │ ├── PersistenceFailure.scala │ │ │ ├── ReservationFailure.scala │ │ │ └── StagingFailure.scala │ │ ├── manifests │ │ │ ├── DefaultManifestStore.scala │ │ │ └── ManifestStore.scala │ │ ├── nodes │ │ │ ├── DefaultNodeStore.scala │ │ │ └── NodeStore.scala │ │ ├── reservations │ │ │ ├── DefaultReservationStore.scala │ │ │ └── ReservationStore.scala │ │ └── staging │ │ │ └── StagingStore.scala │ │ ├── routing │ │ ├── DefaultRouter.scala │ │ ├── Metrics.scala │ │ ├── Node.scala │ │ ├── NodeProxy.scala │ │ ├── Router.scala │ │ └── exceptions │ │ │ ├── DiscardFailure.scala │ │ │ ├── DistributionFailure.scala │ │ │ ├── PullFailure.scala │ │ │ ├── PushFailure.scala │ │ │ └── RoutingFailure.scala │ │ └── security │ │ ├── JwtNodeAuthenticator.scala │ │ ├── JwtNodeCredentialsProvider.scala │ │ ├── NodeAuthenticator.scala │ │ ├── NodeCredentialsProvider.scala │ │ └── PreSharedKeyNodeAuthenticator.scala │ └── test │ ├── resources │ ├── application.conf │ ├── certs │ │ ├── localhost.jks │ │ └── localhost.p12 │ ├── discovery-static-invalid.conf │ ├── discovery-static.conf │ └── logback.xml │ └── scala │ └── stasis │ └── test │ └── specs │ └── unit │ ├── AsyncUnitSpec.scala │ ├── UnitSpec.scala │ └── core │ ├── api │ ├── FormatsSpec.scala │ └── PoolClientSpec.scala │ ├── discovery │ ├── ServiceApiEndpointSpec.scala │ ├── ServiceDiscoveryClientSpec.scala │ ├── ServiceDiscoveryRequestSpec.scala │ ├── ServiceDiscoveryResultSpec.scala │ ├── http │ │ ├── HttpServiceDiscoveryClientSpec.scala │ │ └── HttpServiceDiscoveryEndpointSpec.scala │ ├── mocks │ │ ├── MockDiscoveryApiEndpoint.scala │ │ └── MockServiceDiscoveryClient.scala │ └── providers │ │ ├── client │ │ └── ServiceDiscoveryProviderSpec.scala │ │ └── server │ │ └── ServiceDiscoveryProviderSpec.scala │ ├── networking │ ├── grpc │ │ ├── GrpcEndpointClientSpec.scala │ │ ├── GrpcEndpointSpec.scala │ │ └── internal │ │ │ ├── CredentialsSpec.scala │ │ │ ├── ImplicitsSpec.scala │ │ │ └── RequestsSpec.scala │ ├── http │ │ ├── HttpEndpointClientSpec.scala │ │ └── HttpEndpointSpec.scala │ └── mocks │ │ ├── MockGrpcEndpointClient.scala │ │ ├── MockGrpcNodeCredentialsProvider.scala │ │ ├── MockHttpEndpointClient.scala │ │ └── MockHttpNodeCredentialsProvider.scala │ ├── packaging │ └── ManifestSpec.scala │ ├── persistence │ ├── CrateStorageRequestSpec.scala │ ├── CrateStorageReservationSpec.scala │ ├── Generators.scala │ ├── MetricsSpec.scala │ ├── MockPersistenceMetrics.scala │ ├── backends │ │ ├── EventLogBackendBehaviour.scala │ │ ├── StreamingBackendBehaviour.scala │ │ ├── file │ │ │ ├── ContainerBackendSpec.scala │ │ │ ├── EventLogFileBackendSpec.scala │ │ │ ├── FileBackendSpec.scala │ │ │ ├── container │ │ │ │ ├── ContainerLogEntrySpec.scala │ │ │ │ ├── ContainerSpec.scala │ │ │ │ ├── TestOps.scala │ │ │ │ ├── headers │ │ │ │ │ ├── ChunkHeaderSpec.scala │ │ │ │ │ ├── ContainerHeaderSpec.scala │ │ │ │ │ └── ContainerLogHeaderSpec.scala │ │ │ │ ├── ops │ │ │ │ │ ├── ContainerLogOpsSpec.scala │ │ │ │ │ ├── ContainerOpsSpec.scala │ │ │ │ │ └── ConversionOpsSpec.scala │ │ │ │ └── stream │ │ │ │ │ ├── CrateChunkSinkSpec.scala │ │ │ │ │ ├── CrateChunkSourceSpec.scala │ │ │ │ │ └── transform │ │ │ │ │ ├── ChunksToCrateSpec.scala │ │ │ │ │ └── CrateToChunksSpec.scala │ │ │ └── state │ │ │ │ ├── StateStoreBehaviour.scala │ │ │ │ └── StateStoreSpec.scala │ │ ├── memory │ │ │ ├── EventLogMemoryBackendSpec.scala │ │ │ └── StreamingMemoryBackendSpec.scala │ │ └── slick │ │ │ ├── SlickBackendSpec.scala │ │ │ └── SlickProfileSpec.scala │ ├── commands │ │ └── DefaultCommandStoreSpec.scala │ ├── crates │ │ ├── CrateStoreSpec.scala │ │ └── MockCrateStore.scala │ ├── events │ │ └── EventLogSpec.scala │ ├── manifests │ │ ├── DefaultManifestStoreSpec.scala │ │ └── MockManifestStore.scala │ ├── nodes │ │ ├── DefaultNodeStoreSpec.scala │ │ ├── MockNodeStore.scala │ │ └── NodeStoreSpec.scala │ ├── reservations │ │ ├── DefaultReservationStoreSpec.scala │ │ ├── MockReservationStore.scala │ │ └── ReservationStoreSpec.scala │ └── staging │ │ └── StagingStoreSpec.scala │ ├── routing │ ├── DefaultRouterSpec.scala │ ├── MetricsSpec.scala │ ├── NodeProxySpec.scala │ ├── NodeSpec.scala │ └── mocks │ │ ├── MockRouter.scala │ │ └── MockRoutingMetrics.scala │ ├── security │ ├── JwtNodeAuthenticatorSpec.scala │ ├── JwtNodeCredentialsProviderSpec.scala │ ├── PreSharedKeyNodeAuthenticatorSpec.scala │ └── mocks │ │ ├── MockGrpcAuthenticator.scala │ │ └── MockHttpAuthenticator.scala │ └── telemetry │ └── MockTelemetryContext.scala ├── deployment ├── .gitignore ├── README.md ├── dev │ ├── README.md │ ├── config │ │ ├── client.conf │ │ ├── client.rules │ │ ├── client.schedules │ │ ├── grafana │ │ │ ├── dashboards │ │ │ │ └── dashboard.yml │ │ │ └── datasources │ │ │ │ └── datasource.yml │ │ ├── identity-bootstrap.conf │ │ ├── prometheus-local │ │ │ └── prometheus.yml │ │ ├── prometheus │ │ │ └── prometheus.yml │ │ ├── server-bootstrap.conf │ │ └── server-discovery.conf │ ├── docker-compose-metrics.yml │ ├── docker-compose-no-auth-hash.yml │ ├── docker-compose.yml │ ├── dockerfiles │ │ └── client-cli.Dockerfile │ ├── scripts │ │ ├── client_install.sh │ │ ├── client_uninstall.sh │ │ ├── generate_artifacts.py │ │ ├── generate_device_secret.py │ │ ├── generate_self_signed_cert.py │ │ ├── generate_user_password.py │ │ ├── prepare_deployment.sh │ │ └── run_smoke_test.sh │ └── secrets │ │ └── .gitignore ├── grafana │ └── dashboards │ │ ├── client │ │ └── client-overview.json │ │ ├── identity │ │ ├── identity-apis.json │ │ ├── identity-kv-stores.json │ │ └── identity-overview.json │ │ ├── jvm │ │ └── jvm-overview.json │ │ ├── postgresql │ │ └── postgresql-overview.json │ │ └── server │ │ ├── server-apis.json │ │ ├── server-io.json │ │ ├── server-kv-stores.json │ │ └── server-overview.json └── production │ ├── README.md │ ├── bootstrap │ ├── identity.conf │ └── server.conf │ ├── docker-compose.yml │ ├── local │ └── .gitignore │ ├── scripts │ ├── client_install.sh │ ├── client_uninstall.sh │ ├── generate_cert.py │ ├── generate_user_password.py │ ├── server_create_device.sh │ ├── server_create_user.sh │ ├── server_delete_user.sh │ ├── server_get_bootstrap_code.sh │ └── server_prepare_deployment.sh │ ├── secrets │ ├── .gitignore │ └── templates │ │ ├── db-identity-exporter.env.template │ │ ├── db-identity.env.template │ │ ├── db-server-exporter.env.template │ │ ├── db-server.env.template │ │ ├── identity-ui.env.template │ │ ├── identity.bootstrap.env.template │ │ ├── identity.env.template │ │ ├── server-ui.env.template │ │ ├── server.bootstrap.env.template │ │ └── server.env.template │ └── telemetry │ ├── grafana │ ├── dashboards │ │ └── dashboard.yml │ └── datasources │ │ └── datasource.yml │ └── prometheus │ └── prometheus.yml ├── identity-ui ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── deployment │ ├── dev │ │ ├── .env │ │ ├── bootstrap.conf │ │ ├── build.py │ │ ├── docker-compose.yml │ │ ├── run_browser.py │ │ └── run_server.py │ └── production │ │ ├── .env.template │ │ ├── Dockerfile │ │ ├── build.py │ │ ├── entrypoint.sh │ │ └── nginx.template ├── lib │ ├── api │ │ ├── api_client.dart │ │ ├── default_api_client.dart │ │ └── oauth.dart │ ├── color_schemes.dart │ ├── main.dart │ ├── model │ │ ├── api.dart │ │ ├── client.dart │ │ ├── requests │ │ │ ├── create_api.dart │ │ │ ├── create_client.dart │ │ │ ├── create_owner.dart │ │ │ ├── update_client.dart │ │ │ ├── update_client_credentials.dart │ │ │ ├── update_owner.dart │ │ │ └── update_owner_credentials.dart │ │ ├── resource_owner.dart │ │ ├── stored_authorization_code.dart │ │ └── stored_refresh_token.dart │ └── pages │ │ ├── authorize │ │ ├── authorization_callback.dart │ │ ├── authorize.dart │ │ ├── credentials_form.dart │ │ └── derived_passwords.dart │ │ ├── default │ │ ├── components.dart │ │ ├── home.dart │ │ └── not_found.dart │ │ ├── manage │ │ ├── apis.dart │ │ ├── clients.dart │ │ ├── codes.dart │ │ ├── components │ │ │ ├── entity_form.dart │ │ │ ├── entity_table.dart │ │ │ └── rendering.dart │ │ ├── owners.dart │ │ └── tokens.dart │ │ └── page_router.dart ├── pubspec.yaml ├── qa.py ├── test │ ├── api │ │ ├── default_api_client_test.dart │ │ └── oauth_test.dart │ └── pages │ │ ├── authorize │ │ └── derived_passwords_test.dart │ │ └── manage │ │ └── components │ │ └── rendering_test.dart └── web │ ├── assets │ └── logo.svg │ ├── authorize │ └── authorize.js │ ├── index.html │ └── manifest.json ├── identity ├── README.md └── src │ ├── main │ ├── resources │ │ ├── example-bootstrap.conf │ │ ├── logback.xml │ │ └── reference.conf │ └── scala │ │ └── stasis │ │ └── identity │ │ ├── Main.scala │ │ ├── api │ │ ├── Formats.scala │ │ ├── IdentityEndpoint.scala │ │ ├── Jwks.scala │ │ ├── Manage.scala │ │ ├── OAuth.scala │ │ ├── manage │ │ │ ├── Apis.scala │ │ │ ├── Clients.scala │ │ │ ├── Codes.scala │ │ │ ├── Owners.scala │ │ │ ├── Tokens.scala │ │ │ ├── directives │ │ │ │ ├── UserAuthentication.scala │ │ │ │ └── UserAuthorization.scala │ │ │ ├── requests │ │ │ │ ├── CreateApi.scala │ │ │ │ ├── CreateClient.scala │ │ │ │ ├── CreateOwner.scala │ │ │ │ ├── UpdateClient.scala │ │ │ │ ├── UpdateClientCredentials.scala │ │ │ │ ├── UpdateOwner.scala │ │ │ │ └── UpdateOwnerCredentials.scala │ │ │ ├── responses │ │ │ │ └── CreatedClient.scala │ │ │ └── setup │ │ │ │ ├── Config.scala │ │ │ │ └── Providers.scala │ │ └── oauth │ │ │ ├── AuthorizationCodeGrant.scala │ │ │ ├── ClientCredentialsGrant.scala │ │ │ ├── ImplicitGrant.scala │ │ │ ├── PkceAuthorizationCodeGrant.scala │ │ │ ├── RefreshTokenGrant.scala │ │ │ ├── ResourceOwnerPasswordCredentialsGrant.scala │ │ │ ├── directives │ │ │ ├── AccessTokenGeneration.scala │ │ │ ├── AudienceExtraction.scala │ │ │ ├── AuthDirectives.scala │ │ │ ├── AuthorizationCodeConsumption.scala │ │ │ ├── AuthorizationCodeGeneration.scala │ │ │ ├── ClientAuthentication.scala │ │ │ ├── ClientRetrieval.scala │ │ │ ├── RefreshTokenConsumption.scala │ │ │ ├── RefreshTokenGeneration.scala │ │ │ └── ResourceOwnerAuthentication.scala │ │ │ └── setup │ │ │ ├── Config.scala │ │ │ └── Providers.scala │ │ ├── authentication │ │ ├── manage │ │ │ ├── DefaultResourceOwnerAuthenticator.scala │ │ │ └── ResourceOwnerAuthenticator.scala │ │ └── oauth │ │ │ ├── ClientAuthenticator.scala │ │ │ ├── DefaultClientAuthenticator.scala │ │ │ ├── DefaultResourceOwnerAuthenticator.scala │ │ │ ├── EntityAuthenticator.scala │ │ │ └── ResourceOwnerAuthenticator.scala │ │ ├── model │ │ ├── ChallengeMethod.scala │ │ ├── GrantType.scala │ │ ├── ResponseType.scala │ │ ├── Seconds.scala │ │ ├── apis │ │ │ └── Api.scala │ │ ├── clients │ │ │ └── Client.scala │ │ ├── codes │ │ │ ├── AuthorizationCode.scala │ │ │ ├── StoredAuthorizationCode.scala │ │ │ └── generators │ │ │ │ ├── AuthorizationCodeGenerator.scala │ │ │ │ └── DefaultAuthorizationCodeGenerator.scala │ │ ├── errors │ │ │ ├── AuthorizationError.scala │ │ │ └── TokenError.scala │ │ ├── owners │ │ │ └── ResourceOwner.scala │ │ ├── secrets │ │ │ └── Secret.scala │ │ └── tokens │ │ │ ├── AccessToken.scala │ │ │ ├── AccessTokenWithExpiration.scala │ │ │ ├── RefreshToken.scala │ │ │ ├── StoredRefreshToken.scala │ │ │ ├── TokenType.scala │ │ │ └── generators │ │ │ ├── AccessTokenGenerator.scala │ │ │ ├── JwtBearerAccessTokenGenerator.scala │ │ │ ├── RandomRefreshTokenGenerator.scala │ │ │ └── RefreshTokenGenerator.scala │ │ ├── persistence │ │ ├── apis │ │ │ ├── ApiStore.scala │ │ │ └── DefaultApiStore.scala │ │ ├── clients │ │ │ ├── ClientStore.scala │ │ │ └── DefaultClientStore.scala │ │ ├── codes │ │ │ ├── AuthorizationCodeStore.scala │ │ │ └── DefaultAuthorizationCodeStore.scala │ │ ├── internal │ │ │ └── LegacyKeyValueStore.scala │ │ ├── owners │ │ │ ├── DefaultResourceOwnerStore.scala │ │ │ └── ResourceOwnerStore.scala │ │ └── tokens │ │ │ ├── DefaultRefreshTokenStore.scala │ │ │ └── RefreshTokenStore.scala │ │ └── service │ │ ├── Persistence.scala │ │ ├── Service.scala │ │ ├── SignatureKey.scala │ │ └── bootstrap │ │ ├── ApiBootstrapEntityProvider.scala │ │ ├── ClientBootstrapEntityProvider.scala │ │ └── ResourceOwnerBootstrapEntityProvider.scala │ └── test │ ├── resources │ ├── application-invalid-bootstrap.conf │ ├── application-invalid-config.conf │ ├── application.conf │ ├── bootstrap-integration.conf │ ├── bootstrap-unit.conf │ └── keys │ │ ├── ec.jwk.json │ │ ├── oct.jwk.json │ │ └── rsa.jwk.json │ └── scala │ └── stasis │ └── identity │ ├── EncodingHelpers.scala │ ├── RouteTest.scala │ ├── api │ ├── FormatsSpec.scala │ ├── IdentityEndpointSpec.scala │ ├── JwksSpec.scala │ ├── ManageSpec.scala │ ├── OAuthSpec.scala │ ├── manage │ │ ├── ApisSpec.scala │ │ ├── ClientsSpec.scala │ │ ├── CodesSpec.scala │ │ ├── ManageFixtures.scala │ │ ├── OwnersSpec.scala │ │ ├── TokensSpec.scala │ │ ├── directives │ │ │ ├── UserAuthenticationSpec.scala │ │ │ └── UserAuthorizationSpec.scala │ │ └── requests │ │ │ ├── CreateApiSpec.scala │ │ │ ├── CreateClientSpec.scala │ │ │ ├── CreateOwnerSpec.scala │ │ │ ├── UpdateClientCredentialsSpec.scala │ │ │ ├── UpdateClientSpec.scala │ │ │ ├── UpdateOwnerCredentialsSpec.scala │ │ │ └── UpdateOwnerSpec.scala │ └── oauth │ │ ├── AuthorizationCodeGrantSpec.scala │ │ ├── ClientCredentialsGrantSpec.scala │ │ ├── ImplicitGrantSpec.scala │ │ ├── OAuthFixtures.scala │ │ ├── PkceAuthorizationCodeGrantSpec.scala │ │ ├── RefreshTokenGrantSpec.scala │ │ ├── ResourceOwnerPasswordCredentialsGrantSpec.scala │ │ └── directives │ │ ├── AccessTokenGenerationSpec.scala │ │ ├── AudienceExtractionSpec.scala │ │ ├── AuthorizationCodeConsumptionSpec.scala │ │ ├── AuthorizationCodeGenerationSpec.scala │ │ ├── ClientAuthenticationSpec.scala │ │ ├── ClientRetrievalSpec.scala │ │ ├── RefreshTokenConsumptionSpec.scala │ │ ├── RefreshTokenGenerationSpec.scala │ │ └── ResourceOwnerAuthenticationSpec.scala │ ├── authentication │ ├── manage │ │ └── DefaultResourceOwnerAuthenticatorSpec.scala │ └── oauth │ │ ├── DefaultClientAuthenticatorSpec.scala │ │ ├── DefaultResourceOwnerAuthenticatorSpec.scala │ │ └── EntityAuthenticatorSpec.scala │ ├── model │ ├── Generators.scala │ ├── apis │ │ └── ApiSpec.scala │ ├── clients │ │ └── ClientSpec.scala │ ├── codes │ │ └── generators │ │ │ └── DefaultAuthorizationCodeGeneratorSpec.scala │ ├── owners │ │ └── ResourceOwnerSpec.scala │ ├── secrets │ │ └── SecretSpec.scala │ └── tokens │ │ └── generators │ │ ├── JwtBearerAccessTokenGeneratorBehaviour.scala │ │ ├── JwtBearerAccessTokenGeneratorSpec.scala │ │ └── RandomRefreshTokenGeneratorSpec.scala │ ├── persistence │ ├── apis │ │ └── DefaultApiStoreSpec.scala │ ├── clients │ │ └── DefaultClientStoreSpec.scala │ ├── codes │ │ └── DefaultAuthorizationCodeStoreSpec.scala │ ├── mocks │ │ ├── MockApiStore.scala │ │ ├── MockAuthorizationCodeStore.scala │ │ ├── MockClientStore.scala │ │ ├── MockRefreshTokenStore.scala │ │ └── MockResourceOwnerStore.scala │ ├── owners │ │ └── DefaultResourceOwnerStoreSpec.scala │ └── tokens │ │ └── DefaultRefreshTokenStoreSpec.scala │ └── service │ ├── PersistenceSpec.scala │ ├── ServiceSpec.scala │ ├── SignatureKeySpec.scala │ └── bootstrap │ ├── ApiBootstrapEntityProviderSpec.scala │ ├── ClientBootstrapEntityProviderSpec.scala │ └── ResourceOwnerBootstrapEntityProviderSpec.scala ├── layers ├── README.md └── src │ ├── main │ └── scala │ │ └── stasis │ │ └── layers │ │ ├── api │ │ ├── Formats.scala │ │ ├── Matchers.scala │ │ ├── MessageResponse.scala │ │ ├── Metrics.scala │ │ └── directives │ │ │ ├── EntityDiscardingDirectives.scala │ │ │ └── LoggingDirectives.scala │ │ ├── files │ │ └── FilteringFileVisitor.scala │ │ ├── persistence │ │ ├── KeyValueStore.scala │ │ ├── Metrics.scala │ │ ├── Store.scala │ │ ├── memory │ │ │ └── MemoryStore.scala │ │ └── migration │ │ │ ├── Migration.scala │ │ │ ├── MigrationExecutor.scala │ │ │ └── MigrationResult.scala │ │ ├── security │ │ ├── Metrics.scala │ │ ├── exceptions │ │ │ ├── AuthenticationFailure.scala │ │ │ ├── ProviderFailure.scala │ │ │ └── SecurityFailure.scala │ │ ├── jwt │ │ │ ├── DefaultJwtAuthenticator.scala │ │ │ ├── DefaultJwtProvider.scala │ │ │ ├── JwtAuthenticator.scala │ │ │ └── JwtProvider.scala │ │ ├── keys │ │ │ ├── Generators.scala │ │ │ ├── KeyProvider.scala │ │ │ ├── LocalKeyProvider.scala │ │ │ └── RemoteKeyProvider.scala │ │ ├── oauth │ │ │ ├── DefaultOAuthClient.scala │ │ │ └── OAuthClient.scala │ │ └── tls │ │ │ └── EndpointContext.scala │ │ ├── service │ │ ├── BootstrapProvider.scala │ │ ├── PersistenceProvider.scala │ │ └── bootstrap │ │ │ ├── BootstrapEntityProvider.scala │ │ │ ├── BootstrapExecutor.scala │ │ │ └── BootstrapResult.scala │ │ ├── streaming │ │ └── Operators.scala │ │ └── telemetry │ │ ├── DefaultTelemetryContext.scala │ │ ├── TelemetryContext.scala │ │ └── metrics │ │ ├── MeterExtensions.scala │ │ ├── MetricsExporter.scala │ │ ├── MetricsProvider.scala │ │ └── internal │ │ └── PrometheusProxyRegistry.scala │ └── test │ ├── resources │ ├── application.conf │ ├── bootstrap.conf │ └── logback.xml │ └── scala │ └── stasis │ └── layers │ ├── FileSystemHelpers.scala │ ├── Generators.scala │ ├── UnitSpec.scala │ ├── api │ ├── FormatsSpec.scala │ ├── MatchersSpec.scala │ ├── MetricsSpec.scala │ └── directives │ │ ├── EntityDiscardingDirectivesSpec.scala │ │ └── LoggingDirectivesSpec.scala │ ├── files │ ├── FilteringFileVisitorBehaviour.scala │ └── FilteringFileVisitorSpec.scala │ ├── persistence │ ├── KeyValueStoreBehaviour.scala │ ├── MetricsSpec.scala │ ├── SlickTestDatabase.scala │ ├── memory │ │ └── MemoryStoreSpec.scala │ └── migration │ │ ├── MigrationExecutorSpec.scala │ │ ├── MigrationResultSpec.scala │ │ └── MigrationSpec.scala │ ├── security │ ├── MetricsSpec.scala │ ├── jwt │ │ ├── DefaultJwtAuthenticatorSpec.scala │ │ ├── DefaultJwtProviderSpec.scala │ │ └── JwtAuthenticatorBehaviour.scala │ ├── keys │ │ ├── GeneratorsSpec.scala │ │ ├── LocalKeyProviderSpec.scala │ │ └── RemoteKeyProviderSpec.scala │ ├── mocks │ │ ├── MockJwkProvider.scala │ │ ├── MockJwksEndpoint.scala │ │ ├── MockJwksGenerators.scala │ │ ├── MockJwtEndpoint.scala │ │ ├── MockJwtGenerators.scala │ │ └── MockOAuthClient.scala │ ├── oauth │ │ └── DefaultOAuthClientSpec.scala │ └── tls │ │ └── EndpointContextSpec.scala │ ├── service │ ├── BootstrapProviderSpec.scala │ ├── PersistenceProviderSpec.scala │ └── bootstrap │ │ ├── BootstrapEntityProviderSpec.scala │ │ ├── BootstrapExecutorSpec.scala │ │ ├── BootstrapResultSpec.scala │ │ └── mocks │ │ ├── TestClass.scala │ │ └── TestProvider.scala │ ├── streaming │ └── OperatorsSpec.scala │ └── telemetry │ ├── DefaultTelemetryContextSpec.scala │ ├── MockTelemetryContext.scala │ ├── metrics │ ├── MeterExtensionsSpec.scala │ ├── MetricsExporterSpec.scala │ └── internal │ │ └── PrometheusProxyRegistrySpec.scala │ └── mocks │ ├── MockApiMetrics.scala │ ├── MockMeter.scala │ ├── MockPersistenceMetrics.scala │ └── MockSecurityMetrics.scala ├── project ├── build.properties └── plugins.sbt ├── proto ├── README.md └── src │ ├── main │ ├── protobuf │ │ ├── commands.proto │ │ ├── commands_aux_options.proto │ │ ├── common.proto │ │ ├── common_aux_options.proto │ │ └── stasis.proto │ └── scala │ │ └── stasis │ │ └── core │ │ └── commands │ │ └── proto │ │ ├── CommandSource.scala │ │ └── package.scala │ └── test │ └── scala │ └── stasis │ └── core │ └── commands │ └── proto │ ├── CommandSourceSpec.scala │ └── PackageSpec.scala ├── release.py ├── server-ui ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── deployment │ ├── dev │ │ ├── .env │ │ ├── build.py │ │ ├── docker-compose.yml │ │ ├── identity-bootstrap.conf │ │ ├── run_browser.py │ │ ├── run_server.py │ │ └── server-bootstrap.conf │ └── production │ │ ├── .env.template │ │ ├── Dockerfile │ │ ├── build.py │ │ ├── entrypoint.sh │ │ └── nginx.template ├── lib │ ├── api │ │ ├── api_client.dart │ │ ├── bootstrap_api_client.dart │ │ ├── default_api_client.dart │ │ ├── derived_passwords.dart │ │ └── oauth.dart │ ├── color_schemes.dart │ ├── main.dart │ ├── model │ │ ├── api │ │ │ ├── requests │ │ │ │ ├── create_dataset_definition.dart │ │ │ │ ├── create_device_own.dart │ │ │ │ ├── create_device_privileged.dart │ │ │ │ ├── create_node.dart │ │ │ │ ├── create_schedule.dart │ │ │ │ ├── create_user.dart │ │ │ │ ├── update_dataset_definition.dart │ │ │ │ ├── update_device_limits.dart │ │ │ │ ├── update_device_state.dart │ │ │ │ ├── update_node.dart │ │ │ │ ├── update_schedule.dart │ │ │ │ ├── update_user_limits.dart │ │ │ │ ├── update_user_password.dart │ │ │ │ ├── update_user_permissions.dart │ │ │ │ ├── update_user_salt.dart │ │ │ │ └── update_user_state.dart │ │ │ └── responses │ │ │ │ ├── created_dataset_definition.dart │ │ │ │ ├── created_device.dart │ │ │ │ ├── created_node.dart │ │ │ │ ├── created_schedule.dart │ │ │ │ ├── created_user.dart │ │ │ │ ├── ping.dart │ │ │ │ └── updated_user_salt.dart │ │ ├── commands │ │ │ └── command.dart │ │ ├── datasets │ │ │ ├── dataset_definition.dart │ │ │ └── dataset_entry.dart │ │ ├── devices │ │ │ ├── device.dart │ │ │ ├── device_bootstrap_code.dart │ │ │ └── device_key.dart │ │ ├── formats.dart │ │ ├── manifests │ │ │ └── manifest.dart │ │ ├── nodes │ │ │ ├── crate_store_descriptor.dart │ │ │ └── node.dart │ │ ├── reservations │ │ │ └── crate_storage_reservation.dart │ │ ├── schedules │ │ │ └── schedule.dart │ │ └── users │ │ │ ├── permission.dart │ │ │ └── user.dart │ ├── pages │ │ ├── authorize │ │ │ └── authorization_callback.dart │ │ ├── default │ │ │ ├── components.dart │ │ │ ├── home.dart │ │ │ └── not_found.dart │ │ ├── manage │ │ │ ├── codes.dart │ │ │ ├── commands.dart │ │ │ ├── components │ │ │ │ ├── entity_form.dart │ │ │ │ ├── entity_table.dart │ │ │ │ ├── extensions.dart │ │ │ │ ├── forms │ │ │ │ │ ├── boolean_field.dart │ │ │ │ │ ├── command_parameters_field.dart │ │ │ │ │ ├── crate_store_field.dart │ │ │ │ │ ├── date_time_field.dart │ │ │ │ │ ├── device_limits_field.dart │ │ │ │ │ ├── duration_field.dart │ │ │ │ │ ├── file_size_field.dart │ │ │ │ │ ├── node_address_field.dart │ │ │ │ │ ├── node_field.dart │ │ │ │ │ ├── password_field.dart │ │ │ │ │ ├── policy_field.dart │ │ │ │ │ ├── retention_field.dart │ │ │ │ │ ├── state_field.dart │ │ │ │ │ ├── user_limits_field.dart │ │ │ │ │ └── user_permissions_field.dart │ │ │ │ └── rendering.dart │ │ │ ├── definitions.dart │ │ │ ├── device_keys.dart │ │ │ ├── devices.dart │ │ │ ├── entries.dart │ │ │ ├── nodes.dart │ │ │ ├── reservations.dart │ │ │ ├── schedules.dart │ │ │ └── users.dart │ │ ├── page_destinations.dart │ │ └── page_router.dart │ └── utils │ │ ├── chrono_unit.dart │ │ ├── debouncer.dart │ │ ├── file_size_unit.dart │ │ ├── pair.dart │ │ └── triple.dart ├── pubspec.yaml ├── qa.py ├── test │ ├── api │ │ ├── api_client_test.dart │ │ ├── bootstrap_api_client_test.dart │ │ ├── default_api_client_test.dart │ │ ├── derived_passwords_test.dart │ │ └── oauth_test.dart │ ├── model │ │ ├── api │ │ │ └── requests │ │ │ │ └── create_node_test.dart │ │ ├── devices │ │ │ └── device_bootstrap_code_test.dart │ │ ├── formats_test.dart │ │ ├── nodes │ │ │ ├── crate_store_descriptor_test.dart │ │ │ └── node_test.dart │ │ ├── schedules │ │ │ └── schedule_test.dart │ │ └── users │ │ │ ├── permission_test.dart │ │ │ └── user_test.dart │ └── pages │ │ └── manage │ │ └── components │ │ └── rendering_test.dart └── web │ ├── assets │ └── logo.svg │ ├── index.html │ └── manifest.json ├── server ├── README.md └── src │ ├── main │ ├── resources │ │ ├── example-bootstrap.conf │ │ ├── example-discovery-static.conf │ │ ├── logback.xml │ │ └── reference.conf │ └── scala │ │ └── stasis │ │ └── server │ │ ├── Main.scala │ │ ├── api │ │ ├── ApiEndpoint.scala │ │ ├── BootstrapEndpoint.scala │ │ ├── handlers │ │ │ ├── Rejection.scala │ │ │ └── Sanitizing.scala │ │ └── routes │ │ │ ├── ApiRoutes.scala │ │ │ ├── DatasetDefinitions.scala │ │ │ ├── DatasetEntries.scala │ │ │ ├── DeviceBootstrap.scala │ │ │ ├── Devices.scala │ │ │ ├── Manifests.scala │ │ │ ├── Nodes.scala │ │ │ ├── Reservations.scala │ │ │ ├── RoutesContext.scala │ │ │ ├── Schedules.scala │ │ │ ├── Service.scala │ │ │ ├── Staging.scala │ │ │ └── Users.scala │ │ ├── persistence │ │ ├── datasets │ │ │ ├── DatasetDefinitionStore.scala │ │ │ ├── DatasetEntryStore.scala │ │ │ ├── DefaultDatasetDefinitionStore.scala │ │ │ └── DefaultDatasetEntryStore.scala │ │ ├── devices │ │ │ ├── DefaultDeviceBootstrapCodeStore.scala │ │ │ ├── DefaultDeviceKeyStore.scala │ │ │ ├── DefaultDeviceStore.scala │ │ │ ├── DeviceBootstrapCodeStore.scala │ │ │ ├── DeviceCommandStore.scala │ │ │ ├── DeviceKeyStore.scala │ │ │ └── DeviceStore.scala │ │ ├── manifests │ │ │ └── ServerManifestStore.scala │ │ ├── nodes │ │ │ └── ServerNodeStore.scala │ │ ├── reservations │ │ │ └── ServerReservationStore.scala │ │ ├── schedules │ │ │ ├── DefaultScheduleStore.scala │ │ │ └── ScheduleStore.scala │ │ ├── staging │ │ │ └── ServerStagingStore.scala │ │ └── users │ │ │ ├── DefaultUserStore.scala │ │ │ └── UserStore.scala │ │ ├── security │ │ ├── CredentialsProvider.scala │ │ ├── CurrentUser.scala │ │ ├── DefaultResourceProvider.scala │ │ ├── Resource.scala │ │ ├── ResourceProvider.scala │ │ ├── authenticators │ │ │ ├── BootstrapCodeAuthenticator.scala │ │ │ ├── DefaultBootstrapCodeAuthenticator.scala │ │ │ ├── DefaultUserAuthenticator.scala │ │ │ └── UserAuthenticator.scala │ │ ├── devices │ │ │ ├── DeviceBootstrapCodeGenerator.scala │ │ │ ├── DeviceClientSecretGenerator.scala │ │ │ ├── DeviceCredentialsManager.scala │ │ │ └── IdentityDeviceCredentialsManager.scala │ │ ├── exceptions │ │ │ ├── AuthorizationFailure.scala │ │ │ └── CredentialsManagementFailure.scala │ │ └── users │ │ │ ├── IdentityUserCredentialsManager.scala │ │ │ └── UserCredentialsManager.scala │ │ └── service │ │ ├── CorePersistence.scala │ │ ├── ServerPersistence.scala │ │ ├── Service.scala │ │ └── bootstrap │ │ ├── DatasetDefinitionBootstrapEntityProvider.scala │ │ ├── DeviceBootstrapEntityProvider.scala │ │ ├── NodeBootstrapEntityProvider.scala │ │ ├── ScheduleBootstrapEntityProvider.scala │ │ └── UserBootstrapEntityProvider.scala │ └── test │ ├── resources │ ├── application-device-bootstrap.conf │ ├── application-invalid-bootstrap.conf │ ├── application-invalid-config.conf │ ├── application.conf │ ├── bootstrap-device-additional-config.conf │ ├── bootstrap-integration.conf │ └── bootstrap-unit.conf │ └── scala │ └── stasis │ └── server │ ├── Secrets.scala │ ├── api │ ├── ApiEndpointSpec.scala │ ├── BootstrapEndpointSpec.scala │ ├── handlers │ │ ├── RejectionSpec.scala │ │ └── SanitizingSpec.scala │ └── routes │ │ ├── DatasetDefinitionsSpec.scala │ │ ├── DatasetEntriesSpec.scala │ │ ├── DeviceBootstrapSpec.scala │ │ ├── DevicesSpec.scala │ │ ├── ManifestsSpec.scala │ │ ├── NodesSpec.scala │ │ ├── ReservationsSpec.scala │ │ ├── SchedulesSpec.scala │ │ ├── ServiceSpec.scala │ │ ├── StagingSpec.scala │ │ └── UsersSpec.scala │ ├── persistence │ ├── datasets │ │ ├── DatasetDefinitionStoreSpec.scala │ │ ├── DatasetEntryStoreSpec.scala │ │ ├── DefaultDatasetDefinitionStoreSpec.scala │ │ ├── DefaultDatasetEntryStoreSpec.scala │ │ ├── MockDatasetDefinitionStore.scala │ │ └── MockDatasetEntryStore.scala │ ├── devices │ │ ├── DefaultDeviceBootstrapCodeStoreSpec.scala │ │ ├── DefaultDeviceKeyStoreSpec.scala │ │ ├── DefaultDeviceStoreSpec.scala │ │ ├── DeviceBootstrapCodeStoreSpec.scala │ │ ├── DeviceCommandStoreSpec.scala │ │ ├── DeviceKeyStoreSpec.scala │ │ ├── DeviceStoreSpec.scala │ │ ├── MockCommandStore.scala │ │ ├── MockDeviceBootstrapCodeStore.scala │ │ ├── MockDeviceCommandStore.scala │ │ ├── MockDeviceKeyStore.scala │ │ └── MockDeviceStore.scala │ ├── manifests │ │ └── ServerManifestStoreSpec.scala │ ├── nodes │ │ └── ServerNodeStoreSpec.scala │ ├── reservations │ │ └── ServerReservationStoreSpec.scala │ ├── schedules │ │ ├── DefaultScheduleStoreSpec.scala │ │ ├── MockScheduleStore.scala │ │ └── ScheduleStoreSpec.scala │ ├── staging │ │ └── ServerStagingStoreSpec.scala │ └── users │ │ ├── DefaultUserStoreSpec.scala │ │ ├── MockUserStore.scala │ │ └── UserStoreSpec.scala │ ├── security │ ├── CredentialsProviderSpec.scala │ ├── DefaultResourceProviderSpec.scala │ ├── authenticators │ │ ├── DefaultBootstrapCodeAuthenticatorSpec.scala │ │ └── DefaultUserAuthenticatorSpec.scala │ ├── devices │ │ ├── DeviceBootstrapCodeGeneratorSpec.scala │ │ ├── DeviceClientSecretGeneratorSpec.scala │ │ └── IdentityDeviceCredentialsManagerSpec.scala │ ├── mocks │ │ ├── MockBootstrapCodeAuthenticator.scala │ │ ├── MockDeviceBootstrapCodeGenerator.scala │ │ ├── MockDeviceClientSecretGenerator.scala │ │ ├── MockDeviceCredentialsManager.scala │ │ ├── MockIdentityDeviceManageEndpoint.scala │ │ ├── MockIdentityUserManageEndpoint.scala │ │ ├── MockResourceProvider.scala │ │ ├── MockSimpleJwtEndpoint.scala │ │ ├── MockUserAuthenticator.scala │ │ └── MockUserCredentialsManager.scala │ └── users │ │ └── IdentityUserCredentialsManagerSpec.scala │ └── service │ ├── CorePersistenceSpec.scala │ ├── ServerPersistenceSpec.scala │ ├── ServiceSpec.scala │ └── bootstrap │ ├── DatasetDefinitionBootstrapEntityProviderSpec.scala │ ├── DeviceBootstrapEntityProviderSpec.scala │ ├── NodeBootstrapEntityProviderSpec.scala │ ├── ScheduleBootstrapEntityProviderSpec.scala │ └── UserBootstrapEntityProviderSpec.scala ├── shared ├── README.md └── src │ ├── main │ └── scala │ │ └── stasis │ │ └── shared │ │ ├── api │ │ ├── Formats.scala │ │ ├── requests │ │ │ ├── CreateDatasetDefinition.scala │ │ │ ├── CreateDatasetEntry.scala │ │ │ ├── CreateDeviceOwn.scala │ │ │ ├── CreateDevicePrivileged.scala │ │ │ ├── CreateNode.scala │ │ │ ├── CreateSchedule.scala │ │ │ ├── CreateUser.scala │ │ │ ├── ReEncryptDeviceSecret.scala │ │ │ ├── ResetUserPassword.scala │ │ │ ├── UpdateDatasetDefinition.scala │ │ │ ├── UpdateDevice.scala │ │ │ ├── UpdateDeviceKey.scala │ │ │ ├── UpdateDeviceLimits.scala │ │ │ ├── UpdateDeviceState.scala │ │ │ ├── UpdateNode.scala │ │ │ ├── UpdateSchedule.scala │ │ │ ├── UpdateUser.scala │ │ │ ├── UpdateUserLimits.scala │ │ │ ├── UpdateUserPasswordOwn.scala │ │ │ ├── UpdateUserPermissions.scala │ │ │ ├── UpdateUserSalt.scala │ │ │ ├── UpdateUserSaltOwn.scala │ │ │ └── UpdateUserState.scala │ │ └── responses │ │ │ ├── CreatedDatasetDefinition.scala │ │ │ ├── CreatedDatasetEntry.scala │ │ │ ├── CreatedDevice.scala │ │ │ ├── CreatedNode.scala │ │ │ ├── CreatedSchedule.scala │ │ │ ├── CreatedUser.scala │ │ │ ├── DeletedCommand.scala │ │ │ ├── DeletedDatasetDefinition.scala │ │ │ ├── DeletedDatasetEntry.scala │ │ │ ├── DeletedDevice.scala │ │ │ ├── DeletedDeviceKey.scala │ │ │ ├── DeletedManifest.scala │ │ │ ├── DeletedNode.scala │ │ │ ├── DeletedPendingDestaging.scala │ │ │ ├── DeletedReservation.scala │ │ │ ├── DeletedSchedule.scala │ │ │ ├── DeletedUser.scala │ │ │ ├── Ping.scala │ │ │ └── UpdatedUserSalt.scala │ │ ├── model │ │ ├── datasets │ │ │ ├── DatasetDefinition.scala │ │ │ └── DatasetEntry.scala │ │ ├── devices │ │ │ ├── Device.scala │ │ │ ├── DeviceBootstrapCode.scala │ │ │ ├── DeviceBootstrapParameters.scala │ │ │ └── DeviceKey.scala │ │ ├── schedules │ │ │ └── Schedule.scala │ │ └── users │ │ │ └── User.scala │ │ ├── ops │ │ └── Operation.scala │ │ ├── secrets │ │ ├── DerivedPasswords.scala │ │ └── SecretsConfig.scala │ │ └── security │ │ └── Permission.scala │ └── test │ ├── resources │ └── application.conf │ └── scala │ └── stasis │ └── test │ └── specs │ └── unit │ └── shared │ ├── api │ ├── FormatsSpec.scala │ └── requests │ │ ├── CreateDatasetDefinitionSpec.scala │ │ ├── CreateDatasetEntrySpec.scala │ │ ├── CreateDeviceOwnSpec.scala │ │ ├── CreateDevicePrivilegedSpec.scala │ │ ├── CreateNodeSpec.scala │ │ ├── CreateScheduleSpec.scala │ │ ├── CreateUserSpec.scala │ │ ├── UpdateDatasetDefinitionSpec.scala │ │ ├── UpdateDeviceKeySpec.scala │ │ ├── UpdateDeviceSpec.scala │ │ ├── UpdateNodeSpec.scala │ │ ├── UpdateScheduleSpec.scala │ │ └── UpdateUserSpec.scala │ ├── model │ ├── Generators.scala │ ├── datasets │ │ └── DatasetDefinitionSpec.scala │ ├── devices │ │ ├── DeviceBootstrapCodeSpec.scala │ │ ├── DeviceBootstrapParametersSpec.scala │ │ └── DeviceSpec.scala │ └── schedules │ │ └── ScheduleSpec.scala │ └── secrets │ ├── DerivedPasswordsSpec.scala │ └── SecretsConfigSpec.scala └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | target 3 | classes 4 | 5 | .idea 6 | .idea_modules 7 | .classpath 8 | .project 9 | .settings 10 | *.keystore 11 | .bsp 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /.sbtopts: -------------------------------------------------------------------------------- 1 | -J-XX:MaxMetaspaceSize=1G 2 | -J-Xms1536m 3 | -J-Xmx1536m 4 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | runner.dialect = scala213 2 | version = 3.8.6 3 | maxColumn = 130 4 | continuationIndent.defnSite = 2 5 | rewrite.rules = [RedundantBraces, RedundantParens, SortImports] 6 | rewrite.redundantBraces.parensForOneLineApply = false 7 | rewrite.redundantBraces.generalExpressions = false 8 | docstrings.blankFirstLine = yes 9 | docstrings.style = SpaceAsterisk 10 | docstrings.wrap = no 11 | 12 | fileOverride { 13 | "glob:**/*.sbt" { 14 | align.preset = most 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | As `stasis` is still at version `1.x.x`, this is the only supported version. 5 | 6 | For security updates, check the [wiki](https://github.com/sndnv/stasis/wiki/General-%3A%3A-Compatibility). 7 | 8 | ## Reporting a Vulnerability 9 | A vulnerability can be reported by creating a new issue and selecting the `Report a security vulnerability` option or going directly [here](https://github.com/sndnv/stasis/security/advisories/new). 10 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # stasis / assets 2 | 3 | Image assets used by other submodules/services. 4 | 5 | The files here are the single source of truth for all shared assets. Any changes should be made here first and 6 | synchronized across the submodules via the provided `./refresh_assets.py` script. 7 | -------------------------------------------------------------------------------- /assets/icons/stasis.logo.1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/icons/stasis.logo.1024.png -------------------------------------------------------------------------------- /assets/icons/stasis.logo.128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/icons/stasis.logo.128.png -------------------------------------------------------------------------------- /assets/icons/stasis.logo.16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/icons/stasis.logo.16.png -------------------------------------------------------------------------------- /assets/icons/stasis.logo.256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/icons/stasis.logo.256.png -------------------------------------------------------------------------------- /assets/icons/stasis.logo.32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/icons/stasis.logo.32.png -------------------------------------------------------------------------------- /assets/icons/stasis.logo.512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/icons/stasis.logo.512.png -------------------------------------------------------------------------------- /assets/icons/stasis.logo.64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/icons/stasis.logo.64.png -------------------------------------------------------------------------------- /assets/launchers/stasis.logo.144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/launchers/stasis.logo.144.png -------------------------------------------------------------------------------- /assets/launchers/stasis.logo.192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/launchers/stasis.logo.192.png -------------------------------------------------------------------------------- /assets/launchers/stasis.logo.48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/launchers/stasis.logo.48.png -------------------------------------------------------------------------------- /assets/launchers/stasis.logo.72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/launchers/stasis.logo.72.png -------------------------------------------------------------------------------- /assets/launchers/stasis.logo.96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/launchers/stasis.logo.96.png -------------------------------------------------------------------------------- /assets/launchers/stasis.logo.round.144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/launchers/stasis.logo.round.144.png -------------------------------------------------------------------------------- /assets/launchers/stasis.logo.round.192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/launchers/stasis.logo.round.192.png -------------------------------------------------------------------------------- /assets/launchers/stasis.logo.round.48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/launchers/stasis.logo.round.48.png -------------------------------------------------------------------------------- /assets/launchers/stasis.logo.round.72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/launchers/stasis.logo.round.72.png -------------------------------------------------------------------------------- /assets/launchers/stasis.logo.round.96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/launchers/stasis.logo.round.96.png -------------------------------------------------------------------------------- /assets/screenshots/client_android_screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/screenshots/client_android_screenshot_1.png -------------------------------------------------------------------------------- /assets/screenshots/client_android_screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/screenshots/client_android_screenshot_2.png -------------------------------------------------------------------------------- /assets/screenshots/client_android_screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/screenshots/client_android_screenshot_3.png -------------------------------------------------------------------------------- /assets/screenshots/client_android_screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/screenshots/client_android_screenshot_4.png -------------------------------------------------------------------------------- /assets/screenshots/client_android_screenshot_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/screenshots/client_android_screenshot_5.png -------------------------------------------------------------------------------- /assets/screenshots/client_android_screenshot_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/screenshots/client_android_screenshot_6.png -------------------------------------------------------------------------------- /assets/screenshots/client_ui_screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/screenshots/client_ui_screenshot_1.png -------------------------------------------------------------------------------- /assets/screenshots/client_ui_screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/screenshots/client_ui_screenshot_2.png -------------------------------------------------------------------------------- /assets/screenshots/client_ui_screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/screenshots/client_ui_screenshot_3.png -------------------------------------------------------------------------------- /assets/screenshots/client_ui_screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/assets/screenshots/client_ui_screenshot_4.png -------------------------------------------------------------------------------- /client-android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | -------------------------------------------------------------------------------- /client-android/app/src/androidTest/java/stasis/client_android/mocks/MockSearch.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.mocks 2 | 3 | import stasis.client_android.lib.ops.search.Search 4 | import stasis.client_android.lib.utils.Try 5 | import stasis.client_android.lib.utils.Try.Success 6 | import java.time.Instant 7 | 8 | class MockSearch : Search { 9 | override suspend fun search(query: Regex, until: Instant?): Try = 10 | Success(Search.Result(definitions = emptyMap())) 11 | } 12 | -------------------------------------------------------------------------------- /client-android/app/src/androidTest/java/stasis/client_android/mocks/MockServerMonitor.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.mocks 2 | 3 | import stasis.client_android.lib.ops.monitoring.ServerMonitor 4 | 5 | class MockServerMonitor : ServerMonitor { 6 | override suspend fun stop() = Unit 7 | } 8 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/StasisClientApplication.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class StasisClientApplication : Application() { 8 | val component: ServiceComponent = DaggerServiceComponent.builder().application(this).build() 9 | } 10 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/activities/fragments/rules/RuleSuggestion.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.activities.fragments.rules 2 | 3 | data class RuleSuggestion( 4 | val include: Boolean, 5 | val description: String, 6 | val directory: String, 7 | val pattern: String 8 | ) -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/persistence/rules/RuleEntity.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.persistence.rules 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import stasis.client_android.lib.collection.rules.Rule 6 | import stasis.client_android.lib.model.server.datasets.DatasetDefinitionId 7 | 8 | @Entity(tableName = "rules") 9 | data class RuleEntity( 10 | @PrimaryKey(autoGenerate = true) 11 | val id: Long = 0L, 12 | val operation: Rule.Operation, 13 | val directory: String, 14 | val pattern: String, 15 | val definition: DatasetDefinitionId? 16 | ) 17 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/persistence/schedules/ActiveScheduleEntity.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.persistence.schedules 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import stasis.client_android.lib.model.server.schedules.ScheduleId 6 | 7 | @Entity(tableName = "active_schedules") 8 | data class ActiveScheduleEntity( 9 | @PrimaryKey(autoGenerate = true) 10 | val id: Long = 0L, 11 | val schedule: ScheduleId, 12 | val type: String, 13 | val data: String? 14 | ) 15 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/serialization/ByteStrings.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.serialization 2 | 3 | import okio.ByteString 4 | import okio.ByteString.Companion.toByteString 5 | import java.util.* 6 | 7 | object ByteStrings { 8 | fun ByteString.encodeAsBase64(): String = Base64 9 | .getUrlEncoder() 10 | .withoutPadding() 11 | .encodeToString(this.toByteArray()) 12 | 13 | fun String.decodeFromBase64(): ByteString = 14 | Base64.getUrlDecoder().decode(this).toByteString() 15 | } 16 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/tracking/BackupTrackerManage.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.tracking 2 | 3 | import stasis.client_android.lib.ops.OperationId 4 | 5 | interface BackupTrackerManage { 6 | fun remove(operation: OperationId) 7 | fun clear() 8 | } 9 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/tracking/BackupTrackerView.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.tracking 2 | 3 | import androidx.lifecycle.LiveData 4 | import stasis.client_android.lib.ops.OperationId 5 | import stasis.client_android.lib.tracking.state.BackupState 6 | 7 | interface BackupTrackerView : BackupTrackerManage { 8 | val state: LiveData> 9 | fun updates(operation: OperationId): LiveData 10 | } 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/tracking/DefaultTrackers.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.tracking 2 | 3 | data class DefaultTrackers( 4 | val backup: DefaultBackupTracker, 5 | val recovery: DefaultRecoveryTracker, 6 | val server: DefaultServerTracker 7 | ) 8 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/tracking/RecoveryTrackerManage.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.tracking 2 | 3 | import stasis.client_android.lib.ops.OperationId 4 | 5 | interface RecoveryTrackerManage { 6 | fun remove(operation: OperationId) 7 | fun clear() 8 | } 9 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/tracking/RecoveryTrackerView.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.tracking 2 | 3 | import androidx.lifecycle.LiveData 4 | import stasis.client_android.lib.ops.OperationId 5 | import stasis.client_android.lib.tracking.state.RecoveryState 6 | 7 | interface RecoveryTrackerView : RecoveryTrackerManage { 8 | val state: LiveData> 9 | fun updates(operation: OperationId): LiveData 10 | } 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/tracking/ServerTrackerView.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.tracking 2 | 3 | import androidx.lifecycle.LiveData 4 | import stasis.client_android.lib.tracking.ServerTracker 5 | 6 | interface ServerTrackerView { 7 | val state: LiveData> 8 | fun updates(server: String): LiveData 9 | } 10 | -------------------------------------------------------------------------------- /client-android/app/src/main/java/stasis/client_android/tracking/TrackerViews.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.tracking 2 | 3 | data class TrackerViews( 4 | val backup: BackupTrackerView, 5 | val recovery: RecoveryTrackerView, 6 | val server: ServerTrackerView 7 | ) 8 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/dot_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_about.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_action_details.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_action_edit.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_actions_expand.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_backup.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_commands.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_dot_default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_dot_selected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_entity_state_existing.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_entity_state_new.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_entity_state_updated.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_export.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_filter_on.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_import.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_logout.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_operations.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_pull.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_push.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_recover.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_reset.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_rule_operation_include.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_rules.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_status.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_status_collapse.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_status_expand.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_stop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_tree.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_tree_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_tree_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_tree_directory.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_tree_file.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_update_salt.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/drawable/ic_warning.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/layout/dropdown_duration_type_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/layout/dropdown_policy_type_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/layout/dropdown_schedule_assignment_definition.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/layout/dropdown_schedule_assignment_type_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/layout/fragment_status.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/layout/list_item_dataset_definition_summary.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/layout/list_item_dataset_entry_summary.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/layout/list_item_permission_chip.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/mipmap-anydpi/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client-android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client-android/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | android.enableJetifier=true 3 | android.jetifier.ignorelist=moshi-1.15.1 4 | android.useAndroidX=true 5 | org.gradle.jvmargs=-Xms512m -Xmx2048m 6 | android.nonTransitiveRClass=false 7 | android.nonFinalResIds=false 8 | -------------------------------------------------------------------------------- /client-android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /client-android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jan 22 13:43:54 CET 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 7 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/api/clients/ServerBootstrapEndpointClient.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.api.clients 2 | 3 | import stasis.client_android.lib.model.server.devices.DeviceBootstrapParameters 4 | import stasis.client_android.lib.utils.Try 5 | 6 | interface ServerBootstrapEndpointClient { 7 | val server: String 8 | 9 | suspend fun execute(bootstrapCode: String): Try 10 | 11 | interface Factory { 12 | fun create(server: String): ServerBootstrapEndpointClient 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/api/clients/exceptions/AccessDeniedFailure.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.api.clients.exceptions 2 | 3 | class AccessDeniedFailure : Exception() 4 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/api/clients/exceptions/EndpointFailure.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.api.clients.exceptions 2 | 3 | class EndpointFailure(message: String) : Exception(message) 4 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/api/clients/exceptions/InvalidBootstrapCodeFailure.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.api.clients.exceptions 2 | 3 | class InvalidBootstrapCodeFailure : Exception() 4 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/api/clients/exceptions/ResourceMissingFailure.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.api.clients.exceptions 2 | 3 | class ResourceMissingFailure : Exception() 4 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/collection/BackupCollector.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.collection 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import stasis.client_android.lib.model.SourceEntity 5 | 6 | interface BackupCollector { 7 | fun collect(): Flow 8 | } 9 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/collection/RecoveryCollector.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.collection 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import stasis.client_android.lib.model.TargetEntity 5 | 6 | interface RecoveryCollector { 7 | fun collect(): Flow 8 | } 9 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/collection/rules/exceptions/RuleMatchingFailure.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.collection.rules.exceptions 2 | 3 | class RuleMatchingFailure(message: String, cause: Throwable?) : Exception(message, cause) { 4 | constructor(message: String) : this(message, cause = null) 5 | } 6 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/compression/Compressor.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.compression 2 | 3 | interface Compressor : Encoder, Decoder 4 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/compression/Decoder.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.compression 2 | 3 | import okio.Source 4 | 5 | interface Decoder { 6 | fun name(): String 7 | fun decompress(source: Source): Source 8 | } 9 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/compression/Encoder.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.compression 2 | 3 | import okio.Source 4 | 5 | interface Encoder { 6 | fun name(): String 7 | fun compress(source: Source): Source 8 | } 9 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/compression/Identity.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.compression 2 | 3 | import okio.Source 4 | 5 | object Identity : Compressor { 6 | override fun name(): String = "none" 7 | 8 | override fun decompress(source: Source): Source = 9 | source 10 | 11 | override fun compress(source: Source): Source = 12 | source 13 | } 14 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/discovery/ServiceApiClient.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.discovery 2 | 3 | interface ServiceApiClient { 4 | interface Factory { 5 | fun create(endpoint: ServiceApiEndpoint.Api, coreClient: ServiceApiClient): ServiceApiClient 6 | fun create(endpoint: ServiceApiEndpoint.Core): ServiceApiClient 7 | fun create(endpoint: ServiceApiEndpoint.Discovery): ServiceApiClient 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/discovery/ServiceDiscoveryClient.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.discovery 2 | 3 | import stasis.client_android.lib.utils.Try 4 | 5 | interface ServiceDiscoveryClient : ServiceApiClient { 6 | val attributes: Attributes 7 | 8 | suspend fun latest(isInitialRequest: Boolean): Try 9 | 10 | interface Attributes { 11 | fun asServiceDiscoveryRequest(isInitialRequest: Boolean): ServiceDiscoveryRequest 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/discovery/ServiceDiscoveryRequest.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.discovery 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class ServiceDiscoveryRequest( 8 | @Json(name = "is_initial_request") 9 | val isInitialRequest: Boolean, 10 | val attributes: Map 11 | ) { 12 | val id: String by lazy { 13 | attributes.toList() 14 | .sortedBy { it.first } 15 | .joinToString(separator = "::") { (k, v) -> "$k=$v" } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/discovery/exceptions/DiscoveryFailure.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.discovery.exceptions 2 | 3 | class DiscoveryFailure(message: String) : Exception(message) 4 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/encryption/Decoder.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.encryption 2 | 3 | import okio.Source 4 | import stasis.client_android.lib.encryption.secrets.DeviceFileSecret 5 | import stasis.client_android.lib.encryption.secrets.DeviceMetadataSecret 6 | 7 | interface Decoder { 8 | fun decrypt(source: Source, fileSecret: DeviceFileSecret): Source 9 | fun decrypt(source: Source, metadataSecret: DeviceMetadataSecret): Source 10 | } 11 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/encryption/Encoder.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.encryption 2 | 3 | import okio.Sink 4 | import okio.Source 5 | import stasis.client_android.lib.encryption.secrets.DeviceFileSecret 6 | import stasis.client_android.lib.encryption.secrets.DeviceMetadataSecret 7 | 8 | interface Encoder { 9 | fun encrypt(source: Source, fileSecret: DeviceFileSecret): Source 10 | fun encrypt(sink: Sink, fileSecret: DeviceFileSecret): Sink 11 | fun encrypt(source: Source, metadataSecret: DeviceMetadataSecret): Source 12 | val maxPlaintextSize: Long 13 | } 14 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/encryption/secrets/DeviceMetadataSecret.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.encryption.secrets 2 | 3 | import okio.ByteString 4 | import okio.Source 5 | import stasis.client_android.lib.encryption.Aes 6 | 7 | data class DeviceMetadataSecret( 8 | val iv: ByteString, 9 | private val key: ByteString 10 | ) : Secret() { 11 | fun encryption(source: Source): Source = Aes.encryption(source, key, iv) 12 | fun decryption(source: Source): Source = Aes.decryption(source, key, iv) 13 | } 14 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/model/core/CrateStorageReservation.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.model.core 2 | 3 | import com.squareup.moshi.JsonClass 4 | import java.util.UUID 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class CrateStorageReservation( 8 | val id: CrateStorageReservationId, 9 | val crate: CrateId, 10 | val size: Long, 11 | val copies: Int, 12 | val origin: NodeId, 13 | val target: NodeId 14 | ) 15 | 16 | typealias CrateStorageReservationId = UUID 17 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/model/core/Identifiers.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.model.core 2 | 3 | import java.util.UUID 4 | 5 | typealias CrateId = UUID 6 | typealias NodeId = UUID 7 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/model/core/networking/EndpointAddress.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.model.core.networking 2 | 3 | sealed class EndpointAddress { 4 | data class HttpEndpointAddress( 5 | val uri: String 6 | ) : EndpointAddress() 7 | 8 | data class GrpcEndpointAddress( 9 | val host: String, 10 | val port: Int, 11 | val tlsEnabled: Boolean 12 | ) : EndpointAddress() 13 | } 14 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/model/server/api/requests/ResetUserPassword.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.model.server.api.requests 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class ResetUserPassword( 8 | @Json(name = "raw_password") 9 | val rawPassword: String, 10 | ) 11 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/model/server/api/responses/CreatedDatasetDefinition.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.model.server.api.responses 2 | 3 | import com.squareup.moshi.JsonClass 4 | import stasis.client_android.lib.model.server.datasets.DatasetDefinitionId 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class CreatedDatasetDefinition(val definition: DatasetDefinitionId) 8 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/model/server/api/responses/CreatedDatasetEntry.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.model.server.api.responses 2 | 3 | import com.squareup.moshi.JsonClass 4 | import stasis.client_android.lib.model.server.datasets.DatasetEntryId 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class CreatedDatasetEntry(val entry: DatasetEntryId) 8 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/model/server/api/responses/Ping.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.model.server.api.responses 2 | 3 | import com.squareup.moshi.JsonClass 4 | import java.util.UUID 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class Ping(val id: UUID) 8 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/model/server/api/responses/UpdatedUserSalt.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.model.server.api.responses 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class UpdatedUserSalt( 7 | val salt: String, 8 | ) 9 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/ops/exceptions/EntityProcessingFailure.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.ops.exceptions 2 | 3 | import java.nio.file.Path 4 | 5 | data class EntityProcessingFailure(val entity: Path, override val cause: Throwable) : Exception(cause) 6 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/ops/exceptions/OperationRestrictedFailure.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.ops.exceptions 2 | 3 | import stasis.client_android.lib.ops.Operation 4 | 5 | class OperationRestrictedFailure(val restrictions: List) : Exception() 6 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/ops/monitoring/ServerMonitor.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.ops.monitoring 2 | 3 | interface ServerMonitor { 4 | suspend fun stop() 5 | } 6 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/ops/recovery/stages/internal/DecompressedSource.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.ops.recovery.stages.internal 2 | 3 | import okio.Source 4 | import stasis.client_android.lib.compression.Decoder 5 | 6 | object DecompressedSource { 7 | fun Source.decompress(decompressor: Decoder): Source { 8 | return decompressor.decompress(this) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/ops/scheduling/ActiveSchedule.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.ops.scheduling 2 | 3 | data class ActiveSchedule( 4 | val id: Long, 5 | val assignment: OperationScheduleAssignment 6 | ) 7 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/security/exceptions/ExplicitLogout.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.security.exceptions 2 | 3 | class ExplicitLogout : Exception() 4 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/security/exceptions/InvalidUserCredentials.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.security.exceptions 2 | 3 | class InvalidUserCredentials : Exception() 4 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/security/exceptions/MissingDeviceSecret.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.security.exceptions 2 | 3 | class MissingDeviceSecret : Exception() 4 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/security/exceptions/TokenExpired.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.security.exceptions 2 | 3 | class TokenExpired : Exception() 4 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/staging/FileStaging.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.staging 2 | 3 | import java.nio.file.Path 4 | 5 | interface FileStaging { 6 | suspend fun temporary(): Path 7 | suspend fun discard(file: Path) 8 | suspend fun destage(from: Path, to: Path) 9 | } 10 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/tracking/ServerTracker.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.tracking 2 | 3 | import java.time.Instant 4 | 5 | interface ServerTracker { 6 | fun reachable(server: String) 7 | fun unreachable(server: String) 8 | 9 | data class ServerState( 10 | val reachable: Boolean, 11 | val timestamp: Instant 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/tracking/state/OperationState.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.tracking.state 2 | 3 | import stasis.client_android.lib.ops.Operation 4 | import java.time.Instant 5 | 6 | interface OperationState { 7 | val type: Operation.Type 8 | val started: Instant 9 | val completed: Instant? 10 | fun asProgress(): Operation.Progress 11 | } 12 | -------------------------------------------------------------------------------- /client-android/lib/src/main/kotlin/stasis/client_android/lib/utils/NonFatal.kt: -------------------------------------------------------------------------------- 1 | package stasis.client_android.lib.utils 2 | 3 | object NonFatal { 4 | fun Throwable.isNonFatal(): Boolean = when (this) { 5 | is VirtualMachineError, is ThreadDeath, is InterruptedException, is LinkageError -> false 6 | else -> true 7 | } 8 | 9 | fun Throwable.nonFatal(): Throwable = if (isNonFatal()) { 10 | this 11 | } else { 12 | throw this 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client-android/lib/src/test/kotlin/stasis/test/client_android/lib/api/clients/internal/TestDataClass.kt: -------------------------------------------------------------------------------- 1 | package stasis.test.client_android.lib.api.clients.internal 2 | 3 | data class TestDataClass(val int: Int, val bool: Boolean, val string: String) 4 | -------------------------------------------------------------------------------- /client-android/lib/src/test/kotlin/stasis/test/client_android/lib/await.kt: -------------------------------------------------------------------------------- 1 | package stasis.test.client_android.lib 2 | 3 | import kotlinx.coroutines.delay 4 | import java.time.Duration 5 | 6 | suspend inline fun awaitAndThen( 7 | duration: Duration = Duration.ofSeconds(3), 8 | f: () -> T 9 | ): T { 10 | delay(duration.toMillis()) 11 | return f() 12 | } 13 | -------------------------------------------------------------------------------- /client-android/lib/src/test/kotlin/stasis/test/client_android/lib/mocks/MockBackupCollector.kt: -------------------------------------------------------------------------------- 1 | package stasis.test.client_android.lib.mocks 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.asFlow 5 | import stasis.client_android.lib.collection.BackupCollector 6 | import stasis.client_android.lib.model.SourceEntity 7 | 8 | class MockBackupCollector(val files: List) : BackupCollector { 9 | override fun collect(): Flow = files.asFlow() 10 | } 11 | -------------------------------------------------------------------------------- /client-android/lib/src/test/kotlin/stasis/test/client_android/lib/mocks/MockRecoveryCollector.kt: -------------------------------------------------------------------------------- 1 | package stasis.test.client_android.lib.mocks 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.asFlow 5 | import stasis.client_android.lib.collection.RecoveryCollector 6 | import stasis.client_android.lib.model.TargetEntity 7 | 8 | class MockRecoveryCollector(val files: List) : RecoveryCollector { 9 | override fun collect(): Flow = files.asFlow() 10 | } 11 | -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/analysis/metadata-source-file: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/collection/file-1: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/collection/file-2: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/collection/file-3: -------------------------------------------------------------------------------- 1 | 333 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/collection/other-file-1: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/collection/other-file-2: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/encryption/encrypted-file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-android/lib/src/test/resources/encryption/encrypted-file -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/large-source-file-1: -------------------------------------------------------------------------------- 1 | 95a4fffc-6830-4f34-b60d-2560c59631b4 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/nested/source-file-4: -------------------------------------------------------------------------------- 1 | source-file-4 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/nested/source-file-5: -------------------------------------------------------------------------------- 1 | source-file-5 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/processing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !empty 4 | -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/processing/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-android/lib/src/test/resources/ops/processing/empty -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/recovery/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !empty 4 | -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/recovery/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-android/lib/src/test/resources/ops/recovery/empty -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/scheduling/test.file: -------------------------------------------------------------------------------- 1 | line-01 2 | 3 | // comment-01 4 | line-02 5 | 6 | # comment-02 7 | line-03 8 | -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/scheduling/test.rules: -------------------------------------------------------------------------------- 1 | + /home__/stasis ** # include all files for the current user 2 | - /home__/stasis/.m2 repository/** # exclude maven artifacts 3 | - /home__/stasis/.ivy2 {cache|local}/** # exclude ivy artifacts 4 | 5 | # exclude logs, temporary and cache files 6 | - /home__/stasis **/*.{class|obj} 7 | - /home__/stasis **/lost+found/* 8 | - /home__/stasis **/*cache*/* 9 | - /home__/stasis **/*log*/* 10 | - /home__/stasis **/*.{tmp|temp|part|bak|~} 11 | -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/source-file-1: -------------------------------------------------------------------------------- 1 | source-file-1 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/source-file-2: -------------------------------------------------------------------------------- 1 | source-file-2 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/source-file-3: -------------------------------------------------------------------------------- 1 | source-file-3 -------------------------------------------------------------------------------- /client-android/lib/src/test/resources/ops/temp-file-1: -------------------------------------------------------------------------------- 1 | temp-file-1 -------------------------------------------------------------------------------- /client-android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "stasis-client-android" 2 | 3 | include(":lib", ":app") 4 | -------------------------------------------------------------------------------- /client-cli/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source=client_cli 3 | -------------------------------------------------------------------------------- /client-cli/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | **/Dockerfile 4 | **/*.Dockerfile 5 | .dockerignore 6 | .idea 7 | .idea_modules 8 | .classpath 9 | .project 10 | .settings 11 | venv 12 | .venv 13 | *.log 14 | __pycache__ 15 | .coverage 16 | .coveragerc 17 | cover 18 | *.iml 19 | *.db 20 | .pylintrc 21 | *.egg-info 22 | qa.py 23 | tests 24 | /Users 25 | /home 26 | -------------------------------------------------------------------------------- /client-cli/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .idea_modules 3 | .classpath 4 | .project 5 | .settings 6 | venv 7 | *.log 8 | __pycache__ 9 | .coverage 10 | .coverage.xml 11 | cover 12 | htmlcov 13 | *.iml 14 | *.db 15 | *.egg-info 16 | build 17 | dist 18 | -------------------------------------------------------------------------------- /client-cli/client_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-cli/client_cli/__init__.py -------------------------------------------------------------------------------- /client-cli/client_cli/cli/context.py: -------------------------------------------------------------------------------- 1 | """CLI context container.""" 2 | 3 | 4 | class Context: 5 | """CLI context container.""" 6 | 7 | def __init__(self): 8 | self.api = None 9 | self.init = None 10 | self.service_binary = None 11 | self.service_main_class = None 12 | self.filtering = None 13 | self.sorting = None 14 | self.rendering = None 15 | -------------------------------------------------------------------------------- /client-cli/client_cli/render/default/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-cli/client_cli/render/default/__init__.py -------------------------------------------------------------------------------- /client-cli/client_cli/render/flatten/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-cli/client_cli/render/flatten/__init__.py -------------------------------------------------------------------------------- /client-cli/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-cli/tests/__init__.py -------------------------------------------------------------------------------- /client-cli/tests/api/test_inactive_init_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from click import Abort 4 | 5 | from client_cli.api.inactive_init_api import InactiveInitApi 6 | 7 | 8 | class InactiveInitApiSpec(unittest.TestCase): 9 | 10 | def test_should_fail_all_requests(self): 11 | api = InactiveInitApi() 12 | 13 | with self.assertRaises(Abort): 14 | api.state() 15 | 16 | with self.assertRaises(Abort): 17 | api.provide_credentials(username="username", password="password") 18 | -------------------------------------------------------------------------------- /client-cli/tests/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-cli/tests/cli/__init__.py -------------------------------------------------------------------------------- /client-cli/tests/cli/cli_runner.py: -------------------------------------------------------------------------------- 1 | import click 2 | from click.testing import CliRunner 3 | 4 | 5 | class Runner: 6 | def __init__(self, cli=None): 7 | self.runner = CliRunner() 8 | 9 | if cli: 10 | self.cli = cli 11 | else: 12 | @click.group() 13 | def empty_cli(): 14 | pass 15 | 16 | self.cli = empty_cli 17 | 18 | def with_command(self, command): 19 | self.cli.add_command(command) 20 | return self 21 | 22 | def invoke(self, args, obj=None): 23 | return self.runner.invoke(self.cli, args=args, obj=obj) 24 | -------------------------------------------------------------------------------- /client-cli/tests/cli/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-cli/tests/cli/common/__init__.py -------------------------------------------------------------------------------- /client-cli/tests/mocks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-cli/tests/mocks/__init__.py -------------------------------------------------------------------------------- /client-cli/tests/render/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-cli/tests/render/__init__.py -------------------------------------------------------------------------------- /client-cli/tests/render/default/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-cli/tests/render/default/__init__.py -------------------------------------------------------------------------------- /client-cli/tests/render/flatten/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-cli/tests/render/flatten/__init__.py -------------------------------------------------------------------------------- /client-ui/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | prefer_single_quotes: true 6 | always_use_package_imports: true 7 | avoid_type_to_string: true 8 | 9 | analyzer: 10 | exclude: 11 | - "**/*.g.dart" 12 | - "**/*.freezed.dart" 13 | errors: 14 | invalid_annotation_target: ignore 15 | -------------------------------------------------------------------------------- /client-ui/lib/config/api_token.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | class ApiTokenFactory { 4 | static String? load({required String path}) { 5 | try { 6 | return File(path).readAsStringSync(); 7 | } on FileSystemException { 8 | return null; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client-ui/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:stasis_client_ui/client_app.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() { 5 | runApp(const ClientApp()); 6 | } 7 | -------------------------------------------------------------------------------- /client-ui/lib/model/api/requests/update_user_password.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_user_password.freezed.dart'; 4 | part 'update_user_password.g.dart'; 5 | 6 | @freezed 7 | class UpdateUserPassword with _$UpdateUserPassword { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateUserPassword({ 10 | required String currentPassword, 11 | required String newPassword, 12 | }) = _UpdateUserPassword; 13 | 14 | factory UpdateUserPassword.fromJson(Map json) => _$UpdateUserPasswordFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /client-ui/lib/model/api/requests/update_user_salt.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_user_salt.freezed.dart'; 4 | part 'update_user_salt.g.dart'; 5 | 6 | @freezed 7 | class UpdateUserSalt with _$UpdateUserSalt { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateUserSalt({ 10 | required String currentPassword, 11 | required String newSalt, 12 | }) = _UpdateUserSalt; 13 | 14 | factory UpdateUserSalt.fromJson(Map json) => _$UpdateUserSaltFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /client-ui/lib/model/api/responses/created_dataset_definition.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'created_dataset_definition.freezed.dart'; 4 | part 'created_dataset_definition.g.dart'; 5 | 6 | @freezed 7 | class CreatedDatasetDefinition with _$CreatedDatasetDefinition { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory CreatedDatasetDefinition({ 10 | required String definition, 11 | }) = _CreatedDatasetDefinition; 12 | 13 | factory CreatedDatasetDefinition.fromJson(Map json) => _$CreatedDatasetDefinitionFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /client-ui/lib/model/api/responses/operation_started.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'operation_started.freezed.dart'; 4 | part 'operation_started.g.dart'; 5 | 6 | @freezed 7 | class OperationStarted with _$OperationStarted { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory OperationStarted({ 10 | required String operation, 11 | }) = _OperationStarted; 12 | 13 | factory OperationStarted.fromJson(Map json) => _$OperationStartedFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /client-ui/lib/model/devices/server_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'server_state.freezed.dart'; 4 | part 'server_state.g.dart'; 5 | 6 | @freezed 7 | class ServerState with _$ServerState { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory ServerState({ 10 | required bool reachable, 11 | required DateTime timestamp, 12 | }) = _ServerState; 13 | 14 | factory ServerState.fromJson(Map json) => _$ServerStateFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /client-ui/lib/model/formats.dart: -------------------------------------------------------------------------------- 1 | Duration durationFromJson(int duration) => Duration(seconds: duration); 2 | 3 | int durationToJson(Duration duration) => duration.inSeconds; 4 | 5 | DateTime dateTimeFromJson(String dateTime) => DateTime.parse(dateTime); 6 | 7 | String dateTimeToJson(DateTime dateTime) => dateTime.toIso8601String(); 8 | -------------------------------------------------------------------------------- /client-ui/lib/model/service/ping.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'ping.freezed.dart'; 4 | part 'ping.g.dart'; 5 | 6 | @freezed 7 | class Ping with _$Ping { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory Ping({ 10 | required String id, 11 | }) = _Ping; 12 | 13 | factory Ping.fromJson(Map json) => _$PingFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /client-ui/lib/pages/components/context/entry_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class EntryAction { 4 | const EntryAction({ 5 | required this.icon, 6 | required this.name, 7 | required this.description, 8 | required this.handler, 9 | this.color, 10 | }); 11 | 12 | final IconData icon; 13 | final String name; 14 | final String description; 15 | final void Function() handler; 16 | final Color? color; 17 | } 18 | -------------------------------------------------------------------------------- /client-ui/lib/pages/components/sizing.dart: -------------------------------------------------------------------------------- 1 | class Sizing { 2 | static const double xs = 576.0; 3 | static const double sm = 768.0; 4 | static const double lg = 992.0; 5 | static const double xl = 1200.0; 6 | } 7 | -------------------------------------------------------------------------------- /client-ui/lib/utils/chrono_unit.dart: -------------------------------------------------------------------------------- 1 | enum ChronoUnit { 2 | seconds(singular: 'second', plural: 'seconds'), 3 | minutes(singular: 'minute', plural: 'minutes'), 4 | hours(singular: 'hour', plural: 'hours'), 5 | days(singular: 'day', plural: 'days'), 6 | months(singular: 'month', plural: 'months'), 7 | years(singular: 'year', plural: 'years'); 8 | 9 | const ChronoUnit({required this.singular, required this.plural}); 10 | final String singular; 11 | final String plural; 12 | } 13 | -------------------------------------------------------------------------------- /client-ui/lib/utils/debouncer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | class Debouncer { 4 | Debouncer({required this.timeout}); 5 | 6 | final Duration timeout; 7 | Timer? _timer; 8 | 9 | void run(void Function() action) { 10 | _timer?.cancel(); 11 | _timer = Timer(timeout, action); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client-ui/lib/utils/env.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | int getConfiguredTimeout({int defaultTimeout = 30}) { 4 | return int.tryParse(Platform.environment['STASIS_CLIENT_UI_TIMEOUT'] ?? '') ?? defaultTimeout; 5 | } 6 | -------------------------------------------------------------------------------- /client-ui/lib/utils/file_size_unit.dart: -------------------------------------------------------------------------------- 1 | enum FileSizeUnit { 2 | bytes(symbol: 'B'), 3 | kilobytes(symbol: 'kB'), 4 | megabytes(symbol: 'MB'), 5 | gigabytes(symbol: 'GB'), 6 | terabytes(symbol: 'TB'), 7 | petabytes(symbol: 'PB'); 8 | 9 | const FileSizeUnit({required this.symbol}); 10 | final String symbol; 11 | } 12 | -------------------------------------------------------------------------------- /client-ui/lib/utils/pair.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | class Pair { 4 | A a; 5 | B b; 6 | 7 | Pair(this.a, this.b); 8 | 9 | @override 10 | String toString() => '($a,$b)'; 11 | 12 | @override 13 | bool operator ==(Object other) => 14 | other is Pair && this.a == other.a && this.b == other.b; 15 | 16 | @override 17 | int get hashCode => Object.hash( 18 | runtimeType, 19 | const DeepCollectionEquality().hash(a), 20 | const DeepCollectionEquality().hash(b), 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /client-ui/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /client-ui/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void fl_register_plugins(FlPluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /client-ui/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /client-ui/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /client-ui/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /client-ui/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /client-ui/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /client-ui/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /client-ui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client-ui/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client-ui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client-ui/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /client-ui/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /client-ui/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /client-ui/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client-ui/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client-ui/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client-ui/test/resources/api_token: -------------------------------------------------------------------------------- 1 | test-token -------------------------------------------------------------------------------- /client-ui/test/resources/app_files/api-token: -------------------------------------------------------------------------------- 1 | test-token -------------------------------------------------------------------------------- /client-ui/test/resources/app_files/client.conf: -------------------------------------------------------------------------------- 1 | stasis { 2 | client { 3 | ops { 4 | backup { 5 | rules-file = "client.rules" 6 | } 7 | 8 | scheduling { 9 | schedules-file = "client.schedules" 10 | } 11 | } 12 | } 13 | } 14 | 15 | a { 16 | b { 17 | c = test-value 18 | } 19 | } -------------------------------------------------------------------------------- /client-ui/test/resources/app_files/client.rules: -------------------------------------------------------------------------------- 1 | test-rules 2 | -------------------------------------------------------------------------------- /client-ui/test/resources/app_files/client.schedules: -------------------------------------------------------------------------------- 1 | test-schedules -------------------------------------------------------------------------------- /client-ui/test/resources/app_files/invalid/client.conf: -------------------------------------------------------------------------------- 1 | stasis { 2 | client { 3 | ops { 4 | backup { 5 | rules-file = "missing.rules" 6 | } 7 | 8 | scheduling { 9 | schedules-file = "missing.schedules" 10 | } 11 | } 12 | } 13 | } 14 | 15 | a { 16 | b { 17 | c = test-value 18 | } 19 | } -------------------------------------------------------------------------------- /client-ui/test/resources/invalid.conf: -------------------------------------------------------------------------------- 1 | i = 2 | -------------------------------------------------------------------------------- /client-ui/test/resources/invalid.json: -------------------------------------------------------------------------------- 1 | i = 2 | -------------------------------------------------------------------------------- /client-ui/test/resources/localhost.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client-ui/test/resources/localhost.p12 -------------------------------------------------------------------------------- /client-ui/test/resources/secret.conf: -------------------------------------------------------------------------------- 1 | a { 2 | b { 3 | c = "test-value" 4 | d = null 5 | password = "test-password" 6 | secret = "test-secret" 7 | } 8 | } -------------------------------------------------------------------------------- /client-ui/test/resources/valid.conf: -------------------------------------------------------------------------------- 1 | a { # comment 1 2 | b { // comment 2 3 | c { 4 | // comment 3 5 | # comment 4 6 | d = "value-1" # comment 5 7 | d = ${?TEST_ABCD} // comment 6 8 | e = yes 9 | e = ${?TEST_ABCE} 10 | } 11 | } 12 | 13 | f { 14 | g = null 15 | g = ${?TEST_AFG} 16 | } 17 | 18 | h = 1 19 | } 20 | 21 | i = 3 seconds 22 | 23 | j { 24 | k = other test 25 | l = 128M 26 | m = 4.2 27 | } 28 | 29 | n { 30 | // empty 31 | } 32 | 33 | flutter-test = false 34 | flutter-test = ${?FLUTTER_TEST} 35 | -------------------------------------------------------------------------------- /client-ui/test/resources/valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": { 3 | "b": { 4 | "c": { 5 | "d": "value-1", 6 | "e": true 7 | } 8 | }, 9 | "f": { 10 | "g": null 11 | }, 12 | "h": 1 13 | }, 14 | "i": "3 seconds", 15 | "j": { 16 | "k": "other test", 17 | "l": "128M", 18 | "m": 4.2 19 | }, 20 | "n": {}, 21 | "flutter-test": true 22 | } -------------------------------------------------------------------------------- /client-ui/test/utils/pair_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:stasis_client_ui/utils/pair.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | group('A Pair should', () { 6 | test('support comparing pairs', () async { 7 | expect(Pair(1, 2) == Pair(1, 2), true); 8 | expect(Pair(1, 2) == Pair(3, 4), false); 9 | expect(Pair('a', 2) == Pair('a', 2), true); 10 | }); 11 | 12 | test('support converting pairs to strings', () async { 13 | expect(Pair(1, 2).toString(), '(1,2)'); 14 | expect(Pair('a', 2).toString(), '(a,2)'); 15 | }); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /client/src/main/resources/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client/src/main/resources/assets/logo.png -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/Main.scala: -------------------------------------------------------------------------------- 1 | package stasis.client 2 | 3 | import stasis.client.service.Service 4 | 5 | object Main extends App with Service with Service.Arguments 6 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/api/clients/ServerBootstrapEndpointClient.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.api.clients 2 | 3 | import scala.concurrent.Future 4 | 5 | import stasis.shared.model.devices.DeviceBootstrapParameters 6 | 7 | trait ServerBootstrapEndpointClient { 8 | def server: String 9 | 10 | def execute(bootstrapCode: String): Future[DeviceBootstrapParameters] 11 | } 12 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/api/clients/exceptions/ServerApiFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.api.clients.exceptions 2 | 3 | import org.apache.pekko.http.scaladsl.model.StatusCode 4 | 5 | class ServerApiFailure(val status: StatusCode, val message: String) extends Exception(message) 6 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/api/clients/exceptions/ServerBootstrapFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.api.clients.exceptions 2 | 3 | class ServerBootstrapFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/api/http/routes/ApiRoutes.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.api.http.routes 2 | 3 | import org.slf4j.Logger 4 | 5 | import stasis.client.api.Context 6 | import stasis.layers.api.directives.EntityDiscardingDirectives 7 | 8 | trait ApiRoutes extends EntityDiscardingDirectives { 9 | def log(implicit context: Context): Logger = context.log 10 | } 11 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/collection/rules/exceptions/RuleMatchingFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.collection.rules.exceptions 2 | 3 | class RuleMatchingFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/collection/rules/exceptions/RuleParsingFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.collection.rules.exceptions 2 | 3 | class RuleParsingFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/collection/rules/internal/IndexedRule.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.collection.rules.internal 2 | 3 | import stasis.client.collection.rules.Rule 4 | 5 | final case class IndexedRule( 6 | index: Int, 7 | underlying: Rule 8 | ) 9 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/compression/Decoder.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.compression 2 | 3 | import org.apache.pekko.NotUsed 4 | import org.apache.pekko.stream.scaladsl.Flow 5 | import org.apache.pekko.util.ByteString 6 | 7 | trait Decoder { 8 | def name: String 9 | def decompress: Flow[ByteString, ByteString, NotUsed] 10 | } 11 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/compression/Deflate.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.compression 2 | 3 | import org.apache.pekko.NotUsed 4 | import org.apache.pekko.stream.scaladsl.Flow 5 | import org.apache.pekko.stream.scaladsl.{Compression => PekkoCompression} 6 | import org.apache.pekko.util.ByteString 7 | 8 | object Deflate extends Encoder with Decoder { 9 | override val name: String = "deflate" 10 | override def compress: Flow[ByteString, ByteString, NotUsed] = PekkoCompression.deflate 11 | override def decompress: Flow[ByteString, ByteString, NotUsed] = PekkoCompression.inflate() 12 | } 13 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/compression/Encoder.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.compression 2 | 3 | import org.apache.pekko.NotUsed 4 | import org.apache.pekko.stream.scaladsl.Flow 5 | import org.apache.pekko.util.ByteString 6 | 7 | trait Encoder { 8 | def name: String 9 | def compress: Flow[ByteString, ByteString, NotUsed] 10 | } 11 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/compression/Gzip.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.compression 2 | 3 | import org.apache.pekko.NotUsed 4 | import org.apache.pekko.stream.scaladsl.Flow 5 | import org.apache.pekko.stream.scaladsl.{Compression => PekkoCompression} 6 | import org.apache.pekko.util.ByteString 7 | 8 | object Gzip extends Encoder with Decoder { 9 | override val name: String = "gzip" 10 | override def compress: Flow[ByteString, ByteString, NotUsed] = PekkoCompression.gzip 11 | override def decompress: Flow[ByteString, ByteString, NotUsed] = PekkoCompression.gunzip() 12 | } 13 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/compression/Identity.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.compression 2 | 3 | import org.apache.pekko.NotUsed 4 | import org.apache.pekko.stream.scaladsl.Flow 5 | import org.apache.pekko.util.ByteString 6 | 7 | object Identity extends Encoder with Decoder { 8 | override val name: String = "none" 9 | override def compress: Flow[ByteString, ByteString, NotUsed] = Flow[ByteString] 10 | override def decompress: Flow[ByteString, ByteString, NotUsed] = Flow[ByteString] 11 | } 12 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/encryption/Decoder.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.encryption 2 | 3 | import org.apache.pekko.NotUsed 4 | import org.apache.pekko.stream.scaladsl.Flow 5 | import org.apache.pekko.util.ByteString 6 | 7 | import stasis.client.encryption.secrets.DeviceFileSecret 8 | import stasis.client.encryption.secrets.DeviceMetadataSecret 9 | 10 | trait Decoder { 11 | def decrypt(fileSecret: DeviceFileSecret): Flow[ByteString, ByteString, NotUsed] 12 | def decrypt(metadataSecret: DeviceMetadataSecret): Flow[ByteString, ByteString, NotUsed] 13 | } 14 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/encryption/Encoder.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.encryption 2 | 3 | import org.apache.pekko.NotUsed 4 | import org.apache.pekko.stream.scaladsl.Flow 5 | import org.apache.pekko.util.ByteString 6 | 7 | import stasis.client.encryption.secrets.DeviceFileSecret 8 | import stasis.client.encryption.secrets.DeviceMetadataSecret 9 | 10 | trait Encoder { 11 | def encrypt(fileSecret: DeviceFileSecret): Flow[ByteString, ByteString, NotUsed] 12 | def encrypt(metadataSecret: DeviceMetadataSecret): Flow[ByteString, ByteString, NotUsed] 13 | def maxPlaintextSize: Long 14 | } 15 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/ParallelismConfig.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops 2 | 3 | final case class ParallelismConfig(entities: Int, entityParts: Int) 4 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/backup/stages/internal/CompressedByteStringSource.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.backup.stages.internal 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.stream.IOResult 6 | import org.apache.pekko.stream.scaladsl.Source 7 | import org.apache.pekko.util.ByteString 8 | 9 | import stasis.client.compression.Encoder 10 | 11 | class CompressedByteStringSource(val source: Source[ByteString, Future[IOResult]]) { 12 | def compress(compressor: Encoder): source.Repr[ByteString] = 13 | source.via(compressor.compress) 14 | } 15 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/commands/ProcessedCommand.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.commands 2 | 3 | import stasis.core.commands.proto.Command 4 | 5 | final case class ProcessedCommand(command: Command, isProcessed: Boolean) 6 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/exceptions/EntityDiscardFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.exceptions 2 | 3 | class EntityDiscardFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/exceptions/EntityMergeFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.exceptions 2 | 3 | class EntityMergeFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/exceptions/EntityProcessingFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.exceptions 2 | 3 | import java.nio.file.Path 4 | 5 | final case class EntityProcessingFailure(entity: Path, cause: Throwable) extends Exception(cause) 6 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/exceptions/OperationExecutionFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.exceptions 2 | 3 | class OperationExecutionFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/exceptions/OperationStopped.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.exceptions 2 | 3 | import scala.util.control.NonFatal 4 | 5 | final case class OperationStopped(message: String) extends Exception(message) 6 | 7 | object OperationStopped { 8 | def unapply(t: Throwable): Option[Throwable] = 9 | Option(t).flatMap { 10 | case NonFatal(e: OperationStopped) => Some(e) 11 | case NonFatal(e) => unapply(e.getCause) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/exceptions/ScheduleAssignmentParsingFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.exceptions 2 | 3 | class ScheduleAssignmentParsingFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/exceptions/ScheduleRetrievalFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.exceptions 2 | 3 | final case class ScheduleRetrievalFailure(message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/monitoring/ServerMonitor.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.monitoring 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.Done 6 | 7 | trait ServerMonitor { 8 | def stop(): Future[Done] 9 | } 10 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/ops/recovery/stages/internal/DecompressedByteStringSource.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.ops.recovery.stages.internal 2 | 3 | import org.apache.pekko.NotUsed 4 | import org.apache.pekko.stream.scaladsl.Source 5 | import org.apache.pekko.util.ByteString 6 | 7 | import stasis.client.compression.Decoder 8 | 9 | class DecompressedByteStringSource(val source: Source[ByteString, NotUsed]) { 10 | def decompress(decompressor: Decoder): source.Repr[ByteString] = 11 | source.via(decompressor.decompress) 12 | } 13 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/security/CredentialsProvider.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.security 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.http.scaladsl.model.headers.HttpCredentials 6 | 7 | trait CredentialsProvider { 8 | def core: Future[HttpCredentials] 9 | def api: Future[HttpCredentials] 10 | } 11 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/security/FrontendAuthenticator.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.security 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.Done 6 | import org.apache.pekko.http.scaladsl.model.headers.HttpCredentials 7 | 8 | trait FrontendAuthenticator { 9 | def authenticate(credentials: HttpCredentials): Future[Done] 10 | } 11 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/staging/FileStaging.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.staging 2 | 3 | import java.nio.file.Path 4 | 5 | import scala.concurrent.Future 6 | 7 | import org.apache.pekko.Done 8 | 9 | trait FileStaging { 10 | def temporary(): Future[Path] 11 | def discard(file: Path): Future[Done] 12 | def destage(from: Path, to: Path): Future[Done] 13 | } 14 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/tracking/TrackerViews.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.tracking 2 | 3 | trait TrackerViews { 4 | def backup: BackupTracker.View with BackupTracker.Manage 5 | def recovery: RecoveryTracker.View with RecoveryTracker.Manage 6 | def server: ServerTracker.View 7 | } 8 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/tracking/Trackers.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.tracking 2 | 3 | final case class Trackers( 4 | backup: BackupTracker, 5 | recovery: RecoveryTracker, 6 | server: ServerTracker 7 | ) { parent => 8 | def views: TrackerViews = new TrackerViews { 9 | override val backup: BackupTracker.View with BackupTracker.Manage = parent.backup 10 | override val recovery: RecoveryTracker.View with RecoveryTracker.Manage = parent.recovery 11 | override val server: ServerTracker.View = parent.server 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/main/scala/stasis/client/tracking/state/OperationState.scala: -------------------------------------------------------------------------------- 1 | package stasis.client.tracking.state 2 | 3 | import java.time.Instant 4 | 5 | import stasis.shared.ops.Operation 6 | 7 | trait OperationState { 8 | def `type`: Operation.Type 9 | def started: Instant 10 | def isCompleted: Boolean 11 | def asProgress: Operation.Progress 12 | } 13 | -------------------------------------------------------------------------------- /client/src/test/resources/analysis/metadata-source-file: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet -------------------------------------------------------------------------------- /client/src/test/resources/collection/file-1: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /client/src/test/resources/collection/file-2: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /client/src/test/resources/collection/file-3: -------------------------------------------------------------------------------- 1 | 333 -------------------------------------------------------------------------------- /client/src/test/resources/collection/other-file-1: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /client/src/test/resources/collection/other-file-2: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /client/src/test/resources/encryption/encrypted-file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client/src/test/resources/encryption/encrypted-file -------------------------------------------------------------------------------- /client/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker : -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /client/src/test/resources/ops/large-source-file: -------------------------------------------------------------------------------- 1 | 95a4fffc-6830-4f34-b60d-2560c59631b4 -------------------------------------------------------------------------------- /client/src/test/resources/ops/nested/source-file-4: -------------------------------------------------------------------------------- 1 | source-file-4 -------------------------------------------------------------------------------- /client/src/test/resources/ops/nested/source-file-5: -------------------------------------------------------------------------------- 1 | source-file-5 -------------------------------------------------------------------------------- /client/src/test/resources/ops/processing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !empty 4 | -------------------------------------------------------------------------------- /client/src/test/resources/ops/processing/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client/src/test/resources/ops/processing/empty -------------------------------------------------------------------------------- /client/src/test/resources/ops/recovery/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !empty 4 | -------------------------------------------------------------------------------- /client/src/test/resources/ops/recovery/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/client/src/test/resources/ops/recovery/empty -------------------------------------------------------------------------------- /client/src/test/resources/ops/scheduling/extra.rules: -------------------------------------------------------------------------------- 1 | definition = 9457cdf4-85b2-445b-a5b1-ee7e7f5dc50c 2 | definition = 3eee3d55-557d-4be7-8075-f802437c99a1 3 | definition = c0378fd9-9f77-45b7-99e6-9428743b6a91 # latest is always used 4 | 5 | + /home__/stasis ** # include all files for the current user 6 | - /home__/stasis/test ** # exclude test directory 7 | -------------------------------------------------------------------------------- /client/src/test/resources/ops/scheduling/test.file: -------------------------------------------------------------------------------- 1 | line-01 2 | 3 | // comment-01 4 | line-02 5 | 6 | # comment-02 7 | line-03 8 | -------------------------------------------------------------------------------- /client/src/test/resources/ops/scheduling/test.rules: -------------------------------------------------------------------------------- 1 | + /home__/stasis ** # include all files for the current user 2 | - /home__/stasis/.m2 repository/** # exclude maven artifacts 3 | - /home__/stasis/.ivy2 {cache|local}/** # exclude ivy artifacts 4 | 5 | # exclude logs, temporary and cache files 6 | - /home__/stasis **/*.{class|obj} 7 | - /home__/stasis **/lost+found/* 8 | - /home__/stasis **/*cache*/* 9 | - /home__/stasis **/*log*/* 10 | - /home__/stasis **/*.{tmp|temp|part|bak|~} 11 | -------------------------------------------------------------------------------- /client/src/test/resources/ops/source-file-1: -------------------------------------------------------------------------------- 1 | source-file-1 -------------------------------------------------------------------------------- /client/src/test/resources/ops/source-file-2: -------------------------------------------------------------------------------- 1 | source-file-2 -------------------------------------------------------------------------------- /client/src/test/resources/ops/source-file-3: -------------------------------------------------------------------------------- 1 | source-file-3 -------------------------------------------------------------------------------- /client/src/test/resources/ops/temp-file-1: -------------------------------------------------------------------------------- 1 | temp-file-1 -------------------------------------------------------------------------------- /client/src/test/scala/stasis/test/specs/unit/client/EncodingHelpers.scala: -------------------------------------------------------------------------------- 1 | package stasis.test.specs.unit.client 2 | 3 | import java.util.Base64 4 | 5 | import org.apache.pekko.util.ByteString 6 | 7 | trait EncodingHelpers { 8 | implicit class ByteStringToBase64(raw: ByteString) { 9 | def encodeAsBase64: String = Base64.getMimeEncoder().encodeToString(raw.toArray) 10 | } 11 | 12 | implicit class Base64StringToByteString(raw: String) { 13 | def decodeFromBase64: ByteString = ByteString.fromArray(Base64.getMimeDecoder.decode(raw)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/test/scala/stasis/test/specs/unit/client/mocks/MockBackupCollector.scala: -------------------------------------------------------------------------------- 1 | package stasis.test.specs.unit.client.mocks 2 | 3 | import org.apache.pekko.NotUsed 4 | import org.apache.pekko.stream.scaladsl.Source 5 | 6 | import stasis.client.collection.BackupCollector 7 | import stasis.client.model.SourceEntity 8 | 9 | class MockBackupCollector(files: List[SourceEntity]) extends BackupCollector { 10 | override def collect(): Source[SourceEntity, NotUsed] = Source(files) 11 | } 12 | -------------------------------------------------------------------------------- /client/src/test/scala/stasis/test/specs/unit/client/mocks/MockRecoveryCollector.scala: -------------------------------------------------------------------------------- 1 | package stasis.test.specs.unit.client.mocks 2 | 3 | import org.apache.pekko.NotUsed 4 | import org.apache.pekko.stream.scaladsl.Source 5 | 6 | import stasis.client.collection.RecoveryCollector 7 | import stasis.client.model.TargetEntity 8 | 9 | class MockRecoveryCollector(files: List[TargetEntity]) extends RecoveryCollector { 10 | override def collect(): Source[TargetEntity, NotUsed] = Source(files) 11 | } 12 | -------------------------------------------------------------------------------- /client/src/test/scala/stasis/test/specs/unit/client/mocks/MockServerMonitor.scala: -------------------------------------------------------------------------------- 1 | package stasis.test.specs.unit.client.mocks 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.Done 6 | 7 | import stasis.client.ops.monitoring.ServerMonitor 8 | 9 | class MockServerMonitor extends ServerMonitor { 10 | override def stop(): Future[Done] = Future.successful(Done) 11 | } 12 | 13 | object MockServerMonitor { 14 | def apply(): MockServerMonitor = new MockServerMonitor() 15 | } 16 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | # stasis / core 2 | 3 | Core routing, networking and persistence code. Represents the subsystem that handles data exchange. 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/discovery/ServiceApiClient.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.discovery 2 | 3 | trait ServiceApiClient 4 | 5 | object ServiceApiClient { 6 | trait Factory { 7 | def create(endpoint: ServiceApiEndpoint.Api, coreClient: ServiceApiClient): ServiceApiClient 8 | def create(endpoint: ServiceApiEndpoint.Core): ServiceApiClient 9 | def create(endpoint: ServiceApiEndpoint.Discovery): ServiceApiClient 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/discovery/ServiceDiscoveryRequest.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.discovery 2 | 3 | final case class ServiceDiscoveryRequest( 4 | isInitialRequest: Boolean, 5 | attributes: Map[String, String] 6 | ) { 7 | lazy val id: String = attributes.toList 8 | .sortBy(_._1) 9 | .map { case (k, v) => s"$k=$v" } 10 | .mkString("::") 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/discovery/exceptions/DiscoveryFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.discovery.exceptions 2 | 3 | class DiscoveryFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/networking/Endpoint.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.networking 2 | 3 | import stasis.core.security.NodeAuthenticator 4 | 5 | trait Endpoint[C] { 6 | protected def authenticator: NodeAuthenticator[C] 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/networking/EndpointAddress.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.networking 2 | 3 | trait EndpointAddress 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/networking/exceptions/ClientFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.networking.exceptions 2 | 3 | final case class ClientFailure(override val message: String) extends NetworkingFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/networking/exceptions/CredentialsFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.networking.exceptions 2 | 3 | final case class CredentialsFailure(override val message: String) extends NetworkingFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/networking/exceptions/EndpointFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.networking.exceptions 2 | 3 | final case class EndpointFailure(override val message: String) extends NetworkingFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/networking/exceptions/NetworkingFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.networking.exceptions 2 | 3 | class NetworkingFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/networking/exceptions/ReservationFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.networking.exceptions 2 | 3 | final case class ReservationFailure(override val message: String) extends NetworkingFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/networking/grpc/GrpcEndpointAddress.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.networking.grpc 2 | 3 | import stasis.core.networking.EndpointAddress 4 | 5 | final case class GrpcEndpointAddress(host: String, port: Int, tlsEnabled: Boolean) extends EndpointAddress 6 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/networking/http/HttpEndpointAddress.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.networking.http 2 | 3 | import org.apache.pekko.http.scaladsl.model.Uri 4 | 5 | import stasis.core.networking.EndpointAddress 6 | 7 | final case class HttpEndpointAddress(uri: Uri) extends EndpointAddress 8 | 9 | object HttpEndpointAddress { 10 | def apply(uri: String): HttpEndpointAddress = HttpEndpointAddress(Uri(uri)) 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/packaging/Crate.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.packaging 2 | 3 | object Crate { 4 | type Id = java.util.UUID 5 | 6 | def generateId(): Id = java.util.UUID.randomUUID() 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/backends/EventLogBackend.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.backends 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.stream.scaladsl.Source 6 | import org.apache.pekko.Done 7 | import org.apache.pekko.NotUsed 8 | 9 | trait EventLogBackend[E, S] { 10 | def getState: Future[S] 11 | def getStateStream: Source[S, NotUsed] 12 | def storeEventAndUpdateState(event: E, update: (E, S) => S): Future[Done] 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/backends/KeyValueBackend.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.backends 2 | 3 | import org.apache.pekko.util.ByteString 4 | 5 | object KeyValueBackend { 6 | trait Serdes[K, V] { 7 | implicit def serializeKey: K => String 8 | implicit def deserializeKey: String => K 9 | implicit def serializeValue: V => ByteString 10 | implicit def deserializeValue: ByteString => V 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/backends/file/container/CrateChunk.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.backends.file.container 2 | 3 | import org.apache.pekko.util.ByteString 4 | 5 | import stasis.core.persistence.backends.file.container.headers.ChunkHeader 6 | 7 | final case class CrateChunk(header: ChunkHeader, data: ByteString) 8 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/backends/file/container/CrateChunkDescriptor.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.backends.file.container 2 | 3 | import stasis.core.persistence.backends.file.container.headers.ChunkHeader 4 | 5 | final case class CrateChunkDescriptor(header: ChunkHeader, dataStartOffset: Long) 6 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/backends/file/container/exceptions/ContainerFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.backends.file.container.exceptions 2 | 3 | class ContainerFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/backends/file/container/exceptions/ContainerSinkFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.backends.file.container.exceptions 2 | 3 | final case class ContainerSinkFailure(override val message: String) extends ContainerFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/backends/file/container/exceptions/ContainerSourceFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.backends.file.container.exceptions 2 | 3 | final case class ContainerSourceFailure(override val message: String) extends ContainerFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/backends/file/container/exceptions/ConversionFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.backends.file.container.exceptions 2 | 3 | final case class ConversionFailure(override val message: String) extends ContainerFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/exceptions/PersistenceFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.exceptions 2 | 3 | class PersistenceFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/exceptions/ReservationFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.exceptions 2 | 3 | final case class ReservationFailure(override val message: String) extends PersistenceFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/exceptions/StagingFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.exceptions 2 | 3 | final case class StagingFailure(override val message: String) extends PersistenceFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/persistence/manifests/ManifestStore.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.persistence.manifests 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.Done 6 | import stasis.core.packaging.Crate 7 | import stasis.core.packaging.Manifest 8 | import stasis.layers.persistence.Store 9 | 10 | trait ManifestStore extends Store { store => 11 | def put(manifest: Manifest): Future[Done] 12 | def delete(crate: Crate.Id): Future[Boolean] 13 | def get(crate: Crate.Id): Future[Option[Manifest]] 14 | def list(): Future[Seq[Manifest]] 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/routing/exceptions/DiscardFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.routing.exceptions 2 | 3 | final case class DiscardFailure(override val message: String) extends RoutingFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/routing/exceptions/DistributionFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.routing.exceptions 2 | 3 | final case class DistributionFailure(override val message: String) extends RoutingFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/routing/exceptions/PullFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.routing.exceptions 2 | 3 | final case class PullFailure(override val message: String) extends RoutingFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/routing/exceptions/PushFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.routing.exceptions 2 | 3 | final case class PushFailure(override val message: String) extends RoutingFailure(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/routing/exceptions/RoutingFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.routing.exceptions 2 | 3 | class RoutingFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/security/NodeAuthenticator.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.security 2 | 3 | import scala.concurrent.Future 4 | 5 | import stasis.core.routing.Node 6 | 7 | trait NodeAuthenticator[C] { 8 | def authenticate(credentials: C): Future[Node.Id] 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/stasis/core/security/NodeCredentialsProvider.scala: -------------------------------------------------------------------------------- 1 | package stasis.core.security 2 | 3 | import scala.concurrent.Future 4 | 5 | import stasis.core.networking.EndpointAddress 6 | 7 | trait NodeCredentialsProvider[A <: EndpointAddress, C] { 8 | def provide(address: A): Future[C] 9 | } 10 | -------------------------------------------------------------------------------- /core/src/test/resources/certs/localhost.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/core/src/test/resources/certs/localhost.jks -------------------------------------------------------------------------------- /core/src/test/resources/certs/localhost.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sndnv/stasis/611356afaf86ad919aa4c9c45454f8f085e69739/core/src/test/resources/certs/localhost.p12 -------------------------------------------------------------------------------- /core/src/test/resources/discovery-static-invalid.conf: -------------------------------------------------------------------------------- 1 | endpoints { 2 | api = [ 3 | { 4 | uri = "http://localhost:10000" 5 | } 6 | ] 7 | 8 | core = [ 9 | { 10 | type = "http" 11 | 12 | http { 13 | uri = "http://localhost:20001" 14 | } 15 | }, 16 | { 17 | type = "other" 18 | } 19 | ] 20 | 21 | discovery = [] 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/scala/stasis/test/specs/unit/AsyncUnitSpec.scala: -------------------------------------------------------------------------------- 1 | package stasis.test.specs.unit 2 | 3 | trait AsyncUnitSpec extends stasis.layers.UnitSpec 4 | -------------------------------------------------------------------------------- /deployment/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .idea_modules 3 | .classpath 4 | .project 5 | .settings 6 | venv 7 | *.log 8 | __pycache__ 9 | .coverage 10 | cover 11 | htmlcov 12 | *.iml 13 | *.db 14 | *.egg-info 15 | -------------------------------------------------------------------------------- /deployment/dev/config/client.schedules: -------------------------------------------------------------------------------- 1 | # backups 2 | # operation # schedule-id # definition-id # target files (optional) 3 | backup 54ad087b-7fd7-4403-926e-dbd774651fc7 ad84e86b-6489-4622-8e2d-53ff6921b5a2 4 | backup f1993da5-67bd-40c8-8a97-3b27ee66e264 ad84e86b-6489-4622-8e2d-53ff6921b5a2 /work/file-01, /work/file-02 5 | -------------------------------------------------------------------------------- /deployment/dev/config/grafana/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'prom' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | options: 9 | path: /etc/grafana/provisioning/dashboards 10 | foldersFromFilesStructure: true 11 | -------------------------------------------------------------------------------- /deployment/dev/config/grafana/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | deleteDatasources: 4 | - name: prom 5 | orgId: 1 6 | 7 | datasources: 8 | - name: prom 9 | type: prometheus 10 | access: proxy 11 | orgId: 1 12 | url: http://prometheus:9090 13 | isDefault: true 14 | -------------------------------------------------------------------------------- /deployment/dev/config/prometheus-local/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 5s 3 | 4 | scrape_configs: 5 | - job_name: 'local' 6 | static_configs: 7 | - targets: ['host.docker.internal:9092'] 8 | -------------------------------------------------------------------------------- /deployment/dev/config/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | 4 | scrape_configs: 5 | - job_name: 'identity' 6 | static_configs: 7 | - targets: ['identity:10001'] 8 | - job_name: 'server' 9 | static_configs: 10 | - targets: ['server:20003'] 11 | -------------------------------------------------------------------------------- /deployment/dev/config/server-discovery.conf: -------------------------------------------------------------------------------- 1 | endpoints { 2 | # at least one API endpoint must be provided 3 | api = [ 4 | { 5 | uri = "http://server:20000" 6 | } 7 | ] 8 | 9 | # at least one core endpoint must be provided 10 | core = [ 11 | { 12 | type = "http" # one of [http, grpc] 13 | 14 | http { 15 | uri = "http://server:20001" 16 | } 17 | } 18 | ] 19 | 20 | # if no discovery endpoints are explicitly provided here, 21 | # the API endpoints will also be used for discovery 22 | discovery = [] 23 | } 24 | -------------------------------------------------------------------------------- /deployment/dev/docker-compose-metrics.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | prometheus: 5 | image: prom/prometheus 6 | ports: 7 | - "19090:9090" 8 | volumes: 9 | - ./config/prometheus-local:/etc/prometheus 10 | extra_hosts: 11 | - "host.docker.internal:host-gateway" 12 | 13 | grafana: 14 | image: grafana/grafana 15 | ports: 16 | - "13000:3000" 17 | volumes: 18 | - ./config/grafana:/etc/grafana/provisioning 19 | - ../grafana/dashboards/client:/etc/grafana/provisioning/dashboards/client 20 | -------------------------------------------------------------------------------- /deployment/dev/secrets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /deployment/production/local/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /deployment/production/secrets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !templates 4 | !*.env.template 5 | -------------------------------------------------------------------------------- /deployment/production/secrets/templates/db-identity-exporter.env.template: -------------------------------------------------------------------------------- 1 | DATA_SOURCE_USER=$${IDENTITY_DB_USER} 2 | DATA_SOURCE_PASS=$${IDENTITY_DB_PASSWORD} 3 | -------------------------------------------------------------------------------- /deployment/production/secrets/templates/db-identity.env.template: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=$${IDENTITY_DB_USER} 2 | POSTGRES_PASSWORD=$${IDENTITY_DB_PASSWORD} 3 | -------------------------------------------------------------------------------- /deployment/production/secrets/templates/db-server-exporter.env.template: -------------------------------------------------------------------------------- 1 | DATA_SOURCE_USER=$${SERVER_DB_USER} 2 | DATA_SOURCE_PASS=$${SERVER_DB_PASSWORD} 3 | -------------------------------------------------------------------------------- /deployment/production/secrets/templates/db-server.env.template: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=$${SERVER_DB_USER} 2 | POSTGRES_PASSWORD=$${SERVER_DB_PASSWORD} 3 | -------------------------------------------------------------------------------- /deployment/production/secrets/templates/identity-ui.env.template: -------------------------------------------------------------------------------- 1 | IDENTITY_UI_CLIENT_ID=$${IDENTITY_UI_CLIENT_ID} 2 | IDENTITY_UI_DERIVATION_SALT_PREFIX=$${DERIVATION_SALT_PREFIX} 3 | -------------------------------------------------------------------------------- /deployment/production/secrets/templates/identity.env.template: -------------------------------------------------------------------------------- 1 | STASIS_IDENTITY_PERSISTENCE_DATABASE_USER=$${IDENTITY_DB_USER} 2 | STASIS_IDENTITY_PERSISTENCE_DATABASE_PASSWORD=$${IDENTITY_DB_PASSWORD} 3 | -------------------------------------------------------------------------------- /deployment/production/secrets/templates/server-ui.env.template: -------------------------------------------------------------------------------- 1 | SERVER_UI_CLIENT_ID=$${SERVER_UI_CLIENT_ID} 2 | SERVER_UI_DERIVATION_SALT_PREFIX=$${DERIVATION_SALT_PREFIX} 3 | -------------------------------------------------------------------------------- /deployment/production/telemetry/grafana/dashboards/dashboard.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'prom' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | options: 9 | path: /etc/grafana/provisioning/dashboards 10 | foldersFromFilesStructure: true 11 | -------------------------------------------------------------------------------- /deployment/production/telemetry/grafana/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | deleteDatasources: 4 | - name: prom 5 | orgId: 1 6 | 7 | datasources: 8 | - name: prom 9 | type: prometheus 10 | access: proxy 11 | orgId: 1 12 | url: http://prometheus:9090 13 | isDefault: true 14 | -------------------------------------------------------------------------------- /deployment/production/telemetry/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | 4 | scrape_configs: 5 | - job_name: 'db-identity' 6 | static_configs: 7 | - targets: ['db-identity-exporter:42002'] 8 | - job_name: 'db-server' 9 | static_configs: 10 | - targets: ['db-server-exporter:42003'] 11 | - job_name: 'identity' 12 | static_configs: 13 | - targets: ['identity:42101'] 14 | - job_name: 'server' 15 | static_configs: 16 | - targets: ['server:42303'] 17 | -------------------------------------------------------------------------------- /identity-ui/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | prefer_single_quotes: true 6 | always_use_package_imports: true 7 | avoid_type_to_string: true 8 | avoid_web_libraries_in_flutter: false 9 | 10 | analyzer: 11 | exclude: 12 | - "**/*.g.dart" 13 | - "**/*.freezed.dart" 14 | errors: 15 | invalid_annotation_target: ignore 16 | -------------------------------------------------------------------------------- /identity-ui/deployment/production/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:stable-alpine 2 | 3 | LABEL org.opencontainers.image.description="Web-based user interface for the 'stasis/identity' service" 4 | 5 | RUN mkdir -p /opt/stasis-identity-ui/templates 6 | 7 | COPY build/web /usr/share/nginx/html 8 | COPY deployment/production/.env.template /opt/stasis-identity-ui/templates 9 | COPY deployment/production/nginx.template /opt/stasis-identity-ui/templates 10 | COPY deployment/production/entrypoint.sh /opt/stasis-identity-ui 11 | 12 | RUN chmod +x /opt/stasis-identity-ui/entrypoint.sh 13 | 14 | ENTRYPOINT ["/opt/stasis-identity-ui/entrypoint.sh"] 15 | -------------------------------------------------------------------------------- /identity-ui/lib/model/api.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'api.freezed.dart'; 4 | part 'api.g.dart'; 5 | 6 | @freezed 7 | class Api with _$Api { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory Api({ 10 | required String id, 11 | required DateTime created, 12 | required DateTime updated, 13 | }) = _Api; 14 | 15 | factory Api.fromJson(Map json) => _$ApiFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /identity-ui/lib/model/requests/create_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'create_api.freezed.dart'; 4 | part 'create_api.g.dart'; 5 | 6 | @freezed 7 | class CreateApi with _$CreateApi { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory CreateApi({ 10 | required String id, 11 | }) = _CreateApi; 12 | 13 | factory CreateApi.fromJson(Map json) => _$CreateApiFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /identity-ui/lib/model/requests/create_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'create_client.freezed.dart'; 4 | part 'create_client.g.dart'; 5 | 6 | @freezed 7 | class CreateClient with _$CreateClient { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory CreateClient({ 10 | required String redirectUri, 11 | required int tokenExpiration, 12 | required String rawSecret, 13 | String? subject, 14 | }) = _CreateClient; 15 | 16 | factory CreateClient.fromJson(Map json) => _$CreateClientFromJson(json); 17 | } 18 | -------------------------------------------------------------------------------- /identity-ui/lib/model/requests/create_owner.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'create_owner.freezed.dart'; 4 | part 'create_owner.g.dart'; 5 | 6 | @freezed 7 | class CreateOwner with _$CreateOwner { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory CreateOwner({ 10 | required String username, 11 | required String rawPassword, 12 | required List allowedScopes, 13 | String? subject, 14 | }) = _CreateOwner; 15 | 16 | factory CreateOwner.fromJson(Map json) => _$CreateOwnerFromJson(json); 17 | } 18 | -------------------------------------------------------------------------------- /identity-ui/lib/model/requests/update_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_client.freezed.dart'; 4 | part 'update_client.g.dart'; 5 | 6 | @freezed 7 | class UpdateClient with _$UpdateClient { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateClient({ 10 | required int tokenExpiration, 11 | required bool active, 12 | }) = _UpdateClient; 13 | 14 | factory UpdateClient.fromJson(Map json) => _$UpdateClientFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /identity-ui/lib/model/requests/update_client_credentials.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_client_credentials.freezed.dart'; 4 | part 'update_client_credentials.g.dart'; 5 | 6 | @freezed 7 | class UpdateClientCredentials with _$UpdateClientCredentials { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateClientCredentials({ 10 | required String rawSecret, 11 | }) = _UpdateClientCredentials; 12 | 13 | factory UpdateClientCredentials.fromJson(Map json) => _$UpdateClientCredentialsFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /identity-ui/lib/model/requests/update_owner.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_owner.freezed.dart'; 4 | part 'update_owner.g.dart'; 5 | 6 | @freezed 7 | class UpdateOwner with _$UpdateOwner { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateOwner({ 10 | required List allowedScopes, 11 | required bool active, 12 | }) = _UpdateOwner; 13 | 14 | factory UpdateOwner.fromJson(Map json) => _$UpdateOwnerFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /identity-ui/lib/model/requests/update_owner_credentials.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_owner_credentials.freezed.dart'; 4 | part 'update_owner_credentials.g.dart'; 5 | 6 | @freezed 7 | class UpdateOwnerCredentials with _$UpdateOwnerCredentials { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateOwnerCredentials({ 10 | required String rawPassword, 11 | }) = _UpdateOwnerCredentials; 12 | 13 | factory UpdateOwnerCredentials.fromJson(Map json) => _$UpdateOwnerCredentialsFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /identity-ui/lib/pages/manage/components/rendering.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | extension ExtendedDateTime on DateTime { 4 | String render() { 5 | final DateFormat formatter = DateFormat('yyyy-MM-dd HH:mm'); 6 | return formatter.format(toLocal()); 7 | } 8 | 9 | String renderAsDate() { 10 | final DateFormat formatter = DateFormat('yyyy-MM-dd'); 11 | return formatter.format(toLocal()); 12 | } 13 | 14 | String renderAsTime() { 15 | final DateFormat formatter = DateFormat('HH:mm'); 16 | return formatter.format(toLocal()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /identity-ui/test/pages/manage/components/rendering_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:identity_ui/pages/manage/components/rendering.dart'; 3 | 4 | void main() { 5 | group('Rendering components should', () { 6 | test('convert DateTimes to strings', () { 7 | final timestamp = DateTime(2020, 12, 31, 23, 45, 30); 8 | 9 | expect(timestamp.render(), '2020-12-31 23:45'); 10 | expect(timestamp.renderAsDate(), '2020-12-31'); 11 | expect(timestamp.renderAsTime(), '23:45'); 12 | }); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /identity-ui/web/authorize/authorize.js: -------------------------------------------------------------------------------- 1 | function sendAuthorization(uri, authorization, callback) { 2 | let request = new XMLHttpRequest(); 3 | request.open('GET', uri, true) 4 | request.setRequestHeader('Authorization', authorization); 5 | request.onload = () => { 6 | let queryParams = new URL(request.responseURL).searchParams; 7 | if(queryParams.get('error') === 'access_denied') { 8 | callback('access_denied'); 9 | } else { 10 | window.location.href = request.responseURL; 11 | callback(null); 12 | } 13 | }; 14 | request.send(null); 15 | } 16 | -------------------------------------------------------------------------------- /identity-ui/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "identity", 3 | "short_name": "identity", 4 | "start_url": "manage", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "Web-based user interface for the `identity` service.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "assets/logo.svg", 14 | "sizes": "192x192", 15 | "type": "image/svg+xml" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /identity/README.md: -------------------------------------------------------------------------------- 1 | # stasis / identity 2 | 3 | OAuth2 identity management service based on [RFC 6749](https://tools.ietf.org/html/rfc6749). 4 | 5 | * The [Management API](./src/main/scala/stasis/identity/api/Manage.scala) is available at `/manage` 6 | * The [OAuth2 API](./src/main/scala/stasis/identity/api/OAuth.scala) is available at `/oauth` 7 | * [JWKs](./src/main/scala/stasis/identity/api/Jwks.scala) can be accessed via `/jwks` 8 | 9 | > An example bootstrap config, for first-run service setup, can be found in the 10 | > [resources](./src/main/resources/example-bootstrap.conf). 11 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/Main.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity 2 | 3 | import stasis.identity.service.Service 4 | 5 | object Main extends App with Service 6 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/api/manage/requests/CreateApi.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.api.manage.requests 2 | 3 | import stasis.identity.model.apis.Api 4 | 5 | final case class CreateApi(id: Api.Id) { 6 | require(id.nonEmpty, "id must not be empty") 7 | 8 | def toApi: Api = 9 | Api.create(id = id) 10 | } 11 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/api/manage/requests/UpdateClient.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.api.manage.requests 2 | 3 | import stasis.identity.model.Seconds 4 | 5 | final case class UpdateClient( 6 | tokenExpiration: Seconds, 7 | active: Boolean 8 | ) { 9 | require(tokenExpiration.value > 0, "token expiration must be a positive number") 10 | } 11 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/api/manage/requests/UpdateClientCredentials.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.api.manage.requests 2 | 3 | import stasis.identity.model.secrets.Secret 4 | 5 | final case class UpdateClientCredentials( 6 | rawSecret: String 7 | ) { 8 | require(rawSecret.nonEmpty, "secret must not be empty") 9 | 10 | def toSecret()(implicit config: Secret.ClientConfig): (Secret, String) = { 11 | val salt = Secret.generateSalt() 12 | val secret = Secret.derive(rawSecret = rawSecret, salt = salt) 13 | 14 | (secret, salt) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/api/manage/requests/UpdateOwner.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.api.manage.requests 2 | 3 | final case class UpdateOwner( 4 | allowedScopes: Seq[String], 5 | active: Boolean 6 | ) { 7 | require(allowedScopes.nonEmpty, "allowed scopes must not be empty") 8 | } 9 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/api/manage/requests/UpdateOwnerCredentials.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.api.manage.requests 2 | 3 | import stasis.identity.model.secrets.Secret 4 | 5 | final case class UpdateOwnerCredentials( 6 | rawPassword: String 7 | ) { 8 | require(rawPassword.nonEmpty, "password must not be empty") 9 | 10 | def toSecret()(implicit config: Secret.ResourceOwnerConfig): (Secret, String) = { 11 | val salt = Secret.generateSalt() 12 | val secret = Secret.derive(rawSecret = rawPassword, salt = salt) 13 | 14 | (secret, salt) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/api/manage/responses/CreatedClient.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.api.manage.responses 2 | 3 | import stasis.identity.model.clients.Client 4 | 5 | final case class CreatedClient(client: Client.Id) 6 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/api/manage/setup/Config.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.api.manage.setup 2 | 3 | import stasis.identity.model.secrets.Secret 4 | 5 | final case class Config( 6 | realm: String, 7 | clientSecrets: Secret.ClientConfig, 8 | ownerSecrets: Secret.ResourceOwnerConfig 9 | ) 10 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/api/oauth/setup/Config.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.api.oauth.setup 2 | 3 | final case class Config( 4 | realm: String, 5 | refreshTokensAllowed: Boolean 6 | ) 7 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/authentication/manage/ResourceOwnerAuthenticator.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.authentication.manage 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.http.scaladsl.model.headers.OAuth2BearerToken 6 | 7 | import stasis.identity.model.owners.ResourceOwner 8 | 9 | trait ResourceOwnerAuthenticator { 10 | def authenticate(credentials: OAuth2BearerToken): Future[ResourceOwner] 11 | } 12 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/authentication/oauth/ClientAuthenticator.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.authentication.oauth 2 | 3 | import stasis.identity.model.clients.Client 4 | 5 | trait ClientAuthenticator extends EntityAuthenticator[Client] 6 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/authentication/oauth/ResourceOwnerAuthenticator.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.authentication.oauth 2 | 3 | import stasis.identity.model.owners.ResourceOwner 4 | 5 | trait ResourceOwnerAuthenticator extends EntityAuthenticator[ResourceOwner] 6 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/ChallengeMethod.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model 2 | 3 | sealed trait ChallengeMethod 4 | 5 | object ChallengeMethod { 6 | case object Plain extends ChallengeMethod 7 | case object S256 extends ChallengeMethod 8 | } 9 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/GrantType.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model 2 | 3 | sealed trait GrantType 4 | 5 | object GrantType { 6 | case object AuthorizationCode extends GrantType 7 | case object ClientCredentials extends GrantType 8 | case object Implicit extends GrantType 9 | case object RefreshToken extends GrantType 10 | case object Password extends GrantType 11 | } 12 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/ResponseType.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model 2 | 3 | sealed trait ResponseType 4 | 5 | object ResponseType { 6 | case object Code extends ResponseType 7 | case object Token extends ResponseType 8 | } 9 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/Seconds.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model 2 | 3 | import scala.concurrent.duration.FiniteDuration 4 | 5 | final case class Seconds(value: Long) extends AnyVal 6 | 7 | object Seconds { 8 | import scala.language.implicitConversions 9 | 10 | def apply(duration: FiniteDuration): Seconds = Seconds(duration.toSeconds) 11 | 12 | implicit def durationToSeconds(duration: FiniteDuration): Seconds = Seconds(duration) 13 | } 14 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/codes/AuthorizationCode.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.codes 2 | 3 | final case class AuthorizationCode(value: String) extends AnyVal 4 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/codes/generators/AuthorizationCodeGenerator.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.codes.generators 2 | 3 | import stasis.identity.model.codes.AuthorizationCode 4 | 5 | trait AuthorizationCodeGenerator { 6 | def generate(): AuthorizationCode 7 | } 8 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/tokens/AccessToken.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.tokens 2 | 3 | final case class AccessToken(value: String) extends AnyVal 4 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/tokens/AccessTokenWithExpiration.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.tokens 2 | 3 | import stasis.identity.model.Seconds 4 | 5 | final case class AccessTokenWithExpiration(token: AccessToken, expiration: Seconds) 6 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/tokens/RefreshToken.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.tokens 2 | 3 | final case class RefreshToken(value: String) extends AnyVal 4 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/tokens/StoredRefreshToken.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.tokens 2 | 3 | import java.time.Instant 4 | 5 | import stasis.identity.model.clients.Client 6 | import stasis.identity.model.owners.ResourceOwner 7 | 8 | final case class StoredRefreshToken( 9 | token: RefreshToken, 10 | client: Client.Id, 11 | owner: ResourceOwner.Id, 12 | scope: Option[String], 13 | expiration: Instant, 14 | created: Instant 15 | ) 16 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/tokens/TokenType.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.tokens 2 | 3 | sealed trait TokenType 4 | 5 | object TokenType { 6 | case object Bearer extends TokenType 7 | } 8 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/tokens/generators/AccessTokenGenerator.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.tokens.generators 2 | 3 | import stasis.identity.model.apis.Api 4 | import stasis.identity.model.clients.Client 5 | import stasis.identity.model.owners.ResourceOwner 6 | import stasis.identity.model.tokens.AccessTokenWithExpiration 7 | 8 | trait AccessTokenGenerator { 9 | def generate(client: Client, audience: Seq[Client]): AccessTokenWithExpiration 10 | def generate(owner: ResourceOwner, audience: Seq[Api]): AccessTokenWithExpiration 11 | } 12 | -------------------------------------------------------------------------------- /identity/src/main/scala/stasis/identity/model/tokens/generators/RefreshTokenGenerator.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.tokens.generators 2 | 3 | import stasis.identity.model.tokens.RefreshToken 4 | 5 | trait RefreshTokenGenerator { 6 | def generate(): RefreshToken 7 | } 8 | -------------------------------------------------------------------------------- /identity/src/test/resources/bootstrap-integration.conf: -------------------------------------------------------------------------------- 1 | bootstrap { 2 | apis = [ 3 | { 4 | id = "existing-api" 5 | } 6 | ] 7 | 8 | clients = [ 9 | { 10 | redirect-uri = "http://localhost:8080/existing/uri" 11 | token-expiration = 90 minutes 12 | raw-secret = "existing-client-secret" 13 | active = true 14 | } 15 | ] 16 | 17 | owners = [ 18 | { 19 | username = "existing-user" 20 | raw-password = "existing-user-password" 21 | allowed-scopes = ["manage:apis", "manage:owners"] 22 | active = true 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /identity/src/test/resources/keys/ec.jwk.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "kid": "stasis-identity-ec-0", 4 | "alg": "ES256", 5 | "x": "cL5LvINRcxl56jDUka2d8o9oUVi9OKFuqh8D5DLksdM", 6 | "y": "_VfZLJCsFRkk3u07KKvwBon0W_vksAvne2AEELW7lvk", 7 | "crv": "P-256", 8 | "d": "RDgJk4Z2pR1qIceV1NnFgmTWCcM4DWbsjFwxM2INpM0" 9 | } 10 | -------------------------------------------------------------------------------- /identity/src/test/resources/keys/oct.jwk.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "oct", 3 | "kid": "stasis-identity-oct-0", 4 | "alg": "HS256", 5 | "k": "W18CYwzxAGWtleQr5gpavBHgyT4JZCArIdHUWMCz_CA" 6 | } -------------------------------------------------------------------------------- /identity/src/test/scala/stasis/identity/EncodingHelpers.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity 2 | 3 | import java.util.Base64 4 | 5 | import org.apache.pekko.util.ByteString 6 | 7 | trait EncodingHelpers { 8 | implicit class ByteStringToBase64(raw: ByteString) { 9 | def encodeAsBase64: String = Base64.getMimeEncoder().encodeToString(raw.toArray) 10 | } 11 | 12 | implicit class Base64StringToByteString(raw: String) { 13 | def decodeFromBase64: ByteString = ByteString.fromArray(Base64.getMimeDecoder.decode(raw)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /identity/src/test/scala/stasis/identity/api/manage/requests/UpdateClientSpec.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.api.manage.requests 2 | 3 | import scala.concurrent.duration._ 4 | 5 | import stasis.layers.UnitSpec 6 | 7 | class UpdateClientSpec extends UnitSpec { 8 | "An UpdateClient request" should "validate its content" in withRetry { 9 | val request = UpdateClient(tokenExpiration = 1.second, active = true) 10 | 11 | an[IllegalArgumentException] should be thrownBy request.copy(tokenExpiration = 0.seconds) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /identity/src/test/scala/stasis/identity/api/manage/requests/UpdateOwnerSpec.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.api.manage.requests 2 | 3 | import stasis.layers.UnitSpec 4 | 5 | class UpdateOwnerSpec extends UnitSpec { 6 | private val request = UpdateOwner( 7 | allowedScopes = Seq("some-scope"), 8 | active = true 9 | ) 10 | 11 | "An UpdateOwner request" should "validate its content" in withRetry { 12 | an[IllegalArgumentException] should be thrownBy request.copy(allowedScopes = Seq.empty) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /identity/src/test/scala/stasis/identity/model/apis/ApiSpec.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.apis 2 | 3 | import stasis.identity.model.Generators 4 | import stasis.layers.UnitSpec 5 | 6 | class ApiSpec extends UnitSpec { 7 | "An Api" should "validate its fields" in { 8 | val api = Generators.generateApi 9 | an[IllegalArgumentException] should be thrownBy api.copy(id = "") 10 | an[IllegalArgumentException] should be thrownBy api.copy(id = "abc~def") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /identity/src/test/scala/stasis/identity/model/codes/generators/DefaultAuthorizationCodeGeneratorSpec.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.codes.generators 2 | 3 | import stasis.layers.UnitSpec 4 | 5 | class DefaultAuthorizationCodeGeneratorSpec extends UnitSpec { 6 | "A DefaultAuthorizationCodeGenerator" should "generate random authorization codes" in { 7 | val codeSize = 32 8 | val generator = new DefaultAuthorizationCodeGenerator(codeSize) 9 | val code = generator.generate() 10 | 11 | code.value.length should be(codeSize) 12 | code.value.matches("^[A-Za-z0-9]+$") should be(true) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /identity/src/test/scala/stasis/identity/model/tokens/generators/RandomRefreshTokenGeneratorSpec.scala: -------------------------------------------------------------------------------- 1 | package stasis.identity.model.tokens.generators 2 | 3 | import stasis.layers.UnitSpec 4 | 5 | class RandomRefreshTokenGeneratorSpec extends UnitSpec { 6 | "A RandomRefreshTokenGenerator" should "generate random refresh tokens" in { 7 | val tokenSize = 32 8 | val generator = new RandomRefreshTokenGenerator(tokenSize) 9 | val token = generator.generate() 10 | 11 | token.value.length should be(tokenSize) 12 | token.value.matches("^[A-Za-z0-9]+$") should be(true) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /layers/README.md: -------------------------------------------------------------------------------- 1 | # stasis / layers 2 | 3 | Generic code commonly used by the various layers of the `stasis` services - API, persistence, security, telemetry. 4 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/api/MessageResponse.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.api 2 | 3 | import play.api.libs.json.Format 4 | import play.api.libs.json.Json 5 | 6 | final case class MessageResponse(message: String) 7 | 8 | object MessageResponse { 9 | implicit val messageResponseFormat: Format[MessageResponse] = Json.format[MessageResponse] 10 | } 11 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/persistence/KeyValueStore.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.persistence 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.Done 6 | 7 | trait KeyValueStore[K, V] extends Store { 8 | def put(key: K, value: V): Future[Done] 9 | def get(key: K): Future[Option[V]] 10 | def delete(key: K): Future[Boolean] 11 | def contains(key: K): Future[Boolean] 12 | def entries: Future[Map[K, V]] 13 | def load(entries: Map[K, V]): Future[Done] 14 | } 15 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/persistence/Store.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.persistence 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.Done 6 | 7 | import stasis.layers.persistence.migration.Migration 8 | 9 | trait Store { 10 | def name(): String 11 | def migrations(): Seq[Migration] 12 | def init(): Future[Done] 13 | def drop(): Future[Done] 14 | } 15 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/persistence/migration/MigrationResult.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.persistence.migration 2 | 3 | final case class MigrationResult(found: Int, executed: Int) { 4 | def +(other: MigrationResult): MigrationResult = 5 | MigrationResult(found = found + other.found, executed = executed + other.executed) 6 | } 7 | 8 | object MigrationResult { 9 | def empty: MigrationResult = MigrationResult(found = 0, executed = 0) 10 | } 11 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/security/exceptions/AuthenticationFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.security.exceptions 2 | 3 | final case class AuthenticationFailure(override val message: String) extends SecurityFailure(message) 4 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/security/exceptions/ProviderFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.security.exceptions 2 | 3 | final case class ProviderFailure(override val message: String) extends SecurityFailure(message) 4 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/security/exceptions/SecurityFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.security.exceptions 2 | 3 | class SecurityFailure(val message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/security/jwt/JwtAuthenticator.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.security.jwt 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.jose4j.jwt.JwtClaims 6 | 7 | trait JwtAuthenticator { 8 | def identityClaim: String 9 | def authenticate(credentials: String): Future[JwtClaims] 10 | } 11 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/security/jwt/JwtProvider.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.security.jwt 2 | 3 | import scala.concurrent.Future 4 | 5 | trait JwtProvider { 6 | def provide(scope: String): Future[String] 7 | } 8 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/security/keys/KeyProvider.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.security.keys 2 | 3 | import java.security.Key 4 | 5 | import scala.concurrent.Future 6 | 7 | trait KeyProvider { 8 | def key(id: Option[String]): Future[Key] 9 | def issuer: String 10 | def allowedAlgorithms: Seq[String] 11 | } 12 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/service/bootstrap/BootstrapResult.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.service.bootstrap 2 | 3 | final case class BootstrapResult(found: Int, created: Int) { 4 | def +(other: BootstrapResult): BootstrapResult = 5 | BootstrapResult(found = found + other.found, created = created + other.created) 6 | } 7 | 8 | object BootstrapResult { 9 | def empty: BootstrapResult = BootstrapResult(found = 0, created = 0) 10 | } 11 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/telemetry/TelemetryContext.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.telemetry 2 | 3 | import scala.reflect.ClassTag 4 | 5 | import stasis.layers.telemetry.metrics.MetricsProvider 6 | 7 | trait TelemetryContext { 8 | def metrics[M <: MetricsProvider](implicit tag: ClassTag[M]): M 9 | } 10 | -------------------------------------------------------------------------------- /layers/src/main/scala/stasis/layers/telemetry/metrics/MetricsProvider.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.telemetry.metrics 2 | 3 | trait MetricsProvider 4 | -------------------------------------------------------------------------------- /layers/src/test/resources/bootstrap.conf: -------------------------------------------------------------------------------- 1 | bootstrap { 2 | test-classes = [ 3 | {a: x, b: 1}, 4 | {a: y, b: 2}, 5 | {a: z, b: 3}, 6 | ] 7 | 8 | test-classes-with-invalid-values = [ 9 | {a: x, b: false}, 10 | ] 11 | 12 | test-classes-with-duplicates = [ 13 | {a: x, b: 1}, 14 | {a: x, b: 2}, 15 | {a: z, b: 3}, 16 | {a: z, b: 3}, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /layers/src/test/scala/stasis/layers/service/bootstrap/mocks/TestClass.scala: -------------------------------------------------------------------------------- 1 | package stasis.layers.service.bootstrap.mocks 2 | 3 | final case class TestClass(a: String, b: Int) 4 | 5 | object TestClass { 6 | val Default: TestClass = TestClass(a = "a", b = 0) 7 | } 8 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.7 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0") 2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") 3 | addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.3.0") 4 | addSbtPlugin("org.apache.pekko" % "pekko-grpc-sbt-plugin" % "1.1.1") 5 | addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.1") 6 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.3") 7 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") 8 | -------------------------------------------------------------------------------- /proto/README.md: -------------------------------------------------------------------------------- 1 | # stasis / proto 2 | 3 | Protocol Buffers file(s) defining gRPC services and messages used by the [`core`](../core) networking and routing. 4 | -------------------------------------------------------------------------------- /proto/src/main/protobuf/commands.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "common.proto"; 4 | 5 | option java_multiple_files = true; 6 | 7 | package stasis.core.commands.proto; 8 | 9 | message Command { 10 | int64 sequenceId = 1; 11 | string source = 2; 12 | stasis.common.proto.Uuid target = 3; 13 | CommandParameters parameters = 4; 14 | int64 created = 5; 15 | } 16 | 17 | message CommandParameters { 18 | oneof sealed_value { 19 | LogoutUser logoutUser = 1; 20 | } 21 | } 22 | 23 | message LogoutUser { 24 | optional string reason = 1; 25 | } 26 | -------------------------------------------------------------------------------- /proto/src/main/protobuf/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_multiple_files = true; 4 | 5 | package stasis.common.proto; 6 | 7 | message Uuid { 8 | int64 mostSignificantBits = 1; 9 | int64 leastSignificantBits = 2; 10 | } 11 | -------------------------------------------------------------------------------- /proto/src/main/protobuf/common_aux_options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "scalapb/scalapb.proto"; 4 | 5 | package stasis.common.proto; 6 | 7 | option (scalapb.options) = { 8 | scope: PACKAGE 9 | preserve_unknown_fields: false 10 | no_default_values_in_constructor: true 11 | }; 12 | -------------------------------------------------------------------------------- /server-ui/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | prefer_single_quotes: true 6 | always_use_package_imports: true 7 | avoid_type_to_string: true 8 | avoid_web_libraries_in_flutter: false 9 | 10 | analyzer: 11 | exclude: 12 | - "**/*.g.dart" 13 | - "**/*.freezed.dart" 14 | errors: 15 | invalid_annotation_target: ignore 16 | -------------------------------------------------------------------------------- /server-ui/deployment/production/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:stable-alpine 2 | 3 | LABEL org.opencontainers.image.description="Web-based user interface for the 'stasis/server' service" 4 | 5 | RUN mkdir -p /opt/stasis-server-ui/templates 6 | 7 | COPY build/web /usr/share/nginx/html 8 | COPY deployment/production/.env.template /opt/stasis-server-ui/templates 9 | COPY deployment/production/nginx.template /opt/stasis-server-ui/templates 10 | COPY deployment/production/entrypoint.sh /opt/stasis-server-ui 11 | 12 | RUN chmod +x /opt/stasis-server-ui/entrypoint.sh 13 | 14 | ENTRYPOINT ["/opt/stasis-server-ui/entrypoint.sh"] 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/requests/create_device_own.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:server_ui/model/devices/device.dart'; 3 | 4 | part 'create_device_own.freezed.dart'; 5 | part 'create_device_own.g.dart'; 6 | 7 | @freezed 8 | class CreateDeviceOwn with _$CreateDeviceOwn { 9 | @JsonSerializable(fieldRename: FieldRename.snake) 10 | const factory CreateDeviceOwn({ 11 | required String name, 12 | DeviceLimits? limits, 13 | }) = _CreateDeviceOwn; 14 | 15 | factory CreateDeviceOwn.fromJson(Map json) => _$CreateDeviceOwnFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/requests/update_device_limits.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:server_ui/model/devices/device.dart'; 3 | 4 | part 'update_device_limits.freezed.dart'; 5 | part 'update_device_limits.g.dart'; 6 | 7 | @freezed 8 | class UpdateDeviceLimits with _$UpdateDeviceLimits { 9 | @JsonSerializable(fieldRename: FieldRename.snake) 10 | const factory UpdateDeviceLimits({ 11 | DeviceLimits? limits, 12 | }) = _UpdateDeviceLimits; 13 | 14 | factory UpdateDeviceLimits.fromJson(Map json) => _$UpdateDeviceLimitsFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/requests/update_device_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_device_state.freezed.dart'; 4 | part 'update_device_state.g.dart'; 5 | 6 | @freezed 7 | class UpdateDeviceState with _$UpdateDeviceState { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateDeviceState({ 10 | required bool active, 11 | }) = _UpdateDeviceState; 12 | 13 | factory UpdateDeviceState.fromJson(Map json) => _$UpdateDeviceStateFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/requests/update_user_limits.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:server_ui/model/users/user.dart'; 3 | 4 | part 'update_user_limits.freezed.dart'; 5 | part 'update_user_limits.g.dart'; 6 | 7 | @freezed 8 | class UpdateUserLimits with _$UpdateUserLimits { 9 | @JsonSerializable(fieldRename: FieldRename.snake) 10 | const factory UpdateUserLimits({ 11 | UserLimits? limits, 12 | }) = _UpdateUserLimits; 13 | 14 | factory UpdateUserLimits.fromJson(Map json) => _$UpdateUserLimitsFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/requests/update_user_password.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_user_password.freezed.dart'; 4 | part 'update_user_password.g.dart'; 5 | 6 | @freezed 7 | class UpdateUserPassword with _$UpdateUserPassword { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateUserPassword({ 10 | required String rawPassword, 11 | }) = _UpdateUserPassword; 12 | 13 | factory UpdateUserPassword.fromJson(Map json) => _$UpdateUserPasswordFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/requests/update_user_permissions.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_user_permissions.freezed.dart'; 4 | part 'update_user_permissions.g.dart'; 5 | 6 | @freezed 7 | class UpdateUserPermissions with _$UpdateUserPermissions { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateUserPermissions({ 10 | required Set permissions, 11 | }) = _UpdateUserPermissions; 12 | 13 | factory UpdateUserPermissions.fromJson(Map json) => _$UpdateUserPermissionsFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/requests/update_user_salt.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_user_salt.freezed.dart'; 4 | part 'update_user_salt.g.dart'; 5 | 6 | @freezed 7 | class UpdateUserSalt with _$UpdateUserSalt { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateUserSalt({ 10 | required String salt, 11 | }) = _UpdateUserSalt; 12 | 13 | factory UpdateUserSalt.fromJson(Map json) => _$UpdateUserSaltFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/requests/update_user_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'update_user_state.freezed.dart'; 4 | part 'update_user_state.g.dart'; 5 | 6 | @freezed 7 | class UpdateUserState with _$UpdateUserState { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdateUserState({ 10 | required bool active, 11 | }) = _UpdateUserState; 12 | 13 | factory UpdateUserState.fromJson(Map json) => _$UpdateUserStateFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/responses/created_dataset_definition.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'created_dataset_definition.freezed.dart'; 4 | part 'created_dataset_definition.g.dart'; 5 | 6 | @freezed 7 | class CreatedDatasetDefinition with _$CreatedDatasetDefinition { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory CreatedDatasetDefinition({ 10 | required String definition, 11 | }) = _CreatedDatasetDefinition; 12 | 13 | factory CreatedDatasetDefinition.fromJson(Map json) => _$CreatedDatasetDefinitionFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/responses/created_device.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'created_device.freezed.dart'; 4 | part 'created_device.g.dart'; 5 | 6 | @freezed 7 | class CreatedDevice with _$CreatedDevice { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory CreatedDevice({ 10 | required String device, 11 | required String node, 12 | }) = _CreatedDevice; 13 | 14 | factory CreatedDevice.fromJson(Map json) => _$CreatedDeviceFromJson(json); 15 | } 16 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/responses/created_node.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'created_node.freezed.dart'; 4 | part 'created_node.g.dart'; 5 | 6 | @freezed 7 | class CreatedNode with _$CreatedNode { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory CreatedNode({ 10 | required String node, 11 | }) = _CreatedNode; 12 | 13 | factory CreatedNode.fromJson(Map json) => _$CreatedNodeFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/responses/created_schedule.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'created_schedule.freezed.dart'; 4 | part 'created_schedule.g.dart'; 5 | 6 | @freezed 7 | class CreatedSchedule with _$CreatedSchedule { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory CreatedSchedule({ 10 | required String schedule, 11 | }) = _CreatedSchedule; 12 | 13 | factory CreatedSchedule.fromJson(Map json) => _$CreatedScheduleFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/responses/created_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'created_user.freezed.dart'; 4 | part 'created_user.g.dart'; 5 | 6 | @freezed 7 | class CreatedUser with _$CreatedUser { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory CreatedUser({ 10 | required String user, 11 | }) = _CreatedUser; 12 | 13 | factory CreatedUser.fromJson(Map json) => _$CreatedUserFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/responses/ping.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'ping.freezed.dart'; 4 | part 'ping.g.dart'; 5 | 6 | @freezed 7 | class Ping with _$Ping { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory Ping({ 10 | required String id, 11 | }) = _Ping; 12 | 13 | factory Ping.fromJson(Map json) => _$PingFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/api/responses/updated_user_salt.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'updated_user_salt.freezed.dart'; 4 | part 'updated_user_salt.g.dart'; 5 | 6 | @freezed 7 | class UpdatedUserSalt with _$UpdatedUserSalt { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory UpdatedUserSalt({ 10 | required String salt, 11 | }) = _UpdatedUserSalt; 12 | 13 | factory UpdatedUserSalt.fromJson(Map json) => _$UpdatedUserSaltFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /server-ui/lib/model/formats.dart: -------------------------------------------------------------------------------- 1 | Duration durationFromJson(int duration) => Duration(seconds: duration); 2 | 3 | int durationToJson(Duration duration) => duration.inSeconds; 4 | 5 | DateTime dateTimeFromJson(String dateTime) => DateTime.parse(dateTime); 6 | 7 | String dateTimeToJson(DateTime dateTime) => dateTime.toIso8601String(); 8 | -------------------------------------------------------------------------------- /server-ui/lib/utils/chrono_unit.dart: -------------------------------------------------------------------------------- 1 | enum ChronoUnit { 2 | seconds(singular: 'second', plural: 'seconds'), 3 | minutes(singular: 'minute', plural: 'minutes'), 4 | hours(singular: 'hour', plural: 'hours'), 5 | days(singular: 'day', plural: 'days'); 6 | 7 | const ChronoUnit({required this.singular, required this.plural}); 8 | final String singular; 9 | final String plural; 10 | } 11 | -------------------------------------------------------------------------------- /server-ui/lib/utils/debouncer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | class Debouncer { 4 | Debouncer({required this.timeout}); 5 | 6 | final Duration timeout; 7 | Timer? _timer; 8 | 9 | void run(void Function() action) { 10 | _timer?.cancel(); 11 | _timer = Timer(timeout, action); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server-ui/lib/utils/file_size_unit.dart: -------------------------------------------------------------------------------- 1 | enum FileSizeUnit { 2 | bytes(symbol: 'B'), 3 | kilobytes(symbol: 'kB'), 4 | megabytes(symbol: 'MB'), 5 | gigabytes(symbol: 'GB'), 6 | terabytes(symbol: 'TB'), 7 | petabytes(symbol: 'PB'); 8 | 9 | const FileSizeUnit({required this.symbol}); 10 | final String symbol; 11 | } 12 | -------------------------------------------------------------------------------- /server-ui/lib/utils/pair.dart: -------------------------------------------------------------------------------- 1 | class Pair { 2 | Pair(this.a, this.b); 3 | 4 | final A a; 5 | final B b; 6 | 7 | @override 8 | String toString() => '($a,$b)'; 9 | 10 | @override 11 | bool operator ==(Object other) { 12 | return other is Pair && a == other.a && b == other.b; 13 | } 14 | 15 | @override 16 | int get hashCode => Object.hash(a, b); 17 | } 18 | -------------------------------------------------------------------------------- /server-ui/lib/utils/triple.dart: -------------------------------------------------------------------------------- 1 | class Triple { 2 | Triple(this.a, this.b, this.c); 3 | 4 | final A a; 5 | final B b; 6 | final C c; 7 | 8 | @override 9 | String toString() => '($a,$b,$c)'; 10 | 11 | @override 12 | bool operator ==(Object other) { 13 | return other is Triple && a == other.a && b == other.b && c == other.c; 14 | } 15 | 16 | @override 17 | int get hashCode => Object.hash(a, b, c); 18 | } 19 | -------------------------------------------------------------------------------- /server-ui/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server_ui", 3 | "short_name": "server_ui", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "assets/logo.svg", 14 | "sizes": "192x192", 15 | "type": "image/svg+xml" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # stasis / server 2 | 3 | Backup management and storage service. 4 | 5 | * The [Server API](./src/main/scala/stasis/server/api/ApiEndpoint.scala) is available at `/` 6 | * The [Core Endpoint](../core/src/main/scala/stasis/core/networking/http/HttpEndpoint.scala) is available on a separate 7 | port (set via [environment or config](./src/main/resources/reference.conf)) 8 | * Prometheus metrics are available on a separate port (set via [environment or config](./src/main/resources/reference.conf)) 9 | -------------------------------------------------------------------------------- /server/src/main/scala/stasis/server/Main.scala: -------------------------------------------------------------------------------- 1 | package stasis.server 2 | 3 | import stasis.server.service.Service 4 | 5 | object Main extends App with Service 6 | -------------------------------------------------------------------------------- /server/src/main/scala/stasis/server/security/CurrentUser.scala: -------------------------------------------------------------------------------- 1 | package stasis.server.security 2 | import stasis.shared.model.users.User 3 | 4 | final case class CurrentUser(id: User.Id) { 5 | override def toString: String = id.toString 6 | } 7 | -------------------------------------------------------------------------------- /server/src/main/scala/stasis/server/security/Resource.scala: -------------------------------------------------------------------------------- 1 | package stasis.server.security 2 | 3 | import stasis.shared.security.Permission 4 | 5 | trait Resource { 6 | def requiredPermission: Permission 7 | } 8 | -------------------------------------------------------------------------------- /server/src/main/scala/stasis/server/security/ResourceProvider.scala: -------------------------------------------------------------------------------- 1 | package stasis.server.security 2 | 3 | import scala.concurrent.Future 4 | import scala.reflect.ClassTag 5 | 6 | trait ResourceProvider { 7 | def provide[R <: Resource](implicit user: CurrentUser, tag: ClassTag[R]): Future[R] 8 | } 9 | -------------------------------------------------------------------------------- /server/src/main/scala/stasis/server/security/authenticators/BootstrapCodeAuthenticator.scala: -------------------------------------------------------------------------------- 1 | package stasis.server.security.authenticators 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.http.scaladsl.model.headers.HttpCredentials 6 | 7 | import stasis.server.security.CurrentUser 8 | import stasis.shared.model.devices.DeviceBootstrapCode 9 | 10 | trait BootstrapCodeAuthenticator { 11 | def authenticate(credentials: HttpCredentials): Future[(DeviceBootstrapCode, CurrentUser)] 12 | } 13 | -------------------------------------------------------------------------------- /server/src/main/scala/stasis/server/security/authenticators/UserAuthenticator.scala: -------------------------------------------------------------------------------- 1 | package stasis.server.security.authenticators 2 | 3 | import scala.concurrent.Future 4 | 5 | import org.apache.pekko.http.scaladsl.model.headers.HttpCredentials 6 | 7 | import stasis.server.security.CurrentUser 8 | 9 | trait UserAuthenticator { 10 | def authenticate(credentials: HttpCredentials): Future[CurrentUser] 11 | } 12 | -------------------------------------------------------------------------------- /server/src/main/scala/stasis/server/security/devices/DeviceCredentialsManager.scala: -------------------------------------------------------------------------------- 1 | package stasis.server.security.devices 2 | 3 | import scala.concurrent.Future 4 | 5 | import stasis.shared.model.devices.Device 6 | 7 | trait DeviceCredentialsManager { 8 | def setClientSecret(device: Device, clientSecret: String): Future[String] 9 | } 10 | -------------------------------------------------------------------------------- /server/src/main/scala/stasis/server/security/exceptions/AuthorizationFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.server.security.exceptions 2 | 3 | import stasis.layers.security.exceptions.SecurityFailure 4 | 5 | final case class AuthorizationFailure(override val message: String) extends SecurityFailure(message) 6 | -------------------------------------------------------------------------------- /server/src/main/scala/stasis/server/security/exceptions/CredentialsManagementFailure.scala: -------------------------------------------------------------------------------- 1 | package stasis.server.security.exceptions 2 | 3 | final case class CredentialsManagementFailure(message: String) extends Exception(message) 4 | -------------------------------------------------------------------------------- /server/src/test/resources/bootstrap-device-additional-config.conf: -------------------------------------------------------------------------------- 1 | a { 2 | b { 3 | c: "d" 4 | e: 1 5 | f: ["g", "h"] 6 | } 7 | } -------------------------------------------------------------------------------- /server/src/test/scala/stasis/server/security/mocks/MockDeviceClientSecretGenerator.scala: -------------------------------------------------------------------------------- 1 | package stasis.server.security.mocks 2 | 3 | import scala.concurrent.Future 4 | 5 | import stasis.server.security.devices.DeviceClientSecretGenerator 6 | 7 | class MockDeviceClientSecretGenerator extends DeviceClientSecretGenerator { 8 | override def generate(): Future[String] = 9 | Future.successful("test-secret") 10 | } 11 | -------------------------------------------------------------------------------- /server/src/test/scala/stasis/server/security/mocks/MockDeviceCredentialsManager.scala: -------------------------------------------------------------------------------- 1 | package stasis.server.security.mocks 2 | 3 | import scala.concurrent.Future 4 | 5 | import stasis.server.security.devices.DeviceCredentialsManager 6 | import stasis.shared.model.devices.Device 7 | 8 | class MockDeviceCredentialsManager extends DeviceCredentialsManager { 9 | override def setClientSecret(device: Device, clientSecret: String): Future[String] = 10 | Future.successful("test-client-id") 11 | } 12 | -------------------------------------------------------------------------------- /shared/README.md: -------------------------------------------------------------------------------- 1 | # stasis / shared 2 | 3 | API and model code shared between the [`server`](../server) and [`client`](../client) submodules. 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/requests/ReEncryptDeviceSecret.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.requests 2 | 3 | final case class ReEncryptDeviceSecret(userPassword: String) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/requests/ResetUserPassword.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.requests 2 | 3 | final case class ResetUserPassword(rawPassword: String) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/requests/UpdateDeviceLimits.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.requests 2 | 3 | import stasis.shared.model.devices.Device 4 | 5 | final case class UpdateDeviceLimits(limits: Option[Device.Limits]) extends UpdateDevice 6 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/requests/UpdateDeviceState.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.requests 2 | 3 | final case class UpdateDeviceState(active: Boolean) extends UpdateDevice 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/requests/UpdateUserLimits.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.requests 2 | 3 | import stasis.shared.model.users.User 4 | 5 | final case class UpdateUserLimits(limits: Option[User.Limits]) extends UpdateUser 6 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/requests/UpdateUserPasswordOwn.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.requests 2 | 3 | final case class UpdateUserPasswordOwn(currentPassword: String, newPassword: String) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/requests/UpdateUserPermissions.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.requests 2 | 3 | import stasis.shared.security.Permission 4 | 5 | final case class UpdateUserPermissions(permissions: Set[Permission]) extends UpdateUser 6 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/requests/UpdateUserSalt.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.requests 2 | 3 | final case class UpdateUserSalt(salt: String) extends UpdateUser 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/requests/UpdateUserSaltOwn.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.requests 2 | 3 | final case class UpdateUserSaltOwn(currentPassword: String, newSalt: String) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/requests/UpdateUserState.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.requests 2 | 3 | final case class UpdateUserState(active: Boolean) extends UpdateUser 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/CreatedDatasetDefinition.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | import stasis.shared.model.datasets.DatasetDefinition 4 | 5 | final case class CreatedDatasetDefinition(definition: DatasetDefinition.Id) 6 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/CreatedDatasetEntry.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | import stasis.shared.model.datasets.DatasetEntry 4 | 5 | final case class CreatedDatasetEntry(entry: DatasetEntry.Id) 6 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/CreatedDevice.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | import stasis.core.routing.Node 4 | import stasis.shared.model.devices.Device 5 | 6 | final case class CreatedDevice(device: Device.Id, node: Node.Id) 7 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/CreatedNode.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | import stasis.core.routing.Node 4 | 5 | final case class CreatedNode(node: Node.Id) 6 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/CreatedSchedule.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | import stasis.shared.model.schedules.Schedule 4 | 5 | final case class CreatedSchedule(schedule: Schedule.Id) 6 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/CreatedUser.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | import stasis.shared.model.users.User 4 | 5 | final case class CreatedUser(user: User.Id) 6 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedCommand.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedCommand(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedDatasetDefinition.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedDatasetDefinition(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedDatasetEntry.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedDatasetEntry(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedDevice.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedDevice(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedDeviceKey.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedDeviceKey(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedManifest.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedManifest(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedNode.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedNode(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedPendingDestaging.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedPendingDestaging(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedReservation.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedReservation(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedSchedule.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedSchedule(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/DeletedUser.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class DeletedUser(existing: Boolean) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/Ping.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class Ping(id: java.util.UUID) 4 | 5 | object Ping { 6 | def apply(): Ping = Ping(id = java.util.UUID.randomUUID()) 7 | } 8 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/api/responses/UpdatedUserSalt.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.api.responses 2 | 3 | final case class UpdatedUserSalt(salt: String) 4 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/model/datasets/DatasetEntry.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.model.datasets 2 | 3 | import java.time.Instant 4 | 5 | import stasis.core.packaging.Crate 6 | import stasis.shared.model.devices.Device 7 | 8 | final case class DatasetEntry( 9 | id: DatasetEntry.Id, 10 | definition: DatasetDefinition.Id, 11 | device: Device.Id, 12 | data: Set[Crate.Id], 13 | metadata: Crate.Id, 14 | created: Instant 15 | ) 16 | 17 | object DatasetEntry { 18 | type Id = java.util.UUID 19 | 20 | def generateId(): Id = java.util.UUID.randomUUID() 21 | } 22 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/model/devices/DeviceKey.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.model.devices 2 | 3 | import java.time.Instant 4 | 5 | import org.apache.pekko.util.ByteString 6 | 7 | import stasis.shared.model.users.User 8 | 9 | final case class DeviceKey( 10 | value: ByteString, 11 | owner: User.Id, 12 | device: Device.Id, 13 | created: Instant 14 | ) 15 | -------------------------------------------------------------------------------- /shared/src/main/scala/stasis/shared/security/Permission.scala: -------------------------------------------------------------------------------- 1 | package stasis.shared.security 2 | 3 | sealed trait Permission 4 | 5 | object Permission { 6 | sealed trait View extends Permission 7 | object View { 8 | case object Self extends View 9 | case object Privileged extends View 10 | case object Public extends View 11 | case object Service extends View 12 | } 13 | 14 | sealed trait Manage extends Permission 15 | object Manage { 16 | case object Self extends Manage 17 | case object Privileged extends Manage 18 | case object Service extends Manage 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "1.4.3-SNAPSHOT" 2 | --------------------------------------------------------------------------------