commit 95d3845906ea94e0e69245486708fc4e5bda0055 Author: Aiden McClelland Date: Mon Nov 23 13:44:28 2020 -0700 0.2.5 initial commit Makefile incomplete diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..fef391c2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/*.img +/buster.zip +/product_key \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..6700cd931 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,18 @@ +# START9 PERSONAL USE LICENSE v1.0 + +This license governs the use of the accompanying Software. If you use the Software, you accept this license. If you do not accept the license, do not use the Software. + +1. **Definitions** + a. "Licensor" means the copyright owner, Start9 Labs, Inc, or its successor(s) in interest, or a future assignee of the copyright. + b. "Source Code" means the code made available to you by the Licensor pursuant to the terms of this license and any derivative works based thereon. + c. "Object Code" means any non-source form of the Source Code, including the machine-language output by a compiler or assembler. + d. "Distribute" means to convey or to publish and generally has the same meaning here as under U.S. Copyright law. + e. "Personal Use" means accessing, copying, reviewing, auditing, running, testing, or modifying the Source Code. +2. **Grant of Rights** + a. Subject to the terms of this license, the Licensor grants you, the licensee, a non-exclusive, worldwide, royalty-free copyright license to the Source Code for Personal Use only. + b. Subject to the terms of this license, the Licensor grants you, the licensee, the right to Distribute the Source Code or modifications to the Source Code. + c. Distributing the Object Code, or any Object Code created based on your modifications to the Source Code, is not permitted under the terms of this license without express written consent of the Licensor. + d. If you Distribute the Source Code, or if permission is granted to Distribute the Object Code, you expressly undertake not to remove, or modify, in any manner, the copyright notices attached to the Source Code, and displayed in any output of the Object Code when run, and to reproduce these notices, in an identical manner, in any distributed copies of the Source Code or Object Code together with a copy of this license. If you Distribute a modified copy of the Software or a derivative work based thereon, the work must carry prominent notices stating that you modified it, and giving a relevant date. + e. The terms of this license will apply to anyone who comes into possession of a copy of the Source Code or Object Code, and any modifications or derivative works based thereon, made by anyone. +3. **Disclaimer.** THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Licensor has no obligation to support recipients of the Source Code or Object Code. +4. **Contributions.** You hereby grant to Licensor a perpetual, irrevocable, worldwide, non-exclusive, royalty-free license to use and exploit any modifications or derivative works based on the Source Code of which you are the author. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..dc3a22f4e --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +APPMGR_SRC := $(shell find appmgr/src) appmgr/Cargo.toml appmgr/Cargo.lock + +.DELETE_ON_ERROR: + +all: embassy.img + +embassy.img: buster.img product_key appmgr/target/armv7-unknown-linux-musleabihf/release/appmgr ui/www agent/dist/agent agent/config/agent.service + ./make_image.sh + +buster.img: + wget -O buster.zip https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2020-08-24/2020-08-20-raspios-buster-armhf-lite.zip + unzip buster.zip + rm buster.zip + mv 2020-08-20-raspios-buster-armhf-lite.img buster.img + +product_key: + echo "X\c" > product_key + cat /dev/random | base32 | head -c11 | tr '[:upper:]' '[:lower:]' >> product_key + +appmgr/target/armv7-unknown-linux-musleabihf/release/appmgr: $(APPMGR_SRC) + docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)"/appmgr:/home/rust/src start9/rust-arm-cross:latest cargo build --release --features=production + docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)"/appmgr:/home/rust/src start9/rust-arm-cross:latest arm-linux-gnueabi-strip target/armv7-unknown-linux-gnueabihf/release/appmgr \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..723d40c52 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Embassy OS diff --git a/agent/.gitignore b/agent/.gitignore new file mode 100644 index 000000000..68f879c68 --- /dev/null +++ b/agent/.gitignore @@ -0,0 +1,42 @@ +dist* +static/tmp/ +static/combined/ +config/client_session_key.aes +*.hi +*.o +*.sqlite3 +*.sqlite3-shm +*.sqlite3-wal +.hsenv* +cabal-dev/ +.stack-work/ +.stack-work-devel/ +yesod-devel/ +.cabal-sandbox +cabal.sandbox.config +.DS_Store +*.swp +*.keter +*~ +.vscode +*.cabal +\#* +start9-companion-server.cabal +stack.yaml.lock +*.env +agent_* +agent.* +agent* +!agent.service +executables/* +hidden/* +cabal.project.local +dump/* +*.tar.gz +assets/ +911.txt +model +product_key +build-send.sh +*.aes +*.hie diff --git a/agent/.stylish-haskell.yaml b/agent/.stylish-haskell.yaml new file mode 100644 index 000000000..77f782fc0 --- /dev/null +++ b/agent/.stylish-haskell.yaml @@ -0,0 +1,252 @@ +# stylish-haskell configuration file +# ================================== + +# The stylish-haskell tool is mainly configured by specifying steps. These steps +# are a list, so they have an order, and one specific step may appear more than +# once (if needed). Each file is processed by these steps in the given order. +steps: + # Convert some ASCII sequences to their Unicode equivalents. This is disabled + # by default. + # - unicode_syntax: + # # In order to make this work, we also need to insert the UnicodeSyntax + # # language pragma. If this flag is set to true, we insert it when it's + # # not already present. You may want to disable it if you configure + # # language extensions using some other method than pragmas. Default: + # # true. + # add_language_pragma: true + + # Align the right hand side of some elements. This is quite conservative + # and only applies to statements where each element occupies a single + # line. All default to true. + - simple_align: + cases: true + top_level_patterns: true + records: true + + # Import cleanup + - imports: + # There are different ways we can align names and lists. + # + # - global: Align the import names and import list throughout the entire + # file. + # + # - file: Like global, but don't add padding when there are no qualified + # imports in the file. + # + # - group: Only align the imports per group (a group is formed by adjacent + # import lines). + # + # - none: Do not perform any alignment. + # + # Default: global. + align: global + + # The following options affect only import list alignment. + # + # List align has following options: + # + # - after_alias: Import list is aligned with end of import including + # 'as' and 'hiding' keywords. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # > init, last, length) + # + # - with_alias: Import list is aligned with start of alias or hiding. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # > init, last, length) + # + # - with_module_name: Import list is aligned `list_padding` spaces after + # the module name. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # init, last, length) + # + # This is mainly intended for use with `pad_module_names: false`. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # init, last, length, scanl, scanr, take, drop, + # sort, nub) + # + # - new_line: Import list starts always on new line. + # + # > import qualified Data.List as List + # > (concat, foldl, foldr, head, init, last, length) + # + # Default: after_alias + list_align: after_alias + + # Right-pad the module names to align imports in a group: + # + # - true: a little more readable + # + # > import qualified Data.List as List (concat, foldl, foldr, + # > init, last, length) + # > import qualified Data.List.Extra as List (concat, foldl, foldr, + # > init, last, length) + # + # - false: diff-safe + # + # > import qualified Data.List as List (concat, foldl, foldr, init, + # > last, length) + # > import qualified Data.List.Extra as List (concat, foldl, foldr, + # > init, last, length) + # + # Default: true + pad_module_names: true + + # Long list align style takes effect when import is too long. This is + # determined by 'columns' setting. + # + # - inline: This option will put as much specs on same line as possible. + # + # - new_line: Import list will start on new line. + # + # - new_line_multiline: Import list will start on new line when it's + # short enough to fit to single line. Otherwise it'll be multiline. + # + # - multiline: One line per import list entry. + # Type with constructor list acts like single import. + # + # > import qualified Data.Map as M + # > ( empty + # > , singleton + # > , ... + # > , delete + # > ) + # + # Default: inline + long_list_align: inline + + # Align empty list (importing instances) + # + # Empty list align has following options + # + # - inherit: inherit list_align setting + # + # - right_after: () is right after the module name: + # + # > import Vector.Instances () + # + # Default: inherit + empty_list_align: inherit + + # List padding determines indentation of import list on lines after import. + # This option affects 'long_list_align'. + # + # - : constant value + # + # - module_name: align under start of module name. + # Useful for 'file' and 'group' align settings. + # + # Default: 4 + list_padding: 4 + + # Separate lists option affects formatting of import list for type + # or class. The only difference is single space between type and list + # of constructors, selectors and class functions. + # + # - true: There is single space between Foldable type and list of it's + # functions. + # + # > import Data.Foldable (Foldable (fold, foldl, foldMap)) + # + # - false: There is no space between Foldable type and list of it's + # functions. + # + # > import Data.Foldable (Foldable(fold, foldl, foldMap)) + # + # Default: true + separate_lists: true + + # Space surround option affects formatting of import lists on a single + # line. The only difference is single space after the initial + # parenthesis and a single space before the terminal parenthesis. + # + # - true: There is single space associated with the enclosing + # parenthesis. + # + # > import Data.Foo ( foo ) + # + # - false: There is no space associated with the enclosing parenthesis + # + # > import Data.Foo (foo) + # + # Default: false + space_surround: false + + # Language pragmas + - language_pragmas: + + # We can generate different styles of language pragma lists. + # + # - vertical: Vertical-spaced language pragmas, one per line. + # + # - compact: A more compact style. + # + # - compact_line: Similar to compact, but wrap each line with + # `{-#LANGUAGE #-}'. + # + # Default: vertical. + style: vertical + + # Align affects alignment of closing pragma brackets. + # + # - true: Brackets are aligned in same column. + # + # - false: Brackets are not aligned together. There is only one space + # between actual import and closing bracket. + # + # Default: true + align: true + + # stylish-haskell can detect redundancy of some language pragmas. If this + # is set to true, it will remove those redundant pragmas. Default: true. + remove_redundant: false + + # Replace tabs by spaces. This is disabled by default. + - tabs: + # Number of spaces to use for each tab. Default: 8, as specified by the + # Haskell report. + spaces: 4 + + # Remove trailing whitespace + - trailing_whitespace: {} + + # Squash multiple spaces between the left and right hand sides of some + # elements into single spaces. Basically, this undoes the effect of + # simple_align but is a bit less conservative. + # - squash: {} + +# A common setting is the number of columns (parts of) code will be wrapped +# to. Different steps take this into account. Default: 80. +columns: 120 + +# By default, line endings are converted according to the OS. You can override +# preferred format here. +# +# - native: Native newline format. CRLF on Windows, LF on other OSes. +# +# - lf: Convert to LF ("\n"). +# +# - crlf: Convert to CRLF ("\r\n"). +# +# Default: native. +newline: native + +# Sometimes, language extensions are specified in a cabal file or from the +# command line instead of using language pragmas in the file. stylish-haskell +# needs to be aware of these, so it can parse the file correctly. +# +# No language extensions are enabled by default. +language_extensions: + - NoImplicitPrelude + - FlexibleContexts + - FlexibleInstances + - GeneralizedNewtypeDeriving + - LambdaCase + - MultiWayIf + - NamedFieldPuns + - NumericUnderscores + - OverloadedStrings + - TypeApplications diff --git a/agent/Changelog.md b/agent/Changelog.md new file mode 100644 index 000000000..c5162c552 --- /dev/null +++ b/agent/Changelog.md @@ -0,0 +1,12 @@ +# 0.2.5 + +- Upgrade to GHC 8.10.2 / Stackage nightly-2020-09-29 +- Remove internet connectivity check from startup sequence +- Move ssh setup to synchronizers +- Adds new dependency management structure +- Changes version implementation from semver to new "emver" implementation +- Adds autoconfigure feature +- Remaps "Restarting" container status to "Crashed" for better UX +- Persists logs after restart +- Rewrites nginx ssl conf during UI upgrade +- Implements better caching strategy for static assets \ No newline at end of file diff --git a/agent/README.md b/agent/README.md new file mode 100644 index 000000000..4b31a7dc7 --- /dev/null +++ b/agent/README.md @@ -0,0 +1,7 @@ +# Design Decision Log + +* 1/4/20 - Switching from HTTPS to HTTP over local LAN. Due to eventual Tor support/default, this gives +us the neatest slot for the Tor support + * This means it is possible to snoop on traffic between the companion app and the server if you + have a LAN presence. + * This also makes it possible to masquerade as the server if you have a LAN presence \ No newline at end of file diff --git a/agent/TODO.md b/agent/TODO.md new file mode 100644 index 000000000..79d19f3dd --- /dev/null +++ b/agent/TODO.md @@ -0,0 +1,3 @@ +* When adding ssh keys, don't add if identical one exists +* When adding ssh keys, check for newline at the end of the file. if not exists, add it. +* If `appmgr stop ` throws no error, but completes without the app being stopped, we need to restart dockerd. diff --git a/agent/app/main.hs b/agent/app/main.hs new file mode 100644 index 000000000..035cf5583 --- /dev/null +++ b/agent/app/main.hs @@ -0,0 +1,5 @@ +import Application ( appMain ) +import Startlude + +main :: IO () +main = appMain diff --git a/agent/brittany.yaml b/agent/brittany.yaml new file mode 100644 index 000000000..e07b9b930 --- /dev/null +++ b/agent/brittany.yaml @@ -0,0 +1,60 @@ +conf_debug: + dconf_roundtrip_exactprint_only: false + dconf_dump_bridoc_simpl_par: false + dconf_dump_ast_unknown: false + dconf_dump_bridoc_simpl_floating: false + dconf_dump_config: false + dconf_dump_bridoc_raw: false + dconf_dump_bridoc_final: false + dconf_dump_bridoc_simpl_alt: false + dconf_dump_bridoc_simpl_indent: false + dconf_dump_annotations: false + dconf_dump_bridoc_simpl_columns: false + dconf_dump_ast_full: false +conf_forward: + options_ghc: + - -XNoImplicitPrelude + - -XBlockArguments + - -XFlexibleContexts + - -XFlexibleInstances + - -XGeneralizedNewtypeDeriving + - -XKindSignatures + - -XLambdaCase + - -XMultiWayIf + - -XNamedFieldPuns + - -XNumericUnderscores + - -XOverloadedStrings + - -XTemplateHaskell + - -XTypeApplications +conf_errorHandling: + econf_ExactPrintFallback: ExactPrintFallbackModeInline + econf_Werror: false + econf_omit_output_valid_check: false + econf_produceOutputOnErrors: false +conf_preprocessor: + ppconf_CPPMode: CPPModeWarn + ppconf_hackAroundIncludes: false +conf_obfuscate: false +conf_roundtrip_exactprint_only: false +conf_version: 1 +conf_layout: + lconfig_reformatModulePreamble: true + lconfig_altChooser: + tag: AltChooserBoundedSearch + contents: 3 + lconfig_allowSingleLineExportList: false + lconfig_importColumn: 50 + lconfig_hangingTypeSignature: true + lconfig_importAsColumn: 50 + lconfig_alignmentLimit: 30 + lconfig_allowHangingQuasiQuotes: true + lconfig_indentListSpecial: true + lconfig_indentAmount: 4 + lconfig_alignmentBreakOnMultiline: true + lconfig_experimentalSemicolonNewlines: false + lconfig_cols: 120 + lconfig_indentPolicy: IndentPolicyFree + lconfig_indentWhereSpecial: false + lconfig_columnAlignMode: + tag: ColumnAlignModeMajority + contents: 0.7 diff --git a/agent/build.sh b/agent/build.sh new file mode 100755 index 000000000..82cd44b54 --- /dev/null +++ b/agent/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +cat config/settings.yml | grep app-mgr-version-spec +cat package.yaml | grep version + +stack --local-bin-path ./executables build --copy-bins #--flag start9-agent:disable-auth +upx ./executables/agent diff --git a/agent/config/agent.service b/agent/config/agent.service new file mode 100644 index 000000000..791d68d08 --- /dev/null +++ b/agent/config/agent.service @@ -0,0 +1,14 @@ +[Unit] +Description=Boot process for system reset. +After=network.target lifeline.service avahi-daemon.service systemd-time-wait-sync.service +Requires=network.target +Wants=avahi-daemon.service + +[Service] +Type=simple +ExecStart=/usr/local/bin/agent +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/agent/config/journald.conf b/agent/config/journald.conf new file mode 100644 index 000000000..47951df4c --- /dev/null +++ b/agent/config/journald.conf @@ -0,0 +1,6 @@ +[Journal] +Storage=persistent +SystemMaxUse=100M +SystemMaxFileSize=10M +MaxRetentionSec=1month +MaxFileSec=1week \ No newline at end of file diff --git a/agent/config/nginx.conf b/agent/config/nginx.conf new file mode 100644 index 000000000..fa6f87b13 --- /dev/null +++ b/agent/config/nginx.conf @@ -0,0 +1,29 @@ +user www-data; +worker_processes 1; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; + multi_accept on; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + gzip on; + + server_names_hash_bucket_size 128; + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} \ No newline at end of file diff --git a/agent/config/routes b/agent/config/routes new file mode 100644 index 000000000..ace977d7d --- /dev/null +++ b/agent/config/routes @@ -0,0 +1,54 @@ +/auth AuthR Auth getAuth !noAuth + +/git GitR GET +/authenticate AuthenticateR GET +/version VersionR GET !noAuth +/versionLatest VersionLatestR GET !noAuth +/v0 ServerR GET PATCH + +/v0/name NameR PATCH + +/v0/specs SpecsR GET +/v0/metrics MetricsR GET + +/v0/sshKeys SshKeysR GET POST +/v0/sshKeys/#Text SshKeyByFingerprintR DELETE +/v0/password PasswordR PATCH + +/v0/apps/store AvailableAppsR GET -- reg reliant +/v0/apps/installed InstalledAppsR GET +/v0/apps/#AppId/store AvailableAppByIdR GET -- reg reliant + +/v0/apps/#AppId/store/#VersionRange AvailableAppVersionInfoR GET -- reg reliant +/v0/apps/#AppId/installed InstalledAppByIdR GET +/v0/apps/#AppId/logs AppLogsByIdR GET +/v0/apps/#AppId/install InstallNewAppR POST -- reg reliant +/v0/apps/#AppId/config AppConfigR GET PATCH +/v0/apps/#AppId/start StartServerAppR POST +/v0/apps/#AppId/restart RestartServerAppR POST +/v0/apps/#AppId/stop StopServerAppR POST +/v0/apps/#AppId/uninstall UninstallAppR POST +/v0/apps/#AppId/notifications AppNotificationsR GET +/v0/apps/#AppId/metrics AppMetricsR GET +/v0/apps/#AppId/icon AppIconR GET !noAuth !cached +/v0/apps/#AppId/icon/store AvailableAppIconR GET !noAuth !cached -- reg reliant +/v0/apps/#AppId/backup CreateBackupR POST +/v0/apps/#AppId/backup/stop StopBackupR POST +/v0/apps/#AppId/backup/restore RestoreBackupR POST +/v0/apps/#AppId/autoconfig/#AppId AutoconfigureR POST + +/v0/disks ListDisksR GET + +/v0/update UpdateAgentR POST +/v0/wifi WifiR GET POST +/v0/wifi/#Text WifiBySsidR POST DELETE + +/v0/notifications NotificationsR GET +/v0/notifications/#UUID NotificationR DELETE + +/v0/shutdown ShutdownR POST +/v0/restart RestartR POST + +/v0/register RegisterR POST !noAuth +/v0/hosts HostsR GET !noAuth +/v0/certificate CertificateR GET \ No newline at end of file diff --git a/agent/config/settings.yml b/agent/config/settings.yml new file mode 100644 index 000000000..e86956673 --- /dev/null +++ b/agent/config/settings.yml @@ -0,0 +1,39 @@ +# Values formatted like "_env:YESOD_ENV_VAR_NAME:default_value" can be overridden by the specified environment variable. +# See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables + +static-dir: "_env:YESOD_STATIC_DIR:static" +host: "_env:YESOD_HOST:*4" # any IPv4 host +port: 5959 # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line. +ip-from-header: "_env:YESOD_IP_FROM_HEADER:false" +detailed-logging: "_env:DETAILED_LOGGING:false" + +# Default behavior: determine the application root from the request headers. +# Uncomment to set an explicit approot +#approot: "_env:YESOD_APPROOT:http://localhost:3000" + +# By default, `yesod devel` runs in development, and built executables use +# production settings (see below). To override this, use the following: +# +# development: false + +# Optional values with the following production defaults. +# In development, they default to the inverse. +# +# detailed-logging: false +# should-log-all: false +# reload-templates: false +# mutable-static: false +# skip-combining: false +# auth-dummy-login : false + +# NB: If you need a numeric value (e.g. 123) to parse as a String, wrap it in single quotes (e.g. "_env:YESOD_PGPASS:'123'") +# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings +cors-override-star: "_env:CORS_OVERRIDE_STAR:" +filesystem-base: "_env:FILESYSTEM_BASE:/" +database: + database: "start9_agent.sqlite3" + poolsize: "_env:YESOD_SQLITE_POOLSIZE:10" + +app-mgr-version-spec: "=0.2.5" + +#analytics: UA-YOURCODE diff --git a/agent/config/torrc b/agent/config/torrc new file mode 100644 index 000000000..68ede8e27 --- /dev/null +++ b/agent/config/torrc @@ -0,0 +1,5 @@ +SOCKSPort 0.0.0.0:9050 # Default: Bind to localhost:9050 for local connections. +HiddenServiceDir /var/lib/tor/agent/ +HiddenServicePort 5959 127.0.0.1:5959 +HiddenServicePort 80 127.0.0.1:80 +HiddenServicePort 443 127.0.0.1:443 \ No newline at end of file diff --git a/agent/hie.yaml b/agent/hie.yaml new file mode 100644 index 000000000..ed5bc79c8 --- /dev/null +++ b/agent/hie.yaml @@ -0,0 +1,13 @@ +cradle: + stack: + - path: "./src" + component: "ambassador-agent:lib" + + - path: "./app/main.hs" + component: "ambassador-agent:exe:agent" + + - path: "./test" + component: "ambassador-agent:test:agent-test" + + - path: "./" + component: "ambassador-agent:lib" \ No newline at end of file diff --git a/agent/migrations/0.1.0::0.1.0 b/agent/migrations/0.1.0::0.1.0 new file mode 100644 index 000000000..b928005e2 --- /dev/null +++ b/agent/migrations/0.1.0::0.1.0 @@ -0,0 +1 @@ +SELECT TRUE; \ No newline at end of file diff --git a/agent/migrations/0.1.0::0.1.1 b/agent/migrations/0.1.0::0.1.1 new file mode 100644 index 000000000..cb9f5d239 --- /dev/null +++ b/agent/migrations/0.1.0::0.1.1 @@ -0,0 +1 @@ +CREATE TABLE "replay_nonce"("id" VARCHAR PRIMARY KEY,"created_at" TIMESTAMP NOT NULL); \ No newline at end of file diff --git a/agent/migrations/0.1.1::0.1.2 b/agent/migrations/0.1.1::0.1.2 new file mode 100644 index 000000000..b928005e2 --- /dev/null +++ b/agent/migrations/0.1.1::0.1.2 @@ -0,0 +1 @@ +SELECT TRUE; \ No newline at end of file diff --git a/agent/migrations/0.1.2::0.1.3 b/agent/migrations/0.1.2::0.1.3 new file mode 100644 index 000000000..b928005e2 --- /dev/null +++ b/agent/migrations/0.1.2::0.1.3 @@ -0,0 +1 @@ +SELECT TRUE; \ No newline at end of file diff --git a/agent/migrations/0.1.3::0.1.4 b/agent/migrations/0.1.3::0.1.4 new file mode 100644 index 000000000..3471bf585 --- /dev/null +++ b/agent/migrations/0.1.3::0.1.4 @@ -0,0 +1 @@ +SELECT TRUE; diff --git a/agent/migrations/0.1.4::0.1.5 b/agent/migrations/0.1.4::0.1.5 new file mode 100644 index 000000000..3471bf585 --- /dev/null +++ b/agent/migrations/0.1.4::0.1.5 @@ -0,0 +1 @@ +SELECT TRUE; diff --git a/agent/migrations/0.1.5::0.2.0 b/agent/migrations/0.1.5::0.2.0 new file mode 100644 index 000000000..fa220ddf7 --- /dev/null +++ b/agent/migrations/0.1.5::0.2.0 @@ -0,0 +1,2 @@ +DROP TABLE authorized_key; +DROP TABLE replay_nonce; diff --git a/agent/migrations/0.2.0::0.2.1 b/agent/migrations/0.2.0::0.2.1 new file mode 100644 index 000000000..b928005e2 --- /dev/null +++ b/agent/migrations/0.2.0::0.2.1 @@ -0,0 +1 @@ +SELECT TRUE; \ No newline at end of file diff --git a/agent/migrations/0.2.1::0.2.2 b/agent/migrations/0.2.1::0.2.2 new file mode 100644 index 000000000..3471bf585 --- /dev/null +++ b/agent/migrations/0.2.1::0.2.2 @@ -0,0 +1 @@ +SELECT TRUE; diff --git a/agent/migrations/0.2.2::0.2.3 b/agent/migrations/0.2.2::0.2.3 new file mode 100644 index 000000000..b928005e2 --- /dev/null +++ b/agent/migrations/0.2.2::0.2.3 @@ -0,0 +1 @@ +SELECT TRUE; \ No newline at end of file diff --git a/agent/migrations/0.2.3::0.2.4 b/agent/migrations/0.2.3::0.2.4 new file mode 100644 index 000000000..b928005e2 --- /dev/null +++ b/agent/migrations/0.2.3::0.2.4 @@ -0,0 +1 @@ +SELECT TRUE; \ No newline at end of file diff --git a/agent/migrations/0.2.4::0.2.5 b/agent/migrations/0.2.4::0.2.5 new file mode 100644 index 000000000..b928005e2 --- /dev/null +++ b/agent/migrations/0.2.4::0.2.5 @@ -0,0 +1 @@ +SELECT TRUE; \ No newline at end of file diff --git a/agent/package.yaml b/agent/package.yaml new file mode 100644 index 000000000..5d4df9213 --- /dev/null +++ b/agent/package.yaml @@ -0,0 +1,181 @@ +name: ambassador-agent +version: 0.2.5 + +default-extensions: +- NoImplicitPrelude +- BlockArguments +- ConstraintKinds +- DataKinds +- DeriveAnyClass +- DeriveFunctor +- DeriveGeneric +- DerivingStrategies +- EmptyCase +- FlexibleContexts +- FlexibleInstances +- GADTs +- GeneralizedNewtypeDeriving +- InstanceSigs +- KindSignatures +- LambdaCase +- MultiParamTypeClasses +- MultiWayIf +- NamedFieldPuns +- NumericUnderscores +- OverloadedStrings +- PolyKinds +- RankNTypes +- StandaloneDeriving +- StandaloneKindSignatures +- TupleSections +- TypeApplications +- TypeFamilies +- TypeOperators + +dependencies: +- base >=4.9.1.0 && <5 +- aeson +- aeson-flatten +- attoparsec +- bytestring +- casing +- comonad +- conduit +- conduit-extra +- containers +- cryptonite +- cryptonite-conduit +- data-default +- directory +- errors +- exceptions +- exinst +- fast-logger +- file-embed +- filelock +- filepath +- fused-effects +- fused-effects-th +- git-embed +- http-api-data +- http-client +- http-client-tls +- http-conduit +- http-types +- interpolate +- iso8601-time +- lens +- lens-aeson +- lifted-async +- lifted-base +- memory +- mime-types +- monad-control +- monad-logger +- persistent +- persistent-sqlite +- persistent-template +- process +- process-extras +- protolude +- resourcet +- regex-compat # TODO: trim this dep +- shell-conduit +- singletons +- stm +- streaming +- streaming-bytestring +- streaming-conduit +- streaming-utils +- tar-conduit +- template-haskell +- text >=0.11 && <2.0 +- time +- transformers +- transformers-base +- typed-process +- unix +- unliftio # TODO: trim this dep +- unliftio-core # TODO: trim this dep +- unordered-containers +- uuid +- wai +- wai-cors +- wai-extra +- warp +- yaml +- yesod +- yesod-auth +- yesod-core +- yesod-form +- yesod-persistent + +flags: + library-only: + manual: false + default: false + description: Build for use with "yesod devel" + dev: + manual: false + default: false + description: Turn on development settings, like auto-reload templates. + disable-auth: + manual: false + default: false + description: disable authorization checks +library: + source-dirs: src + when: + - condition: (flag(dev)) || (flag(library-only)) + then: + cpp-options: -DDEVELOPMENT + ghc-options: + - -Wall + - -Wunused-packages + - -fwarn-tabs + - -O0 + - -fdefer-typed-holes + else: + ghc-options: + - -Wall + - -Wunused-packages + - -fwarn-tabs + - -O2 + - -fdefer-typed-holes + - condition: (flag(disable-auth)) + cpp-options: -DDISABLE_AUTH +tests: + agent-test: + source-dirs: test + main: Main.hs + ghc-options: + - -Wall + - -fdefer-typed-holes + dependencies: + - ambassador-agent + - hspec >=2.0.0 + - hspec-expectations + - hedgehog + - yesod-test + - random + when: + - condition: false + other-modules: Paths_ambassador_agent + +executables: + agent: + source-dirs: app + main: main.hs + ghc-options: + - -Wall + - -threaded + - -rtsopts + - -with-rtsopts=-N + - -fdefer-typed-holes + dependencies: + - ambassador-agent + when: + - buildable: false + condition: flag(library-only) + - condition: false + other-modules: Paths_ambassador_agent diff --git a/agent/src/Application.hs b/agent/src/Application.hs new file mode 100644 index 000000000..1be93162b --- /dev/null +++ b/agent/src/Application.hs @@ -0,0 +1,227 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +module Application + ( appMain + , makeFoundation + , makeLogWare + -- * for DevelMain + , getApplicationRepl + , getAppSettings + , shutdownAll + , shutdownWeb + , startWeb + -- * for GHCI + , handler + , runDb + , getAgentCtx + ) +where + +import Startlude hiding (runReader) + +import Control.Concurrent.STM.TVar ( newTVarIO ) +import Control.Monad.Logger +import Control.Effect.Labelled ( Labelled, runLabelled ) +import qualified Data.HashMap.Strict as HM +import Data.IORef + +import Database.Persist.Sql +import Database.Persist.Sqlite ( createSqlitePool + , runSqlite + , sqlPoolSize + , sqlDatabase + ) +import Git.Embed +import Network.HTTP.Client.TLS ( getGlobalManager ) +import Network.Wai +import Network.Wai.Handler.Warp ( getPort ) +import System.Directory ( createDirectoryIfMissing ) +import System.Environment ( setEnv ) +import System.IO hiding ( putStrLn, writeFile ) +import System.Log.FastLogger ( defaultBufSize + , newStdoutLoggerSet + ) +import Yesod.Core +import Yesod.Default.Config2 +import Yesod.Persist.Core + +import Constants +import qualified Daemon.AppNotifications as AppNotifications +import Daemon.RefreshProcDev +import Daemon.ZeroConf +import Foundation +import Lib.Algebra.State.RegistryUrl +import Lib.Database +import Lib.External.Metrics.ProcDev +import Lib.SelfUpdate +import Lib.Sound +import Lib.SystemPaths +import Lib.WebServer +import Model +import Settings +import Lib.Background + +appMain :: IO () +appMain = do + hSetBuffering stdout LineBuffering + args <- getArgs + + -- Get the settings from all relevant sources + settings <- loadYamlSettings [] [configSettingsYmlValue] useEnv + + settings' <- case args of + ["--port", n] -> case readMaybe @Word16 $ toS n of + Just n' -> pure $ settings { appPort = n' } + Nothing -> do + die . toS $ "Invalid Port: " <> n + ["--git-hash"] -> do + putStrLn @Text $embedGitRevision + exitWith ExitSuccess + ["--version"] -> do + putStrLn @Text (show agentVersion) + exitWith ExitSuccess + _ -> pure settings + createDirectoryIfMissing False (toS $ agentDataDirectory `relativeTo` appFilesystemBase settings') + + -- Generate the foundation from the settings + foundation <- makeFoundation settings' + + startupSequence foundation + +-- | This function allocates resources (such as a database connection pool), +-- performs initialization and returns a foundation datatype value. This is also +-- the place to put your migrate statements to have automatic database +-- migrations handled by Yesod. +makeFoundation :: AppSettings -> IO AgentCtx +makeFoundation appSettings = do + now <- getCurrentTime + -- Some basic initializations: HTTP connection manager, logger, and static + -- subsite. + appLogger <- newStdoutLoggerSet defaultBufSize >>= makeYesodLogger + appHttpManager <- getGlobalManager + appWebServerThreadId <- newIORef Nothing + appSelfUpdateSpecification <- newEmptyMVar + appIsUpdating <- newIORef Nothing + appIsUpdateFailed <- newIORef Nothing + appBackgroundJobs <- newTVarIO (JobCache HM.empty) + def <- getDefaultProcDevMetrics + appProcDevMomentCache <- newIORef (now, mempty, def) + + -- We need a log function to create a connection pool. We need a connection + -- pool to create our foundation. And we need our foundation to get a + -- logging function. To get out of this loop, we initially create a + -- temporary foundation without a real connection pool, get a log function + -- from there, and then create the real foundation. + let mkFoundation appConnPool appIconTags = AgentCtx { .. } + -- The AgentCtx {..} syntax is an example of record wild cards. For more + -- information, see: + -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html + tempFoundation = mkFoundation + (panic "connPool forced in tempFoundation") + (panic "iconTags forced in tempFoundation") + logFunc = messageLoggerSource tempFoundation appLogger + + db <- interpDb dbPath + + -- Create the database connection pool, will create sqlite file if doesn't already exist + pool <- flip runLoggingT logFunc $ createSqlitePool (toS db) (sqlPoolSize . appDatabaseConf $ appSettings) + + -- run migrations only if agent in charge + when (appPort appSettings == 5959) $ do + runSqlite db $ runMigration migrateAll + void . interpDb $ ensureCoherentDbVersion pool logFunc + + iconTags <- if appPort appSettings == 5959 + then do + iconDigests <- runSqlPool (selectList [] []) pool + newTVarIO . HM.fromList $ (unIconDigestKey . entityKey &&& iconDigestTag . entityVal) <$> iconDigests + else newTVarIO HM.empty + + -- Return the foundation + pure $ mkFoundation pool iconTags + where + interpDb :: (Labelled "sqlDatabase" (ReaderT Text)) (Labelled "filesystemBase" (ReaderT Text) IO) a -> IO a + interpDb = injectFilesystemBaseFromContext appSettings + . flip runReaderT (sqlDatabase . appDatabaseConf $ appSettings) + . runLabelled @"sqlDatabase" + +getAppSettings :: IO AppSettings +getAppSettings = loadYamlSettings [configSettingsYml] [] useEnv + + +startupSequence :: AgentCtx -> IO () +startupSequence foundation = do + +#ifdef DISABLE_AUTH + withAgentVersionLog_ "[WARNING] Agent auth disabled!" +#endif + + injectFilesystemBaseFromContext (appSettings foundation) . runRegistryUrlIOC $ getRegistryUrl >>= \case + Nothing -> pure () + Just x -> liftIO $ do + withAgentVersionLog "Detected Alternate Registry URL" x + -- this is so that appmgr inherits the alternate registry url when it is called. + setEnv "REGISTRY_URL" (show x) + + -- proc dev metrics refresh loop + withAgentVersionLog_ "Initializing proc dev refresh loop" + void . forkIO . forever $ forkIO (refreshProcDev foundation) >> threadDelay 5_000_000 + withAgentVersionLog_ "Proc dev metrics refreshing" + + -- web + withAgentVersionLog_ "Starting web server" + void . forkIO . startWeb $ foundation + withAgentVersionLog_ "Web server running" + + -- all these actions are destructive in some way, and only webserver is needed for self-update + when (appPort (appSettings foundation) == 5959) $ do + synchronizeSystemState foundation agentVersion + + -- app notifications refresh loop + withAgentVersionLog_ "Initializing app notifications refresh loop" + void . forkIO . forever $ forkIO (runReaderT AppNotifications.fetchAndSave foundation) >> threadDelay 5_000_000 + withAgentVersionLog_ "App notifications refreshing" + + -- reloading avahi daemon + -- DRAGONS! make sure this step happens AFTER system synchronization + withAgentVersionLog_ "Publishing Agent to Avahi Daemon" + runReaderT publishAgentToAvahi foundation + withAgentVersionLog_ "Avahi Daemon reloaded with Agent service" + + when (appPort (appSettings foundation) == 5959) $ do + playSong 400 marioCoin + + withAgentVersionLog_ "Listening for Self-Update Signal" + waitForUpdateSignal foundation + +-------------------------------------------------------------- +-- Functions for DevelMain.hs (a way to run the AgentCtx from GHCi) +-------------------------------------------------------------- + +getApplicationRepl :: IO (Int, AgentCtx, Application) +getApplicationRepl = do + foundation <- getAppSettings >>= makeFoundation + wsettings <- getDevSettings $ warpSettings foundation + app1 <- makeApplication foundation + return (getPort wsettings, foundation, app1) + +getAgentCtx :: IO AgentCtx +getAgentCtx = getAppSettings >>= makeFoundation + +--------------------------------------------- +-- Functions for use in development with GHCi +--------------------------------------------- + +-- | Run a handler +handler :: Handler a -> IO a +handler h = getAppSettings >>= makeFoundation >>= flip unsafeHandler h + +-- | Run DB queries +runDb :: ReaderT SqlBackend Handler a -> IO a +runDb = handler . runDB + diff --git a/agent/src/Auth.hs b/agent/src/Auth.hs new file mode 100644 index 000000000..069495337 --- /dev/null +++ b/agent/src/Auth.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE QuasiQuotes #-} +module Auth where + +import Startlude + +import Yesod.Core + +data Auth = Auth + +getAuth :: a -> Auth +getAuth = const Auth + +mkYesodSubData "Auth" [parseRoutes| +/login LoginR POST +/logout LogoutR POST +|] diff --git a/agent/src/Constants.hs b/agent/src/Constants.hs new file mode 100644 index 000000000..158c2bc80 --- /dev/null +++ b/agent/src/Constants.hs @@ -0,0 +1,16 @@ +module Constants where + +import Startlude + +import Data.Version ( showVersion ) +import Lib.Types.Emver ( Version ) +import Paths_ambassador_agent ( version ) + +agentVersion :: Version +agentVersion = fromString $ showVersion version + +withAgentVersionLog :: (Show a, MonadIO m) => Text -> a -> m () +withAgentVersionLog t a = liftIO $ putStrLn @Text $ show agentVersion <> "-- " <> t <> ": " <> show a + +withAgentVersionLog_ :: Text -> IO () +withAgentVersionLog_ t = putStrLn @Text $ show agentVersion <> "-- " <> t diff --git a/agent/src/Daemon/AppNotifications.hs b/agent/src/Daemon/AppNotifications.hs new file mode 100644 index 000000000..afb3f074b --- /dev/null +++ b/agent/src/Daemon/AppNotifications.hs @@ -0,0 +1,48 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +module Daemon.AppNotifications where + +import Startlude + +import qualified Data.HashMap.Strict as HM +import Data.UUID.V4 +import Data.Time.Clock.POSIX +import Database.Persist.Sql + +import Foundation +import Lib.Error +import Lib.Algebra.Domain.AppMgr as AppMgr2 +import Lib.External.AppMgr as AppMgr +import Lib.Types.Core +import Lib.Types.Emver +import Model + +toModelNotif :: (AppId, Version) -> AppMgrNotif -> Notification +toModelNotif (appId, appVersion) AppMgrNotif {..} = + let prefix = (<> "1") $ case appMgrNotifLevel of + INFO -> "0" + SUCCESS -> "1" + WARN -> "2" + ERROR -> "3" + in Notification (posixSecondsToUTCTime . fromRational $ appMgrNotifTime) + Nothing + appId + appVersion + (prefix <> show appMgrNotifCode) + appMgrNotifTitle + appMgrNotifMessage + +fetchAndSave :: ReaderT AgentCtx IO () +fetchAndSave = handleErr $ do + pool <- asks appConnPool + apps <- HM.toList <$> AppMgr2.runAppMgrCliC (AppMgr2.list [AppMgr2.flags| |]) + for_ apps $ \(appId, AppMgr2.InfoRes { infoResVersion }) -> do + notifs <- AppMgr.notifications appId + let mods = toModelNotif (appId, infoResVersion) <$> notifs + keys <- liftIO $ replicateM (length mods) (NotificationKey <$> nextRandom) + let ents = zipWith Entity keys mods + lift $ flip runSqlPool pool $ insertEntityMany ents + where + handleErr m = runExceptT m >>= \case + Left e -> putStrLn (errorMessage $ toError e) + Right _ -> pure () diff --git a/agent/src/Daemon/RefreshProcDev.hs b/agent/src/Daemon/RefreshProcDev.hs new file mode 100644 index 000000000..f958c1d72 --- /dev/null +++ b/agent/src/Daemon/RefreshProcDev.hs @@ -0,0 +1,20 @@ +module Daemon.RefreshProcDev where + +import Startlude + +import Data.IORef + +import Foundation +import Lib.Error +import Lib.External.Metrics.ProcDev + +refreshProcDev :: AgentCtx -> IO () +refreshProcDev agentCtx = do + let procDevCache = appProcDevMomentCache agentCtx + (oldTime, oldMoment, _) <- liftIO . readIORef . appProcDevMomentCache $ agentCtx + + eProcDev <- runS9ErrT $ getProcDevMetrics (oldTime, oldMoment) + case eProcDev of + Left e -> putStrLn @Text . show $ e + Right (newTime, newMoment, newMetrics) -> liftIO $ writeIORef procDevCache (newTime, newMoment, newMetrics) + diff --git a/agent/src/Daemon/ZeroConf.hs b/agent/src/Daemon/ZeroConf.hs new file mode 100644 index 000000000..b9a34cd7f --- /dev/null +++ b/agent/src/Daemon/ZeroConf.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE TypeApplications #-} +module Daemon.ZeroConf where + +import Startlude hiding ( ask ) + +import Control.Lens +import Control.Effect.Reader.Labelled ( ask ) +import Control.Monad.Trans.Reader ( withReaderT ) +import Crypto.Hash +import Data.ByteArray ( convert ) +import Data.ByteArray.Encoding +import qualified Data.ByteString as BS +import System.FilePath.Lens + +import Foundation +import qualified Lib.Avahi as Avahi +import Lib.ProductKey +import Lib.SystemPaths + +import Settings + +start9AgentServicePrefix :: IsString a => a +start9AgentServicePrefix = "start9-" + +getStart9AgentHostname :: (HasFilesystemBase sig m, MonadIO m, ConvertText Text a) => m a +getStart9AgentHostname = do + base <- ask @"filesystemBase" + suffix <- + liftIO + $ decodeUtf8 + . convertToBase Base16 + . BS.take 4 + . convert + . hashWith SHA256 + . encodeUtf8 + <$> getProductKey base + pure . toS $ start9AgentServicePrefix <> suffix + +getStart9AgentHostnameLocal :: (HasFilesystemBase sig m, MonadIO m) => m Text +getStart9AgentHostnameLocal = getStart9AgentHostname <&> (<> ".local") + +publishAgentToAvahi :: ReaderT AgentCtx IO () +publishAgentToAvahi = do + filesystemBase <- asks $ appFilesystemBase . appSettings + start9AgentService <- injectFilesystemBase filesystemBase getStart9AgentHostname + lift $ Avahi.createDaemonConf $ toS start9AgentService + agentPort <- asks $ appPort . appSettings + services <- lift Avahi.listServices + let serviceNames = view basename <$> services + unless (start9AgentService `elem` serviceNames) $ withReaderT appSettings $ Avahi.createService + (toS start9AgentService) + (Avahi.WildcardsEnabled, "%h") + "_http._tcp" + agentPort + lift Avahi.reload + diff --git a/agent/src/Foundation.hs b/agent/src/Foundation.hs new file mode 100644 index 000000000..0ef1fd60f --- /dev/null +++ b/agent/src/Foundation.hs @@ -0,0 +1,219 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE PartialTypeSignatures #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE ViewPatterns #-} + +module Foundation where + +import Startlude + +import qualified Control.Effect.Labelled as FE +import qualified Control.Carrier.Lift as FE +import Control.Concurrent.STM +import Control.Monad.Base +import Control.Monad.Logger ( LogSource ) +import Control.Monad.Trans.Control +import Crypto.Hash ( MD5, Digest ) +import qualified Data.HashMap.Strict as HM +import Data.IORef +import Data.Set +import Data.UUID +import Database.Persist as Persist +import Database.Persist.Sql +import Network.HTTP.Client (Manager) +import Network.HTTP.Types (status200) +import Network.Wai +import Yesod.Core +import Yesod.Core.Types +import Yesod.Auth ( AuthenticationResult(..) + , Creds(..) + , YesodAuth(..) + , YesodAuthPersist + , maybeAuth + ) +import qualified Yesod.Auth.Message as Msg +import Yesod.Form +import qualified Yesod.Core.Unsafe as Unsafe +import Yesod.Persist.Core + +import Auth +import Constants +import Lib.Algebra.State.RegistryUrl +import Lib.Background +import Lib.Error +import Lib.External.Metrics.ProcDev +import Lib.SystemPaths +import Lib.Types.Core +import Lib.Types.Emver +import Model +import Settings + + +-- | The foundation datatype for your application. This can be a good place to +-- keep settings and values requiring initialization before your application +-- starts running, such as database connections. Every handler will have +-- access to the data present here. + +data AgentCtx = AgentCtx + { appSettings :: AppSettings + , appHttpManager :: Manager + , appConnPool :: ConnectionPool -- ^ Database connection pool. + , appLogger :: Logger + , appWebServerThreadId :: IORef (Maybe ThreadId) + , appIsUpdating :: IORef (Maybe Version) + , appIsUpdateFailed :: IORef (Maybe S9Error) + , appProcDevMomentCache :: IORef (UTCTime, ProcDevMomentStats, ProcDevMetrics) + , appSelfUpdateSpecification :: MVar VersionRange + , appBackgroundJobs :: TVar JobCache + , appIconTags :: TVar (HM.HashMap AppId (Digest MD5)) + } + +setWebProcessThreadId :: ThreadId -> AgentCtx -> IO () +setWebProcessThreadId tid a = writeIORef (appWebServerThreadId a) . Just $ tid + +-- This is where we define all of the routes in our application. For a full +-- explanation of the syntax, please see: +-- http://www.yesodweb.com/book/routing-and-handlers +-- +-- Note that this is really half the story; in Application.hs, mkYesodDispatch +-- generates the rest of the code. Please see the following documentation +-- for an explanation for this split: +-- http://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_foundation_and_application_modules +-- +-- This function also generates the following type synonyms: +-- type Handler = HandlerT AgentCtx IO +mkYesodData "AgentCtx" $(parseRoutesFile "config/routes") + +noCacheUnlessSpecified :: Handler a -> Handler a +noCacheUnlessSpecified action = do + getCurrentRoute >>= \case + Nothing -> action + Just r -> if "cached" `member` routeAttrs r + then action + else addHeader "Cache-Control" "no-store" >> action +-- Please see the documentation for the Yesod typeclass. There are a number +-- of settings which can be configured by overriding methods here. +instance Yesod AgentCtx where + approot = ApprootRelative + authRoute _ = Nothing + + isAuthorized route _ | "noAuth" `member` routeAttrs route = pure Authorized + -- HACK! So that updating from 0.1.5 to 0.2.x doesn't leave you unreachable during system sync + -- in the old companion + | (fst $ renderRoute route) == ["v0"] = do + isUpdating <- fmap isJust $ getsYesod appIsUpdating >>= liftIO . readIORef + fresh <- fmap Startlude.null . runDB $ selectList ([] :: [Filter Account]) [] + if isUpdating && fresh + then sendResponseStatus status200 (object ["status" .= ("UPDATING" :: Text)]) + else requireSessionAuth + | otherwise = requireSessionAuth + +-- Yesod Middleware allows you to run code before and after each handler function. +-- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks. +-- Some users may also want to add the defaultCsrfMiddleware, which: +-- a) Sets a cookie with a CSRF token in it. +-- b) Validates that incoming write requests include that token in either a header or POST parameter. +-- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware +-- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package. + yesodMiddleware :: ToTypedContent res => Handler res -> Handler res + yesodMiddleware = defaultYesodMiddleware . cutoffDuringUpdate . noCacheUnlessSpecified + +-- What messages should be logged. The following includes all messages when +-- in development, and warnings and errors in production. + shouldLogIO :: AgentCtx -> LogSource -> LogLevel -> IO Bool + shouldLogIO app _source level = + return $ appShouldLogAll (appSettings app) || level == LevelInfo || level == LevelWarn || level == LevelError + + makeLogger :: AgentCtx -> IO Logger + makeLogger = return . appLogger + + makeSessionBackend :: AgentCtx -> IO (Maybe SessionBackend) + makeSessionBackend ctx = strictSameSiteSessions $ do + filepath <- injectFilesystemBaseFromContext settings $ getAbsoluteLocationFor sessionSigningKeyPath + fmap Just $ defaultClientSessionBackend minutes $ toS filepath + where + settings = appSettings ctx + minutes = 7 * 24 * 60 -- 7 days + +instance RenderMessage AgentCtx FormMessage where + renderMessage _ _ = defaultFormMessage +instance YesodAuth AgentCtx where + type AuthId AgentCtx = AccountId + loginDest _ = AuthenticateR + logoutDest _ = AuthenticateR + authPlugins _ = [] + + -- This gets called on login, but after HashDB's postLoginR handler is called. This validates the username and password, so creds here are legit. + authenticate creds = liftHandler $ runDB $ do + x <- getBy $ UniqueAccount $ credsIdent creds + pure $ case x of + Just (Entity uid _) -> Authenticated uid + Nothing -> UserError Msg.NoIdentifierProvided + +instance YesodAuthPersist AgentCtx + +-- How to run database actions. +instance YesodPersist AgentCtx where + type YesodPersistBackend AgentCtx = SqlBackend + runDB :: SqlPersistT Handler a -> Handler a + runDB action = runSqlPool action . appConnPool =<< getYesod + +instance YesodPersistRunner AgentCtx where + getDBRunner :: Handler (DBRunner AgentCtx, Handler ()) + getDBRunner = defaultGetDBRunner appConnPool + +unsafeHandler :: AgentCtx -> Handler a -> IO a +unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger + +appLogFunc :: AgentCtx -> LogFunc +appLogFunc = appLogger >>= flip messageLoggerSource + +cutoffDuringUpdate :: Handler a -> Handler a +cutoffDuringUpdate m = do + appIsUpdating <- getsYesod appIsUpdating >>= liftIO . readIORef + case appIsUpdating of + Just _ -> do + path <- asks $ pathInfo . reqWaiRequest . handlerRequest + case path of + [v] | v == "v" <> (show . major $ agentVersion) -> m + _ -> handleS9ErrT $ throwE UpdateInProgressE + Nothing -> m + +-- Returns authorized iff there is a valid (non-expired, signed + encrypted) session containing an account. +-- The only way for such a session to exist is if a previous login succeeded +requireSessionAuth :: Handler AuthResult +requireSessionAuth = do +#ifdef DISABLE_AUTH + pure Authorized +#else + maybeAuth >>= \case + Nothing -> pure AuthenticationRequired + Just _ -> pure Authorized +#endif + +type AgentRunner m = + RegistryUrlIOC (FE.Labelled "filesystemBase" (ReaderT Text) (FE.Labelled "httpManager" (ReaderT Manager) (FE.LiftC (ReaderT AgentCtx m)))) + +runInContext :: MonadResource m => AgentRunner m a -> ReaderT AgentCtx m a +runInContext action = do + ctx <- ask + let s = appSettings ctx + action + & runRegistryUrlIOC + & FE.runLabelled @"filesystemBase" + & flip runReaderT (appFilesystemBase s) + & FE.runLabelled @"httpManager" + & flip runReaderT (appHttpManager ctx) + & FE.runM + +instance MonadBase IO Handler where + liftBase m = HandlerFor $ const m +instance MonadBaseControl IO Handler where + type StM Handler a = a + liftBaseWith f = HandlerFor $ \handlerData -> f (($ handlerData) . unHandlerFor) + restoreM = pure diff --git a/agent/src/Handler/Apps.hs b/agent/src/Handler/Apps.hs new file mode 100644 index 000000000..4e5fc61eb --- /dev/null +++ b/agent/src/Handler/Apps.hs @@ -0,0 +1,760 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +module Handler.Apps where + +import Startlude hiding ( modify + , execState + , asks + , Reader + , runReader + , catchError + , forkFinally + , empty + ) + +import Control.Carrier.Reader +import Control.Carrier.Error.Church +import Control.Carrier.Lift +import qualified Control.Concurrent.Async.Lifted + as LAsync +import qualified Control.Concurrent.Lifted as Lifted +import qualified Control.Exception.Lifted as Lifted +import Control.Concurrent.STM.TVar +import Control.Effect.Empty hiding ( guard ) +import Control.Effect.Labelled ( HasLabelled + , Labelled + , runLabelled + ) +import Control.Lens hiding ( (??) ) +import Control.Monad.Logger +import Control.Monad.Trans.Control ( MonadBaseControl ) +import Data.Aeson +import Data.Aeson.Lens +import qualified Data.ByteString.Lazy as LBS +import Data.IORef +import qualified Data.HashMap.Lazy as HML +import qualified Data.HashMap.Strict as HM +import qualified Data.List.NonEmpty as NE +import Data.Singletons +import Data.Singletons.Prelude.Bool ( SBool(..) + , If + ) +import Data.Singletons.Prelude.List ( Elem ) + +import Database.Persist +import Database.Persist.Sql ( ConnectionPool ) +import Database.Persist.Sqlite ( runSqlPool ) +import Exinst +import Network.HTTP.Types +import Yesod.Core.Content +import Yesod.Core.Json +import Yesod.Core.Handler hiding ( cached ) +import Yesod.Core.Types ( JSONResponse(..) ) +import Yesod.Persist.Core + +import Foundation +import Handler.Backups +import Handler.Icons +import Handler.Types.Apps +import Handler.Util +import qualified Lib.Algebra.Domain.AppMgr as AppMgr2 +import Lib.Algebra.State.RegistryUrl +import Lib.Background +import Lib.Error +import qualified Lib.External.AppMgr as AppMgr +import qualified Lib.External.Registry as Reg +import Lib.IconCache +import qualified Lib.Notifications as Notifications +import Lib.SystemPaths +import Lib.TyFam.ConditionalData +import Lib.Types.Core +import Lib.Types.Emver +import Lib.Types.ServerApp +import Model +import Settings +import Crypto.Hash + +pureLog :: Show a => a -> Handler a +pureLog = liftA2 (*>) ($logInfo . show) pure + +logRet :: ToJSON a => Handler a -> Handler a +logRet = (>>= liftA2 (*>) ($logInfo . decodeUtf8 . LBS.toStrict . encode) pure) + +mkAppStatus :: HM.HashMap AppId (BackupJobType, a) -> AppId -> AppContainerStatus -> AppStatus +mkAppStatus hm appId status = case HM.lookup appId hm of + Nothing -> AppStatusAppMgr status + Just (CreateBackup , _) -> AppStatusTmp CreatingBackup + Just (RestoreBackup, _) -> AppStatusTmp RestoringBackup + + +type AllEffects m + = AppMgr2.AppMgrCliC + ( RegistryUrlIOC + ( Labelled + "iconTagCache" + (ReaderT (TVar (HM.HashMap AppId (Digest MD5)))) + ( Labelled + "filesystemBase" + (ReaderT Text) + ( Labelled + "databaseConnection" + (ReaderT ConnectionPool) + (ReaderT AgentCtx (ErrorC S9Error (LiftC m))) + ) + ) + ) + ) + +intoHandler :: AllEffects Handler x -> Handler x +intoHandler m = do + ctx <- getYesod + let fsbase = appFilesystemBase . appSettings $ ctx + runM + . handleS9ErrC + . flip runReaderT ctx + . flip runReaderT (appConnPool ctx) + . runLabelled @"databaseConnection" + . flip runReaderT fsbase + . runLabelled @"filesystemBase" + . flip runReaderT (appIconTags ctx) + . runLabelled @"iconTagCache" + . runRegistryUrlIOC + . AppMgr2.runAppMgrCliC + $ m +{-# INLINE intoHandler #-} + +-- TODO nasty. Also, note that if AppMgr.getInstalledApp fails for any app we will not return available apps res. +getAvailableAppsR :: Handler (JSONResponse [AppAvailablePreview]) +getAvailableAppsR = disableEndpointOnFailedUpdate . intoHandler $ JSONResponse <$> getAvailableAppsLogic + +getAvailableAppsLogic :: ( Has (Reader AgentCtx) sig m + , Has (Error S9Error) sig m + , Has RegistryUrl sig m + , Has AppMgr2.AppMgr sig m + , MonadIO m + , MonadBaseControl IO m + ) + => m [AppAvailablePreview] +getAvailableAppsLogic = do + jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO + let installCache = inspect SInstalling jobCache + (Reg.AppManifestRes apps, serverApps) <- LAsync.concurrently Reg.getAppManifest + (AppMgr2.list [AppMgr2.flags|-s -d|]) + let remapped = remapAppMgrInfo jobCache serverApps + pure $ foreach apps $ \app@StoreApp { storeAppId } -> + let installing = + ( (storeAppVersionInfoVersion . snd . installInfo &&& const (AppStatusTmp Installing)) + . fst + <$> HM.lookup storeAppId installCache + ) + installed = ((view _2 &&& view _1) <$> HM.lookup storeAppId remapped) + in storeAppToAvailablePreview app $ installing <|> installed + +getAvailableAppByIdR :: AppId -> Handler (JSONResponse AppAvailableFull) +getAvailableAppByIdR appId = + disableEndpointOnFailedUpdate . intoHandler $ JSONResponse <$> getAvailableAppByIdLogic appId + +getAvailableAppByIdLogic :: ( Has (Reader AgentCtx) sig m + , Has (Error S9Error) sig m + , Has RegistryUrl sig m + , Has AppMgr2.AppMgr sig m + , MonadIO m + , MonadBaseControl IO m + ) + => AppId + -> m AppAvailableFull +getAvailableAppByIdLogic appId = do + let storeAppId' = storeAppId + jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO + let installCache = inspect SInstalling jobCache + (Reg.AppManifestRes storeApps, serverApps) <- LAsync.concurrently Reg.getAppManifest + (AppMgr2.list [AppMgr2.flags|-s -d|]) + StoreApp {..} <- pure (find ((== appId) . storeAppId) storeApps) `orThrowM` NotFoundE "appId" (show appId) + let remapped = remapAppMgrInfo jobCache serverApps + let installingInfo = + ( (storeAppVersionInfoVersion . snd . installInfo &&& const (AppStatusTmp Installing)) + . fst + <$> HM.lookup appId installCache + ) + <|> ((view _2 &&& view _1) <$> HM.lookup appId remapped) + let latest = extract storeAppVersions + dependencies <- AppMgr2.checkDependencies (AppMgr2.LocalOnly False) + appId + (Just . exactly $ storeAppVersionInfoVersion latest) + enrichedDeps <- maybe (throwError (NotFoundE "dependencyId for" (show appId))) pure $ flip + HML.traverseWithKey + dependencies + \depId depInfo -> + let + base = storeAppToAppBase <$> find ((== depId) . storeAppId') storeApps + status = + (HM.lookup depId installCache $> AppStatusTmp Installing) <|> (view _1 <$> HM.lookup depId remapped) + in + (, status, depInfo) <$> base + let dependencyRequirements = fmap (dependencyInfoToDependencyRequirement (AsInstalled SFalse)) enrichedDeps + pure AppAvailableFull + { appAvailableFullBase = AppBase + appId + storeAppTitle + (storeIconUrl appId (storeAppVersionInfoVersion $ extract storeAppVersions)) + , appAvailableFullInstallInfo = installingInfo + , appAvailableFullVersionLatest = storeAppVersionInfoVersion latest + , appAvailableFullDescriptionShort = storeAppDescriptionShort + , appAvailableFullDescriptionLong = storeAppDescriptionLong + , appAvailableFullReleaseNotes = storeAppVersionInfoReleaseNotes latest + , appAvailableFullDependencyRequirements = HM.elems dependencyRequirements + , appAvailableFullVersions = storeAppVersionInfoVersion <$> storeAppVersions + } + +getAppLogsByIdR :: AppId -> Handler (JSONResponse [Text]) +getAppLogsByIdR appId = disableEndpointOnFailedUpdate $ handleS9ErrT $ do + logs <- AppMgr.getAppLogs appId + pure . JSONResponse . lines $ logs + +getInstalledAppsR :: Handler (JSONResponse [AppInstalledPreview]) +getInstalledAppsR = disableEndpointOnFailedUpdate . intoHandler $ JSONResponse <$> getInstalledAppsLogic + +cached :: MonadIO m => m a -> m (m a) +cached action = do + ref <- liftIO $ newIORef Nothing + pure $ liftIO (readIORef ref) >>= \case + Nothing -> action >>= liftA2 (*>) (liftIO . writeIORef ref . Just) pure + Just x -> pure x + +getInstalledAppsLogic :: (Has (Reader AgentCtx) sig m, Has AppMgr2.AppMgr sig m, MonadIO m) => m [AppInstalledPreview] +getInstalledAppsLogic = do + jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO + let installCache = installInfo . fst <$> inspect SInstalling jobCache + serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|] + let remapped = remapAppMgrInfo jobCache serverApps + installingPreviews = flip + HM.mapWithKey + installCache + \installingId (StoreApp {..}, StoreAppVersionInfo {..}) -> AppInstalledPreview + { appInstalledPreviewBase = AppBase installingId + storeAppTitle + (iconUrl installingId storeAppVersionInfoVersion) + , appInstalledPreviewStatus = AppStatusTmp Installing + , appInstalledPreviewVersionInstalled = storeAppVersionInfoVersion + , appInstalledPreviewTorAddress = Nothing + } + installedPreviews = flip + HML.mapWithKey + remapped + \appId (s, v, AppMgr2.InfoRes {..}) -> AppInstalledPreview + { appInstalledPreviewBase = AppBase appId infoResTitle (iconUrl appId v) + , appInstalledPreviewStatus = s + , appInstalledPreviewVersionInstalled = v + , appInstalledPreviewTorAddress = infoResTorAddress + } + + pure $ HML.elems $ HML.union installingPreviews installedPreviews + +getInstalledAppByIdR :: AppId -> Handler (JSONResponse AppInstalledFull) +getInstalledAppByIdR appId = + disableEndpointOnFailedUpdate . intoHandler $ JSONResponse <$> getInstalledAppByIdLogic appId + +getInstalledAppByIdLogic :: ( Has (Reader AgentCtx) sig m + , Has RegistryUrl sig m + , Has (Error S9Error) sig m + , Has AppMgr2.AppMgr sig m + , MonadIO m + , MonadBaseControl IO m + ) + => AppId + -> m AppInstalledFull +getInstalledAppByIdLogic appId = do + jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO + let installCache = installInfo . fst <$> inspect SInstalling jobCache + db <- asks appConnPool + backupTime' <- LAsync.async $ liftIO $ flip runSqlPool db $ getLastSuccessfulBackup appId + let installing = do + backupTime <- lift $ LAsync.wait backupTime' + hoistMaybe $ HM.lookup appId installCache <&> \(StoreApp {..}, StoreAppVersionInfo {..}) -> AppInstalledFull + { appInstalledFullBase = AppBase appId storeAppTitle (iconUrl appId storeAppVersionInfoVersion) + , appInstalledFullStatus = AppStatusTmp Installing + , appInstalledFullVersionInstalled = storeAppVersionInfoVersion + , appInstalledFullInstructions = Nothing + , appInstalledFullLastBackup = backupTime + , appInstalledFullTorAddress = Nothing + , appInstalledFullConfiguredRequirements = [] + } + serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|] + let remapped = remapAppMgrInfo jobCache serverApps + appManifestFetchCached <- cached Reg.getAppManifest + let + installed = do + (status, version, AppMgr2.InfoRes {..}) <- hoistMaybe (HM.lookup appId remapped) + instructions' <- lift $ LAsync.async $ AppMgr2.instructions appId + requirements <- LAsync.runConcurrently $ flip + HML.traverseWithKey + (HML.filter AppMgr2.dependencyInfoRequired infoResDependencies) + \depId depInfo -> LAsync.Concurrently $ do + let + fromInstalled = (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) + <$> hoistMaybe (HM.lookup depId serverApps) + let fromStore = do + Reg.AppManifestRes res <- lift appManifestFetchCached + (storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions) + <$> hoistMaybe (find ((== depId) . storeAppId) res) + (title, v) <- fromInstalled <|> fromStore + let base = AppBase depId title (iconUrl depId v) + let + depStatus = + (HM.lookup depId installCache $> AppStatusTmp Installing) + <|> (view _1 <$> HM.lookup depId remapped) + pure $ dependencyInfoToDependencyRequirement (AsInstalled STrue) (base, depStatus, depInfo) + instructions <- lift $ LAsync.wait instructions' + backupTime <- lift $ LAsync.wait backupTime' + pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version) + , appInstalledFullStatus = status + , appInstalledFullVersionInstalled = version + , appInstalledFullInstructions = instructions + , appInstalledFullLastBackup = backupTime + , appInstalledFullTorAddress = infoResTorAddress + , appInstalledFullConfiguredRequirements = HM.elems requirements + } + runMaybeT (installing <|> installed) `orThrowM` NotFoundE "appId" (show appId) + +postUninstallAppR :: AppId -> Handler (JSONResponse (WithBreakages ())) +postUninstallAppR appId = do + dry <- AppMgr2.DryRun . isJust <$> lookupGetParam "dryrun" + disableEndpointOnFailedUpdate . intoHandler $ JSONResponse <$> postUninstallAppLogic appId dry + +postUninstallAppLogic :: ( HasFilesystemBase sig m + , Has (Reader AgentCtx) sig m + , Has (Error S9Error) sig m + , Has AppMgr2.AppMgr sig m + , MonadIO m + , HasLabelled "databaseConnection" (Reader ConnectionPool) sig m + , HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m + ) + => AppId + -> AppMgr2.DryRun + -> m (WithBreakages ()) +postUninstallAppLogic appId dryrun = do + jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO + let tmpStatuses = statuses jobCache + serverApps <- AppMgr2.list [AppMgr2.flags| |] + when (not $ HM.member appId serverApps) $ throwError (AppNotInstalledE appId) + case HM.lookup appId tmpStatuses of + Just Installing -> throwError (TemporarilyForbiddenE appId "uninstall" (show Installing)) + Just CreatingBackup -> throwError (TemporarilyForbiddenE appId "uninstall" (show CreatingBackup)) + Just RestoringBackup -> throwError (TemporarilyForbiddenE appId "uninstall" (show RestoringBackup)) + _ -> pure () + let flags = if coerce dryrun then Left dryrun else Right (AppMgr2.Purge True) + breakageIds <- HM.keys . AppMgr2.unBreakageMap <$> AppMgr2.remove flags appId + bs <- pure (traverse (hydrate $ (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) <$> serverApps) breakageIds) + `orThrowM` InternalE "Reported app breakage for app that isn't installed, contact support" + when (not $ coerce dryrun) $ clearIcon appId + pure $ WithBreakages bs () + +type InstallResponse :: Bool -> Type +data InstallResponse a = InstallResponse (If a (WithBreakages ()) AppInstalledFull) +instance ToJSON (Some1 InstallResponse) where + toJSON (Some1 STrue (InstallResponse a)) = toJSON a + toJSON (Some1 SFalse (InstallResponse a)) = toJSON a +postInstallNewAppR :: AppId -> Handler (JSONResponse (Some1 InstallResponse)) +postInstallNewAppR appId = do + dryrun <- isJust <$> lookupGetParam "dryrun" + InstallNewAppReq { installNewAppVersion } <- requireCheckJsonBody + disableEndpointOnFailedUpdate . intoHandler $ JSONResponse <$> do + withSomeSing dryrun $ \sb -> Some1 sb . InstallResponse <$> postInstallNewAppLogic appId installNewAppVersion sb + +postInstallNewAppLogic :: forall sig m a + . ( Has (Reader AgentCtx) sig m + , HasLabelled "databaseConnection" (Reader ConnectionPool) sig m + , HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m + , Has (Error S9Error) sig m + , Has RegistryUrl sig m + , Has AppMgr2.AppMgr sig m + , HasFilesystemBase sig m + , MonadIO m + , MonadBaseControl IO m + ) + => AppId + -> Version + -> SBool a + -> m (If a (WithBreakages ()) AppInstalledFull) +postInstallNewAppLogic appId appVersion dryrun = do + db <- asks appConnPool + full <- (Just <$> getInstalledAppByIdLogic appId) `catchError` \case + NotFoundE "appId" appId' -> + if AppId appId' == appId then pure Nothing else throwError (NotFoundE "appId" appId') + other -> throwError other + case full of + Just aif@AppInstalledFull{} -> if appInstalledFullVersionInstalled aif == appVersion + then pure $ case dryrun of + STrue -> WithBreakages [] () + SFalse -> aif + else installIt db True + Nothing -> installIt db False + where + installIt :: ConnectionPool -> Bool -> m (If a (WithBreakages ()) AppInstalledFull) + installIt db isUpdate = do + jobCacheTVar <- asks appBackgroundJobs + store@StoreApp {..} <- Reg.getStoreAppInfo appId `orThrowM` NotFoundE "appId" (show appId) + vinfo@StoreAppVersionInfo{} <- + find ((== appVersion) . storeAppVersionInfoVersion) storeAppVersions + `orThrowPure` NotFoundE "version" (show appVersion) + -- if it is a dry run of an update we don't want to modify the cache + case dryrun of + STrue -> if not isUpdate + then pure $ WithBreakages [] () + else do + serverApps' <- LAsync.async $ AppMgr2.list [AppMgr2.flags| |] + hm <- AppMgr2.update (AppMgr2.DryRun True) appId (Just $ exactly appVersion) + (serverApps :: HM.HashMap AppId (AppMgr2.InfoRes ( 'Right '[]))) <- LAsync.wait serverApps' + breakages <- + traverse (hydrate ((AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) <$> serverApps)) + (HM.keys $ AppMgr2.unBreakageMap hm) + `orThrowPure` InternalE + "Breakage reported for app that isn't installed, contact support" + pure $ WithBreakages breakages () + SFalse -> do + let + action = do + iconAction <- LAsync.async $ saveIcon (toS storeAppIconUrl) + let install = if isUpdate + then void $ AppMgr2.update (AppMgr2.DryRun False) appId (Just $ exactly appVersion) + else AppMgr2.install (AppMgr2.NoCache True) appId (Just $ exactly appVersion) + let + success = liftIO $ void $ flip runSqlPool db $ Notifications.emit + appId + appVersion + Notifications.InstallSuccess + let failure e = liftIO $ do + let notif = case e of + AppMgrE _ ec -> Notifications.InstallFailedAppMgrExitCode ec + _ -> Notifications.InstallFailedS9Error e + void $ flip runSqlPool db $ Notifications.emit appId appVersion notif + putStrLn @Text (show e) + let todo = do + install + () <- LAsync.wait iconAction + success + todo `catchError` failure + tid <- action `Lifted.forkFinally` const postInstall + liftIO $ atomically $ modifyTVar' jobCacheTVar (insertJob appId (Install store vinfo) tid) + getInstalledAppByIdLogic appId + postInstall :: m () + postInstall = do + jobCache <- asks appBackgroundJobs + pool <- asks appConnPool + liftIO . atomically $ modifyTVar jobCache (deleteJob appId) + ls <- AppMgr2.list [AppMgr2.flags| |] + LAsync.forConcurrently_ (HM.toList ls) $ \(k, AppMgr2.InfoRes {..}) -> when + infoResNeedsRestart + ( postRestartServerAppLogic k + `catchError` \e -> liftIO $ runSqlPool + (void $ Notifications.emit k infoResVersion (Notifications.RestartFailed e)) + pool + ) + + +postStartServerAppR :: AppId -> Handler () +postStartServerAppR appId = disableEndpointOnFailedUpdate . intoHandler $ postStartServerAppLogic appId + +postStartServerAppLogic :: (Has (Error S9Error) sig m, Has AppMgr2.AppMgr sig m, Has (Reader AgentCtx) sig m, MonadIO m) + => AppId + -> m () +postStartServerAppLogic appId = do + jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO + info <- AppMgr2.info [AppMgr2.flags|-s -d|] appId `orThrowM` AppNotInstalledE appId + (status, _, _) <- (HM.lookup appId $ remapAppMgrInfo jobCache (HM.singleton appId info)) + `orThrowPure` InternalE "Remapping magically deleted keys between source and target structures" + case status of + AppStatusAppMgr Stopped -> AppMgr2.start appId + other -> throwError $ AppStateActionIncompatibleE appId other Start + +postRestartServerAppR :: AppId -> Handler () +postRestartServerAppR appId = disableEndpointOnFailedUpdate . intoHandler $ postRestartServerAppLogic appId + +postRestartServerAppLogic :: ( Has (Reader AgentCtx) sig m + , Has AppMgr2.AppMgr sig m + , Has (Error S9Error) sig m + , MonadBaseControl IO m + , MonadIO m + ) + => AppId + -> m () +postRestartServerAppLogic appId = do + jobCache <- asks appBackgroundJobs + answer <- Lifted.newEmptyMVar + void . Lifted.fork $ do + tid <- Lifted.myThreadId + problem <- liftIO . atomically $ do + JobCache jobs <- readTVar jobCache + case HM.lookup appId jobs of + Just (Some1 s _, _) -> pure (Just . throwError $ TemporarilyForbiddenE appId "restart" (show s)) + Nothing -> do + modifyTVar jobCache (insertJob appId RestartApp tid) + pure Nothing + case problem of + Nothing -> do + AppMgr2.restart appId `Lifted.finally` (liftIO . atomically) (modifyTVar jobCache (deleteJob appId)) + Lifted.putMVar answer Nothing + Just p -> Lifted.putMVar answer (Just p) + Lifted.takeMVar answer >>= \case + Nothing -> pure () + Just p -> p + + +postStopServerAppR :: AppId -> Handler (JSONResponse (WithBreakages ())) +postStopServerAppR appId = disableEndpointOnFailedUpdate do + dryrun <- isJust <$> lookupGetParam "dryrun" + mRes <- intoHandler $ runMaybeT (JSONResponse <$> postStopServerAppLogic appId (AppMgr2.DryRun dryrun)) + case mRes of + Nothing -> sendResponseStatus status200 () + Just x -> pure x + +postStopServerAppLogic :: ( Has Empty sig m + , Has (Reader AgentCtx) sig m + , Has (Error S9Error) sig m + , Has AppMgr2.AppMgr sig m + , MonadIO m + , MonadBaseControl IO m + ) + => AppId + -> AppMgr2.DryRun + -> m (WithBreakages ()) +postStopServerAppLogic appId dryrun = do + jobCache <- asks appBackgroundJobs + titles <- (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) <<$>> AppMgr2.list [AppMgr2.flags| |] + let stopIt = do + breakages <- AppMgr2.stop dryrun appId + bases <- traverse (hydrate titles) (HM.keys $ AppMgr2.unBreakageMap breakages) + `orThrowPure` InternalE "Breakages reported for app that isn't installed, contact support" + pure $ WithBreakages bases () + status <- AppMgr2.infoResStatus <<$>> AppMgr2.info [AppMgr2.flags|-S|] appId + case (dryrun, status) of + (_ , Nothing ) -> throwError $ NotFoundE "appId" (show appId) + (AppMgr2.DryRun False, Just Running) -> do + tid <- (void stopIt) + `Lifted.forkFinally` const ((liftIO . atomically) (modifyTVar jobCache (deleteJob appId))) + liftIO . atomically $ modifyTVar jobCache (insertJob appId StopApp tid) + empty + (AppMgr2.DryRun True , Just Running ) -> stopIt + (AppMgr2.DryRun False, Just Restarting) -> do + tid <- (void stopIt) + `Lifted.forkFinally` const ((liftIO . atomically) (modifyTVar jobCache (deleteJob appId))) + liftIO . atomically $ modifyTVar jobCache (insertJob appId StopApp tid) + empty + (AppMgr2.DryRun True, Just Restarting) -> stopIt + (_, Just other) -> throwError $ AppStateActionIncompatibleE appId (AppStatusAppMgr other) Stop + +getAppConfigR :: AppId -> Handler TypedContent +getAppConfigR = + disableEndpointOnFailedUpdate + . handleS9ErrT + . fmap (TypedContent typeJson . toContent) + . AppMgr.getConfigurationAndSpec + +patchAppConfigR :: AppId -> Handler (JSONResponse (WithBreakages ())) +patchAppConfigR appId = disableEndpointOnFailedUpdate $ do + dryrun <- isJust <$> lookupGetParam "dryrun" + value <- requireCheckJsonBody @_ @Value + realVal <- + runM . handleS9ErrC $ ((value ^? key "config") `orThrowPure` (InvalidRequestE value "Missing 'config' key")) + intoHandler $ JSONResponse <$> patchAppConfigLogic appId (AppMgr2.DryRun dryrun) realVal + +patchAppConfigLogic :: ( Has (Reader AgentCtx) sig m + , Has (Error S9Error) sig m + , Has AppMgr2.AppMgr sig m + , MonadBaseControl IO m + , MonadIO m + ) + => AppId + -> AppMgr2.DryRun + -> Value + -> m (WithBreakages ()) +patchAppConfigLogic appId dryrun cfg = do + serverApps <- AppMgr2.list [AppMgr2.flags| |] + AppMgr2.ConfigureRes {..} <- AppMgr2.configure dryrun appId (Just cfg) + when (not $ coerce dryrun) $ for_ configureResNeedsRestart postRestartServerAppLogic + breakages <- + traverse (hydrate ((AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) <$> serverApps)) + (HM.keys configureResStopped) + `orThrowPure` InternalE "Breakage reported for app that is not installed, contact support" + pure $ WithBreakages breakages () + + +getAppNotificationsR :: AppId -> Handler (JSONResponse [Entity Notification]) +getAppNotificationsR appId = disableEndpointOnFailedUpdate $ runDB $ do + page <- lookupGetParam "page" `orDefaultTo` 1 + pageSize <- lookupGetParam "perPage" `orDefaultTo` 20 + evs <- selectList [NotificationAppId ==. appId] + [Desc NotificationCreatedAt, LimitTo pageSize, OffsetBy ((page - 1) * pageSize)] + let toArchive = fmap entityKey $ filter ((== Nothing) . notificationArchivedAt . entityVal) evs + void $ Notifications.archive toArchive + pure $ JSONResponse evs + where + orDefaultTo :: (Monad m, Read a) => m (Maybe Text) -> a -> m a + orDefaultTo m a = do + m' <- m + case m' >>= readMaybe . toS of + Nothing -> pure a + Just x -> pure x + +getAppMetricsR :: AppId -> Handler TypedContent +getAppMetricsR appId = + disableEndpointOnFailedUpdate . handleS9ErrT $ fmap (TypedContent typeJson . toContent) $ AppMgr.stats appId + +getAvailableAppVersionInfoR :: AppId -> VersionRange -> Handler (JSONResponse AppVersionInfo) +getAvailableAppVersionInfoR appId version = + disableEndpointOnFailedUpdate . intoHandler $ JSONResponse <$> getAvailableAppVersionInfoLogic appId version + +getAvailableAppVersionInfoLogic :: ( Has (Reader AgentCtx) sig m + , Has (Error S9Error) sig m + , Has RegistryUrl sig m + , Has AppMgr2.AppMgr sig m + , MonadIO m + , MonadBaseControl IO m + ) + => AppId + -> VersionRange + -> m AppVersionInfo +getAvailableAppVersionInfoLogic appId appVersionSpec = do + jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO + Reg.AppManifestRes storeApps <- Reg.getAppManifest + let titles = + (storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions) <$> indexBy storeAppId storeApps + StoreApp {..} <- find ((== appId) . storeAppId) storeApps `orThrowPure` NotFoundE "appId" (show appId) + serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|] + let remapped = remapAppMgrInfo jobCache serverApps + StoreAppVersionInfo {..} <- + maximumMay (NE.filter ((<|| appVersionSpec) . storeAppVersionInfoVersion) storeAppVersions) + `orThrowPure` NotFoundE "version spec " (show appVersionSpec) + dependencies <- AppMgr2.checkDependencies (AppMgr2.LocalOnly False) + appId + (Just $ exactly storeAppVersionInfoVersion) + requirements <- flip HML.traverseWithKey dependencies $ \depId depInfo -> do + base <- hydrate titles depId `orThrowPure` NotFoundE "metadata for" (show depId) + let status = + (HM.lookup depId (inspect SInstalling jobCache) $> AppStatusTmp Installing) + <|> (view _1 <$> HM.lookup depId remapped) + pure $ dependencyInfoToDependencyRequirement (AsInstalled SFalse) (base, status, depInfo) + pure AppVersionInfo { appVersionInfoVersion = storeAppVersionInfoVersion + , appVersionInfoReleaseNotes = storeAppVersionInfoReleaseNotes + , appVersionInfoDependencyRequirements = HM.elems requirements + } + +postAutoconfigureR :: AppId -> AppId -> Handler (JSONResponse (WithBreakages AutoconfigureChangesRes)) +postAutoconfigureR dependency dependent = do + dry <- AppMgr2.DryRun . isJust <$> lookupGetParam "dryrun" + disableEndpointOnFailedUpdate . intoHandler $ JSONResponse <$> postAutoconfigureLogic dependency dependent dry + +postAutoconfigureLogic :: ( Has (Reader AgentCtx) sig m + , Has AppMgr2.AppMgr sig m + , Has (Error S9Error) sig m + , MonadBaseControl IO m + , MonadIO m + ) + => AppId + -> AppId + -> AppMgr2.DryRun + -> m (WithBreakages AutoconfigureChangesRes) +postAutoconfigureLogic dependency dependent dry = do + -- IMPORTANT! AppMgr reverses arguments from the endpoint + appData <- AppMgr2.list [AppMgr2.flags| |] + let apps = HM.keys appData + case (dependency `elem` apps, dependent `elem` apps) of + (False, _ ) -> throwError $ NotFoundE "appId" (show dependency) + (_ , False) -> throwError $ NotFoundE "appId" (show dependent) + _ -> pure () + AppMgr2.AutoconfigureRes {..} <- AppMgr2.autoconfigure dry dependent dependency + when (not $ coerce dry) $ for_ (AppMgr2.configureResNeedsRestart autoconfigureConfigRes) postRestartServerAppLogic + let titles = (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) <$> appData + bases <- traverse (hydrate titles) (HM.keys (AppMgr2.configureResStopped autoconfigureConfigRes)) + `orThrowPure` InternalE "Breakages reported for app that isn't installed, contact support" + pure $ WithBreakages bases (AutoconfigureChangesRes $ HM.lookup dependency autoconfigureChanged) + +indexBy :: (Eq k, Hashable k) => (v -> k) -> [v] -> HM.HashMap k v +indexBy = flip foldr HM.empty . (>>= HM.insertWith const) +{-# INLINE indexBy #-} + +hydrate :: HM.HashMap AppId (Text, Version) -> AppId -> Maybe AppBase +hydrate titles appId = HM.lookup appId titles <&> \(t, v) -> AppBase appId t (iconUrl appId v) + +remapAppMgrInfo :: (Elem 'AppMgr2.IncludeDependencies ls ~ 'True, Elem 'AppMgr2.IncludeStatus ls ~ 'True) + => JobCache + -> HM.HashMap AppId (AppMgr2.InfoRes ( 'Right ls)) -- ^ AppMgr response + -> HM.HashMap AppId (AppStatus, Version, AppMgr2.InfoRes ( 'Right ls)) +remapAppMgrInfo jobCache serverApps = flip + HML.mapWithKey + serverApps + \appId infoRes@AppMgr2.InfoRes {..} -> + let refinedDepInfo = flip + HML.mapWithKey + infoResDependencies + \depId depInfo -> + case + ( HM.lookup depId tmpStatuses + , AppMgr2.infoResStatus <$> HM.lookup depId serverApps + , AppMgr2.dependencyInfoError depInfo + ) + of + -- mute all of the not-running violations that are currently backing up and container is paused + (Just CreatingBackup, Just Paused, Just AppMgr2.NotRunning) -> + depInfo { AppMgr2.dependencyInfoError = Nothing } + (_, _, _) -> depInfo + realViolations = + any (isJust . AppMgr2.dependencyInfoError <&&> AppMgr2.dependencyInfoRequired) refinedDepInfo + (status, version) = + maybe (AppStatusAppMgr infoResStatus, infoResVersion) (first AppStatusTmp) + $ ((, infoResVersion) <$> HM.lookup appId tmpStatuses) + <|> (guard (not infoResIsConfigured || infoResIsRecoverable) $> (NeedsConfig, infoResVersion)) + <|> (guard realViolations $> (BrokenDependencies, infoResVersion)) + <|> (guard (infoResStatus == Restarting) $> (Crashed, infoResVersion)) + in ( status + , version + , infoRes + { AppMgr2.infoResDependencies = case status of + AppStatusTmp NeedsConfig -> HM.empty + _ -> refinedDepInfo + } + ) + where tmpStatuses = statuses jobCache + +storeAppToAppBase :: StoreApp -> AppBase +storeAppToAppBase StoreApp {..} = + AppBase storeAppId storeAppTitle (storeIconUrl storeAppId (storeAppVersionInfoVersion $ extract storeAppVersions)) + +storeAppToAvailablePreview :: StoreApp -> Maybe (Version, AppStatus) -> AppAvailablePreview +storeAppToAvailablePreview s@StoreApp {..} installed = AppAvailablePreview + (storeAppToAppBase s) + (storeAppVersionInfoVersion $ extract storeAppVersions) + storeAppDescriptionShort + installed + +type AsInstalled :: Bool -> Type +newtype AsInstalled a = AsInstalled { unAsInstalled :: SBool a } +dependencyInfoToDependencyRequirement :: AsInstalled a + -> (AppBase, Maybe AppStatus, AppMgr2.DependencyInfo) + -> (AppDependencyRequirement (If a Strip Keep)) +dependencyInfoToDependencyRequirement asInstalled (base, status, AppMgr2.DependencyInfo {..}) = do + let appDependencyRequirementBase = base + let appDependencyRequirementDescription = dependencyInfoDescription + let appDependencyRequirementVersionSpec = dependencyInfoVersionSpec + let appDependencyRequirementViolation = case (status, dependencyInfoError) of + (Just s@(AppStatusTmp Installing), _) -> Just $ IncompatibleStatus s + (Nothing, _ ) -> Just Missing + (_ , Just AppMgr2.NotInstalled) -> Just Missing + (_, Just (AppMgr2.InvalidVersion _ _)) -> Just IncompatibleVersion + (_, Just (AppMgr2.UnsatisfiedConfig reasons)) -> Just . IncompatibleConfig $ reasons + (Just s , Just AppMgr2.NotRunning ) -> Just $ IncompatibleStatus s + (_ , Nothing ) -> Nothing + case asInstalled of + AsInstalled STrue -> + let appDependencyRequirementReasonOptional = () + appDependencyRequirementDefault = () + in AppDependencyRequirement { .. } + AsInstalled SFalse -> + let appDependencyRequirementReasonOptional = dependencyInfoReasonOptional + appDependencyRequirementDefault = dependencyInfoRequired + in AppDependencyRequirement { .. } diff --git a/agent/src/Handler/Authenticate.hs b/agent/src/Handler/Authenticate.hs new file mode 100644 index 000000000..2e6476971 --- /dev/null +++ b/agent/src/Handler/Authenticate.hs @@ -0,0 +1,9 @@ +module Handler.Authenticate where + +import Startlude + +import Foundation + +-- handled by auth switch in Foundation +getAuthenticateR :: Handler () +getAuthenticateR = pure () diff --git a/agent/src/Handler/Backups.hs b/agent/src/Handler/Backups.hs new file mode 100644 index 000000000..128badf1d --- /dev/null +++ b/agent/src/Handler/Backups.hs @@ -0,0 +1,218 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +module Handler.Backups where + +import Startlude hiding ( Reader + , ask + , runReader + ) + +import Control.Effect.Labelled hiding ( Handler ) +import Control.Effect.Reader.Labelled +import Control.Carrier.Error.Church +import Control.Carrier.Lift +import Control.Carrier.Reader ( runReader ) +import Data.Aeson +import qualified Data.HashMap.Strict as HM +import Data.UUID.V4 +import Database.Persist.Sql +import Yesod.Auth +import Yesod.Core +import Yesod.Core.Types + +import Foundation +import Handler.Util +import Lib.Error +import qualified Lib.External.AppMgr as AppMgr +import qualified Lib.Notifications as Notifications +import Lib.Password +import Lib.Types.Core +import Lib.Types.Emver +import Model +import qualified Lib.Algebra.Domain.AppMgr as AppMgr2 +import Lib.Background +import Control.Concurrent.STM +import Exinst + + +data CreateBackupReq = CreateBackupReq + { createBackupLogicalName :: FilePath + , createBackupPassword :: Maybe Text + } + deriving (Eq, Show) +instance FromJSON CreateBackupReq where + parseJSON = withObject "Create Backup Req" $ \o -> do + createBackupLogicalName <- o .: "logicalname" + createBackupPassword <- o .:? "password" .!= Nothing + pure CreateBackupReq { .. } + +data RestoreBackupReq = RestoreBackupReq + { restoreBackupLogicalName :: FilePath + , restoreBackupPassword :: Maybe Text + } + deriving (Eq, Show) +instance FromJSON RestoreBackupReq where + parseJSON = withObject "Restore Backup Req" $ \o -> do + restoreBackupLogicalName <- o .: "logicalname" + restoreBackupPassword <- o .:? "password" .!= Nothing + pure RestoreBackupReq { .. } + +-- Handlers + +postCreateBackupR :: AppId -> Handler () +postCreateBackupR appId = disableEndpointOnFailedUpdate $ do + req <- requireCheckJsonBody + AgentCtx {..} <- getYesod + account <- entityVal <$> requireAuth + case validatePass account <$> (createBackupPassword req) of + Just False -> runM . handleS9ErrC $ throwError BackupPassInvalidE + _ -> + createBackupLogic appId req + & AppMgr2.runAppMgrCliC + & runLabelled @"databaseConnection" + & runReader appConnPool + & runLabelled @"backgroundJobCache" + & runReader appBackgroundJobs + & handleS9ErrC + & runM + + +postStopBackupR :: AppId -> Handler () +postStopBackupR appId = disableEndpointOnFailedUpdate $ do + cache <- getsYesod appBackgroundJobs + stopBackupLogic appId & runLabelled @"backgroundJobCache" & runReader cache & handleS9ErrC & runM + +postRestoreBackupR :: AppId -> Handler () +postRestoreBackupR appId = disableEndpointOnFailedUpdate $ do + req <- requireCheckJsonBody + AgentCtx {..} <- getYesod + restoreBackupLogic appId req + & AppMgr2.runAppMgrCliC + & runLabelled @"databaseConnection" + & runReader appConnPool + & runLabelled @"backgroundJobCache" + & runReader appBackgroundJobs + & handleS9ErrC + & runM + +getListDisksR :: Handler (JSONResponse [AppMgr.DiskInfo]) +getListDisksR = fmap JSONResponse . runM . handleS9ErrC $ listDisksLogic + + +-- Logic + +createBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)) sig m + , HasLabelled "databaseConnection" (Reader ConnectionPool) sig m + , Has (Error S9Error) sig m + , Has AppMgr2.AppMgr sig m + , MonadIO m + ) + => AppId + -> CreateBackupReq + -> m () +createBackupLogic appId CreateBackupReq {..} = do + jobCache <- ask @"backgroundJobCache" + db <- ask @"databaseConnection" + version <- fmap AppMgr2.infoResVersion $ AppMgr2.info [AppMgr2.flags| |] appId `orThrowM` NotFoundE "appId" + (show appId) + res <- liftIO . atomically $ do + (JobCache jobs) <- readTVar jobCache + case HM.lookup appId jobs of + Just (Some1 SCreatingBackup _, _) -> pure (Left $ BackupE appId "Already creating backup") + Just (Some1 SRestoringBackup _, _) -> pure (Left $ BackupE appId "Cannot backup during restore") + Just (Some1 _ _, _) -> pure (Left $ BackupE appId "Cannot backup: incompatible status") + Nothing -> do + -- this panic is here because we don't have the threadID yet, and it is required. We want to write the + -- TVar anyway though so that we don't accidentally launch multiple backup jobs + -- TODO: consider switching to MVar's for this + modifyTVar jobCache (insertJob appId Backup $ panic "ThreadID prematurely forced") + pure $ Right () + case res of + Left e -> throwError e + Right () -> do + tid <- liftIO . forkIO $ do + appmgrRes <- runExceptT (AppMgr.backupCreate createBackupPassword appId createBackupLogicalName) + atomically $ modifyTVar' jobCache (deleteJob appId) + let notif = case appmgrRes of + Left e -> Notifications.BackupFailed e + Right _ -> Notifications.BackupSucceeded + flip runSqlPool db $ do + void $ insertBackupResult appId version (isRight appmgrRes) + void $ Notifications.emit appId version notif + liftIO . atomically $ modifyTVar jobCache (insertJob appId Backup tid) + +stopBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)) sig m + , Has (Error S9Error) sig m + , MonadIO m + ) + => AppId + -> m () +stopBackupLogic appId = do + jobCache <- ask @"backgroundJobCache" + res <- liftIO . atomically $ do + (JobCache jobs) <- readTVar jobCache + case HM.lookup appId jobs of + Just (Some1 SCreatingBackup _, tid) -> do + modifyTVar jobCache (deleteJob appId) + pure (Right tid) + Just (Some1 SRestoringBackup _, _) -> pure (Left $ BackupE appId "Cannot interrupt restore") + _ -> pure (Left $ NotFoundE "backup job" (show appId)) + case res of + Left e -> throwError e + Right tid -> liftIO $ killThread tid + +restoreBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)) sig m + , HasLabelled "databaseConnection" (Reader ConnectionPool) sig m + , Has (Error S9Error) sig m + , Has AppMgr2.AppMgr sig m + , MonadIO m + ) + => AppId + -> RestoreBackupReq + -> m () +restoreBackupLogic appId RestoreBackupReq {..} = do + jobCache <- ask @"backgroundJobCache" + db <- ask @"databaseConnection" + version <- fmap AppMgr2.infoResVersion $ AppMgr2.info [AppMgr2.flags| |] appId `orThrowM` NotFoundE "appId" + (show appId) + res <- liftIO . atomically $ do + (JobCache jobs) <- readTVar jobCache + case HM.lookup appId jobs of + Just (Some1 SCreatingBackup _, _) -> pure (Left $ BackupE appId "Cannot restore during backup") + Just (Some1 SRestoringBackup _, _) -> pure (Left $ BackupE appId "Already restoring backup") + Just (Some1 _ _, _) -> pure (Left $ BackupE appId "Cannot backup: incompatible status") + Nothing -> do + -- this panic is here because we don't have the threadID yet, and it is required. We want to write the + -- TVar anyway though so that we don't accidentally launch multiple backup jobs + -- TODO: consider switching to MVar's for this + modifyTVar jobCache (insertJob appId Restore $ panic "ThreadID prematurely forced") + pure $ Right () + case res of + Left e -> throwError e + Right _ -> do + tid <- liftIO . forkIO $ do + appmgrRes <- runExceptT (AppMgr.backupRestore restoreBackupPassword appId restoreBackupLogicalName) + atomically $ modifyTVar jobCache (deleteJob appId) + let notif = case appmgrRes of + Left e -> Notifications.RestoreFailed e + Right _ -> Notifications.RestoreSucceeded + flip runSqlPool db $ void $ Notifications.emit appId version notif + liftIO . atomically $ modifyTVar jobCache (insertJob appId Restore tid) + + +listDisksLogic :: (Has (Error S9Error) sig m, MonadIO m) => m [AppMgr.DiskInfo] +listDisksLogic = runExceptT AppMgr.diskShow >>= liftEither + +insertBackupResult :: MonadIO m => AppId -> Version -> Bool -> SqlPersistT m (Entity BackupRecord) +insertBackupResult appId appVersion succeeded = do + uuid <- liftIO nextRandom + now <- liftIO getCurrentTime + let k = (BackupRecordKey uuid) + let v = (BackupRecord now appId appVersion succeeded) + insertKey k v + pure $ Entity k v + +getLastSuccessfulBackup :: MonadIO m => AppId -> SqlPersistT m (Maybe UTCTime) +getLastSuccessfulBackup appId = backupRecordCreatedAt . entityVal <<$>> selectFirst + [BackupRecordAppId ==. appId, BackupRecordSucceeded ==. True] + [Desc BackupRecordCreatedAt] diff --git a/agent/src/Handler/Hosts.hs b/agent/src/Handler/Hosts.hs new file mode 100644 index 000000000..d3a6e2955 --- /dev/null +++ b/agent/src/Handler/Hosts.hs @@ -0,0 +1,85 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} +module Handler.Hosts where + +import Startlude hiding ( ask ) + +import Control.Carrier.Lift ( runM ) +import Control.Carrier.Error.Church +import Data.Conduit +import qualified Data.Conduit.Binary as CB +import Data.Time.ISO8601 +import Yesod.Core hiding ( expiresAt ) + +import Foundation +import Daemon.ZeroConf +import Handler.Register ( produceProofOfKey + , checkExistingPasswordRegistration + ) +import Handler.Types.Hosts +import Handler.Types.Register +import Lib.Crypto +import Lib.Error +import Lib.Password ( rootAccountName ) +import Lib.ProductKey +import Lib.Ssl +import Lib.SystemPaths +import Lib.Tor +import Settings + +getHostsR :: Handler HostsRes +getHostsR = handleS9ErrT $ do + settings <- getsYesod appSettings + productKey <- liftIO . getProductKey . appFilesystemBase $ settings + hostParams <- extractHostsQueryParams + + verifyHmac productKey hostParams + verifyTimestampNotExpired $ hostsParamsExpiration hostParams + + mClaimedAt <- checkExistingPasswordRegistration rootAccountName + case mClaimedAt of + Nothing -> pure $ NullReply + Just claimedAt -> do + fmap HostsRes . mapExceptT (liftIO . runM . injectFilesystemBaseFromContext settings) $ getRegistration + productKey + claimedAt + +verifyHmac :: MonadIO m => Text -> HostsParams -> S9ErrT m () +verifyHmac productKey params = do + let computedHmacDigest = computeHmac productKey hostsParamsExpiration hostsParamsSalt + unless (hostsParamsHmac == computedHmacDigest) $ throwE unauthorizedHmac + where + HostsParams { hostsParamsHmac, hostsParamsExpiration, hostsParamsSalt } = params + unauthorizedHmac = ClientCryptographyE "Unauthorized hmac" + +verifyTimestampNotExpired :: MonadIO m => Text -> S9ErrT m () +verifyTimestampNotExpired expirationTimestamp = do + now <- liftIO getCurrentTime + case parseISO8601 . toS $ expirationTimestamp of + Nothing -> throwE $ TTLExpirationE "invalid timestamp" + Just expiration -> when (expiration < now) (throwE $ TTLExpirationE "expired") + +getRegistration :: (MonadIO m, HasFilesystemBase sig m, Has (Error S9Error) sig m) => Text -> UTCTime -> m RegisterRes +getRegistration productKey registerResClaimedAt = do + torAddress <- getAgentHiddenServiceUrlMaybe >>= \case + Nothing -> throwError $ NotFoundE "prior registration" "torAddress" + Just t -> pure $ t + caCert <- readSystemPath rootCaCertPath >>= \case + Nothing -> throwError $ NotFoundE "prior registration" "cert" + Just t -> pure t + + -- create an hmac of the torAddress + caCert for front end + registerResTorAddressSig <- produceProofOfKey productKey torAddress + registerResCertSig <- produceProofOfKey productKey caCert + + let registerResCertName = root_CA_CERT_NAME + registerResLanAddress <- getStart9AgentHostnameLocal + + pure RegisterRes { .. } + +getCertificateR :: Handler TypedContent +getCertificateR = do + base <- getsYesod $ appFilesystemBase . appSettings + respondSource "application/x-x509-ca-cert" + $ CB.sourceFile (toS $ rootCaCertPath `relativeTo` base) + .| awaitForever sendChunkBS diff --git a/agent/src/Handler/Icons.hs b/agent/src/Handler/Icons.hs new file mode 100644 index 000000000..4eed8ca33 --- /dev/null +++ b/agent/src/Handler/Icons.hs @@ -0,0 +1,106 @@ +{-# LANGUAGE PartialTypeSignatures #-} +module Handler.Icons where + +import Startlude hiding ( Reader + , runReader + ) + +import Control.Carrier.Error.Either +import Control.Carrier.Lift +import Data.Conduit +import Data.Conduit.Binary as CB +import qualified Data.Text as T +import Network.HTTP.Simple +import System.FilePath.Posix +import Yesod.Core + +import Foundation +import Lib.Algebra.State.RegistryUrl +import Lib.Error +import qualified Lib.External.Registry as Reg +import Lib.IconCache +import Lib.SystemPaths hiding ( () ) +import Lib.Types.Core +import Lib.Types.ServerApp +import Settings +import Control.Carrier.Reader hiding ( asks ) +import Control.Effect.Labelled ( runLabelled ) +import qualified Data.HashMap.Strict as HM +import Control.Concurrent.STM ( modifyTVar + , readTVarIO + ) +import Crypto.Hash.Conduit ( hashFile ) +import Lib.Types.Emver + +iconUrl :: AppId -> Version -> Text +iconUrl appId version = (foldMap (T.cons '/') . fst . renderRoute . AppIconR $ appId) <> "?" <> show version + +storeIconUrl :: AppId -> Version -> Text +storeIconUrl appId version = + (foldMap (T.cons '/') . fst . renderRoute . AvailableAppIconR $ appId) <> "?" <> show version + +getAppIconR :: AppId -> Handler TypedContent +getAppIconR appId = handleS9ErrT $ do + ctx <- getYesod + let iconTags = appIconTags ctx + storedTag <- liftIO $ readTVarIO iconTags >>= pure . HM.lookup appId + path <- case storedTag of + Nothing -> interp ctx $ do + findIcon appId >>= \case + Nothing -> fetchIcon + Just fp -> do + tag <- hashFile fp + saveTag appId tag + pure fp + Just x -> do + setWeakEtag (show x) + interp ctx $ findIcon appId >>= \case + Nothing -> do + liftIO $ atomically $ modifyTVar iconTags (HM.delete appId) + fetchIcon + Just fp -> pure fp + cacheSeconds 86_400 + lift $ respondSource (parseContentType path) $ CB.sourceFile path .| awaitForever sendChunkBS + where + fetchIcon = do + url <- find ((== appId) . storeAppId) . Reg.storeApps <$> Reg.getAppManifest >>= \case + Nothing -> throwError $ NotFoundE "icon" (show appId) + Just x -> pure . toS $ storeAppIconUrl x + bp <- getAbsoluteLocationFor iconBasePath + saveIcon url + pure (toS bp takeFileName url) + interp ctx = + mapExceptT (liftIO . runM) + . runReader (appConnPool ctx) + . runLabelled @"databaseConnection" + . runReader (appFilesystemBase $ appSettings ctx) + . runLabelled @"filesystemBase" + . runReader (appIconTags ctx) + . runLabelled @"iconTagCache" + . runRegistryUrlIOC + + +getAvailableAppIconR :: AppId -> Handler TypedContent +getAvailableAppIconR appId = handleS9ErrT $ do + s <- getsYesod appSettings + url <- do + find ((== appId) . storeAppId) . Reg.storeApps <$> interp s Reg.getAppManifest >>= \case + Nothing -> throwE $ NotFoundE "icon" (show appId) + Just x -> pure . toS $ storeAppIconUrl x + req <- case parseRequest url of + Nothing -> throwE $ RegistryParseE (toS url) "invalid url" + Just x -> pure x + cacheSeconds 86_400 + lift $ respondSource (parseContentType url) $ httpSource req getResponseBody .| awaitForever sendChunkBS + where interp s = ExceptT . liftIO . runError . injectFilesystemBaseFromContext s . runRegistryUrlIOC + +parseContentType :: FilePath -> ContentType +parseContentType = contentTypeMapping . takeExtension + where + contentTypeMapping ext = case ext of + ".png" -> typePng + ".jpeg" -> typeJpeg + ".jpg" -> typeJpeg + ".gif" -> typeGif + ".svg" -> typeSvg + _ -> typePlain diff --git a/agent/src/Handler/Login.hs b/agent/src/Handler/Login.hs new file mode 100644 index 000000000..d4241fd9b --- /dev/null +++ b/agent/src/Handler/Login.hs @@ -0,0 +1,75 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE ConstraintKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Handler.Login + ( HasPasswordHash(..) + , defaultStrength + , setPasswordStrength + , setPassword + , validatePass + -- * Interface to database and Yesod.Auth + , validateUserWithPasswordHash + -- Login Route Handler + , postLoginR + -- Logout Route Handler + , postLogoutR + ) +where + +import Startlude +import Data.Aeson ( withObject ) +import Yesod.Auth ( setCredsRedirect + , clearCreds + , Creds(..) + ) +import Yesod.Core +import Yesod.Persist + +import Auth +import Foundation +import Lib.Password +import Model + +-- Internal data type for receiving JSON encoded accountIdentifier and password +data LoginReq = LoginReq + { loginReqName :: Text + , loginReqPassword :: Text + } + +instance FromJSON LoginReq where + parseJSON = withObject "Login Request" $ \o -> do + -- future version can pass an accountIdentifier + let loginReqName = rootAccountName + loginReqPassword <- o .: "password" + pure LoginReq { .. } + +-- the redirect in the 'then' block gets picked up by the 'authenticate' +-- function in the YesodAuth instance for AgentCtx +postLoginR :: SubHandlerFor Auth AgentCtx TypedContent +postLoginR = do + LoginReq name password <- requireCheckJsonBody + isValid <- liftHandler $ validateUserWithPasswordHash (UniqueAccount name) password + if isValid then liftHandler $ setCredsRedirect $ Creds "hashdb" name [] else notAuthenticated + +-- the redirect in the 'then' block gets picked up by the 'authenticate' +-- function in the YesodAuth instance for AgentCtx +postLogoutR :: SubHandlerFor Auth AgentCtx () +postLogoutR = liftHandler $ clearCreds False + +-- | Given a user unique identifier and password in plaintext, validate them against +-- the database values. This function simply looks up the user id in the +-- database and calls 'validatePass' to do the work. +validateUserWithPasswordHash :: Unique Account -> Text -> Handler Bool +validateUserWithPasswordHash name password = do + account <- runDB $ getBy name + pure case account of + Nothing -> False + Just account' -> flip validatePass password . entityVal $ account' + diff --git a/agent/src/Handler/Notifications.hs b/agent/src/Handler/Notifications.hs new file mode 100644 index 000000000..9d99a1076 --- /dev/null +++ b/agent/src/Handler/Notifications.hs @@ -0,0 +1,32 @@ +module Handler.Notifications where + +import Startlude + +import Data.UUID +import Database.Persist +import Yesod.Core.Handler +import Yesod.Core.Types ( JSONResponse(..) ) +import Yesod.Persist.Core + +import Foundation +import qualified Lib.Notifications as Notification +import Model + +getNotificationsR :: Handler (JSONResponse [Entity Notification]) +getNotificationsR = runDB $ do + page <- lookupGetParam "page" `orDefaultTo` 1 + pageSize <- lookupGetParam "perPage" `orDefaultTo` 20 + evs <- selectList [] [Desc NotificationCreatedAt, LimitTo pageSize, OffsetBy ((page - 1) * pageSize)] + let toArchive = fmap entityKey $ filter ((== Nothing) . notificationArchivedAt . entityVal) evs + void $ Notification.archive toArchive + pure $ JSONResponse evs + where + orDefaultTo :: (Monad m, Read a) => m (Maybe Text) -> a -> m a + orDefaultTo m a = do + m' <- m + case m' >>= readMaybe . toS of + Nothing -> pure a + Just x -> pure x + +deleteNotificationR :: UUID -> Handler () +deleteNotificationR notifId = runDB $ delete (coerce @_ @(Key Notification) notifId) diff --git a/agent/src/Handler/PasswordUpdate.hs b/agent/src/Handler/PasswordUpdate.hs new file mode 100644 index 000000000..afcbb1e22 --- /dev/null +++ b/agent/src/Handler/PasswordUpdate.hs @@ -0,0 +1,36 @@ +{-# LANGUAGE RecordWildCards #-} +module Handler.PasswordUpdate where + +import Startlude hiding ( ask ) + +import Data.Aeson +import Yesod.Core hiding ( expiresAt ) +import Yesod.Persist + + +import Foundation +import Lib.Error +import Lib.Password +import Model + +patchPasswordR :: Handler () +patchPasswordR = handleS9ErrT $ do + PasswordUpdateReq {..} <- requireCheckJsonBody + updateAccountRegistration rootAccountName passwordUpdateReqPassword +data PasswordUpdateReq = PasswordUpdateReq + { passwordUpdateReqPassword :: Text + } deriving (Eq, Show) +instance FromJSON PasswordUpdateReq where + parseJSON = withObject "Update Password" $ \o -> do + passwordUpdateReqPassword <- o .: "value" + pure PasswordUpdateReq { .. } + +updateAccountRegistration :: Text -> Text -> S9ErrT Handler () +updateAccountRegistration acctName newPassword = do + now <- liftIO $ getCurrentTime + account <- (lift . runDB . getBy $ UniqueAccount acctName) >>= \case + Nothing -> throwE $ NotFoundE "account" acctName + Just a -> pure a + + account' <- setPassword newPassword $ (entityVal account) { accountUpdatedAt = now } + (lift . runDB $ Yesod.Persist.replace (entityKey account) account') diff --git a/agent/src/Handler/PowerOff.hs b/agent/src/Handler/PowerOff.hs new file mode 100644 index 000000000..d48552723 --- /dev/null +++ b/agent/src/Handler/PowerOff.hs @@ -0,0 +1,28 @@ +module Handler.PowerOff where + +import Startlude + +import System.Process + +import Foundation +import Lib.Sound +import Yesod.Core.Handler +import Network.HTTP.Types + +postShutdownR :: Handler () +postShutdownR = do + liftIO $ callCommand "/bin/sync" + liftIO $ playSong 400 marioDeath + void $ liftIO $ forkIO $ do + threadDelay 1_000_000 + callCommand "/sbin/shutdown now" + sendResponseStatus status200 () + +postRestartR :: Handler () +postRestartR = do + liftIO $ callCommand "/bin/sync" + liftIO $ playSong 400 marioDeath + void $ liftIO $ forkIO $ do + threadDelay 1_000_000 + callCommand "/sbin/reboot" + sendResponseStatus status200 () \ No newline at end of file diff --git a/agent/src/Handler/Register.hs b/agent/src/Handler/Register.hs new file mode 100644 index 000000000..a6a2c24a5 --- /dev/null +++ b/agent/src/Handler/Register.hs @@ -0,0 +1,140 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} +module Handler.Register where + +import Startlude hiding ( ask ) + +import Control.Carrier.Error.Either ( runError ) +import Control.Carrier.Lift +import Control.Effect.Throw ( liftEither ) +import Crypto.Cipher.Types +import Data.ByteArray.Sized +import qualified Data.ByteString as BS +import qualified Data.Text as T +import Database.Persist +import Network.HTTP.Types.Status +import Yesod.Core hiding ( expiresAt ) +import Yesod.Persist.Core + +import Daemon.ZeroConf +import Foundation +import Handler.Register.Nginx +import Handler.Register.Tor +import Handler.Types.HmacSig +import Handler.Types.Register +import Lib.Crypto +import Lib.Error +import Lib.Password +import Lib.ProductKey +import Lib.Ssl +import Lib.SystemPaths +import Model +import Settings + +postRegisterR :: Handler RegisterRes +postRegisterR = handleS9ErrT $ do + settings <- getsYesod appSettings + + productKey <- liftIO . getProductKey . appFilesystemBase $ settings + req <- requireCheckJsonBody + + -- Decrypt torkey and password. This acts as product key authentication. + torKeyFileContents <- decryptTorkey productKey req + password <- decryptPassword productKey req + rsaKeyFileContents <- decryptRSAKey productKey req + + -- Check for existing registration. + checkExistingPasswordRegistration rootAccountName >>= \case + Nothing -> pure () + Just _ -> sendResponseStatus (Status 209 "Preexisting") () + + -- install new tor hidden service key and restart tor + registerResTorAddress <- runM (injectFilesystemBaseFromContext settings $ bootupTor torKeyFileContents) >>= \case + Just t -> pure t + Nothing -> throwE TorServiceTimeoutE + + -- install new ssl CA cert + nginx conf and restart nginx + registerResCert <- + runM . handleS9ErrC . (>>= liftEither) . liftIO . runM . injectFilesystemBaseFromContext settings $ do + bootupHttpNginx + runError @S9Error $ bootupSslNginx rsaKeyFileContents + + -- create an hmac of the torAddress + caCert for front end + registerResTorAddressSig <- produceProofOfKey productKey registerResTorAddress + registerResCertSig <- produceProofOfKey productKey registerResCert + + -- must match CN in config/csr.conf + let registerResCertName = root_CA_CERT_NAME + registerResLanAddress <- runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostnameLocal + + -- registration successful, save the password hash + registerResClaimedAt <- saveAccountRegistration rootAccountName password + pure RegisterRes { .. } + + +decryptTorkey :: MonadIO m => Text -> RegisterReq -> S9ErrT m ByteString +decryptTorkey productKey RegisterReq { registerTorKey, registerTorCtrCounter, registerTorKdfSalt } = do + aesKey <- case mkAesKey registerTorKdfSalt productKey of + Just k -> pure k + Nothing -> throwE ProductKeyE + + torKeyFileContents <- case makeIV registerTorCtrCounter of + Just counter -> pure $ decryptAes256Ctr aesKey counter (unSizedByteArray registerTorKey) + Nothing -> throwE $ ClientCryptographyE "invalid torkey aes ctr counter" + + unless (torKeyPrefix `BS.isPrefixOf` torKeyFileContents) (throwE $ ClientCryptographyE "invalid tor key encryption") + + pure torKeyFileContents + where torKeyPrefix = "== ed25519v1-secret: type0 ==" + +decryptPassword :: MonadIO m => Text -> RegisterReq -> S9ErrT m Text +decryptPassword productKey RegisterReq { registerPassword, registerPasswordCtrCounter, registerPasswordKdfSalt } = do + aesKey <- case mkAesKey registerPasswordKdfSalt productKey of + Just k -> pure k + Nothing -> throwE ProductKeyE + + password <- case makeIV registerPasswordCtrCounter of + Just counter -> pure $ decryptAes256Ctr aesKey counter registerPassword + Nothing -> throwE $ ClientCryptographyE "invalid password aes ctr counter" + + let decoded = decodeUtf8 password + unless (passwordPrefix `T.isPrefixOf` decoded) (throwE $ ClientCryptographyE "invalid password encryption") + + -- drop password prefix in this case + pure . T.drop (T.length passwordPrefix) $ decoded + where passwordPrefix = "== password ==" + +decryptRSAKey :: MonadIO m => Text -> RegisterReq -> S9ErrT m ByteString +decryptRSAKey productKey RegisterReq { registerRsa, registerRsaCtrCounter, registerRsaKdfSalt } = do + aesKey <- case mkAesKey registerRsaKdfSalt productKey of + Just k -> pure k + Nothing -> throwE ProductKeyE + + cert <- case makeIV registerRsaCtrCounter of + Just counter -> pure $ decryptAes256Ctr aesKey counter registerRsa + Nothing -> throwE $ ClientCryptographyE "invalid password aes ctr counter" + + unless (certPrefix `BS.isPrefixOf` cert) (throwE $ ClientCryptographyE "invalid cert encryption") + + pure cert + where certPrefix = "-----BEGIN RSA PRIVATE KEY-----" + + +checkExistingPasswordRegistration :: Text -> S9ErrT Handler (Maybe UTCTime) +checkExistingPasswordRegistration acctIdentifier = lift . runDB $ do + mAccount <- getBy $ UniqueAccount acctIdentifier + pure $ fmap (accountCreatedAt . entityVal) mAccount + +saveAccountRegistration :: Text -> Text -> S9ErrT Handler UTCTime +saveAccountRegistration acctName password = lift . runDB $ do + now <- liftIO getCurrentTime + account <- setPassword password $ accountNoPw now + insert_ account + pure now + where accountNoPw t = Account t t acctName "" + +produceProofOfKey :: MonadIO m => Text -> Text -> m HmacSig +produceProofOfKey key message = do + salt <- random16 + let hmac = computeHmac key message salt + pure $ HmacSig hmac message salt diff --git a/agent/src/Handler/Register/Nginx.hs b/agent/src/Handler/Register/Nginx.hs new file mode 100644 index 000000000..59b4da6bc --- /dev/null +++ b/agent/src/Handler/Register/Nginx.hs @@ -0,0 +1,158 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE QuasiQuotes #-} +module Handler.Register.Nginx where + +import Startlude hiding ( ask + , catchError + ) + +import Control.Carrier.Error.Church +import Control.Effect.Lift +import qualified Control.Effect.Reader.Labelled + as Fused +import qualified Data.ByteString as BS +import System.Directory +import Daemon.ZeroConf +import Lib.ClientManifest +import Lib.Error +import Lib.Ssl +import Lib.Synchronizers +import Lib.SystemPaths +import Lib.Tor +import System.Posix ( removeLink ) + +-- Left error, Right CA cert for hmac signing +bootupSslNginx :: (HasFilesystemBase sig m, Has (Error S9Error) sig m, Has (Lift IO) sig m, MonadIO m) + => ByteString + -> m Text +bootupSslNginx rsaKeyFileContents = do + -- we need to ensure if the ssl setup fails that we remove all openssl key material and the nginx ssl conf before + -- starting again + resetSslState + cert <- writeSslKeyAndCert rsaKeyFileContents + sid <- getStart9AgentHostname + installAmbassadorUiNginxHTTPS (sslOverrides sid) "start9-ambassador-ssl.conf" + pure cert + where + sslOverrides sid = + let hostname = sid <> ".local" + in NginxSiteConfOverride + { nginxSiteConfOverrideAdditionalServerName = hostname + , nginxSiteConfOverrideListen = 443 + , nginxSiteConfOverrideSsl = Just $ NginxSsl { nginxSslKeyPath = entityKeyPath sid + , nginxSslCertPath = entityCertPath sid + , nginxSslOnlyServerNames = [hostname] + } + } + +resetSslState :: (HasFilesystemBase sig m, Has (Lift IO) sig m, MonadIO m) => m () +resetSslState = do + base <- Fused.ask @"filesystemBase" + host <- getStart9AgentHostname + -- remove all files we explicitly create + traverse_ + (liftIO . removePathForcibly . toS . flip relativeTo base) + [ rootCaKeyPath + , relBase $ (rootCaCertPath `relativeTo` "/") <> ".csr" + , rootCaCertPath + , intermediateCaKeyPath + , relBase $ (intermediateCaCertPath `relativeTo` "/") <> ".csr" + , intermediateCaCertPath + , entityKeyPath host + , relBase $ (entityCertPath host `relativeTo` "/") <> ".csr" + , entityCertPath host + , entityConfPath host + , nginxSitesAvailable nginxSslConf + ] + liftIO $ do + withCurrentDirectory (toS $ flip relativeTo base $ rootCaDirectory <> "/newcerts") + $ listDirectory "." + >>= traverse_ removePathForcibly + withCurrentDirectory (toS $ flip relativeTo base $ intermediateCaDirectory <> "/newcerts") + $ listDirectory "." + >>= traverse_ removePathForcibly + writeFile (toS $ flip relativeTo base $ rootCaDirectory <> "/index.txt") "" + writeFile (toS $ flip relativeTo base $ intermediateCaDirectory <> "/index.txt") "" + _ <- liftIO $ try @SomeException . removeLink . toS $ (nginxSitesEnabled nginxSslConf) `relativeTo` base + pure () + + +bootupHttpNginx :: (HasFilesystemBase sig m, MonadIO m) => m () +bootupHttpNginx = installAmbassadorUiNginxHTTP "start9-ambassador.conf" + +writeSslKeyAndCert :: (MonadIO m, HasFilesystemBase sig m, Has (Error S9Error) sig m) => ByteString -> m Text +writeSslKeyAndCert rsaKeyFileContents = do + directory <- toS <$> getAbsoluteLocationFor sslDirectory + caKeyPath <- toS <$> getAbsoluteLocationFor rootCaKeyPath + caConfPath <- toS <$> getAbsoluteLocationFor rootCaOpenSslConfPath + caCertPath <- toS <$> getAbsoluteLocationFor rootCaCertPath + intCaKeyPath <- toS <$> getAbsoluteLocationFor intermediateCaKeyPath + intCaConfPath <- toS <$> getAbsoluteLocationFor intermediateCaOpenSslConfPath + intCaCertPath <- toS <$> getAbsoluteLocationFor intermediateCaCertPath + sid <- getStart9AgentHostname + entKeyPath <- toS <$> getAbsoluteLocationFor (entityKeyPath sid) + entConfPath <- toS <$> getAbsoluteLocationFor (entityConfPath sid) + entCertPath <- toS <$> getAbsoluteLocationFor (entityCertPath sid) + torAddr <- getAgentHiddenServiceUrl + + let hostname = sid <> ".local" + + liftIO $ createDirectoryIfMissing False directory + liftIO $ BS.writeFile caKeyPath rsaKeyFileContents + + (exit, str1, str2) <- writeRootCaCert caConfPath caKeyPath caCertPath + liftIO $ do + putStrLn @Text "openssl logs" + putStrLn @Text "exit code: " + print exit + putStrLn @String $ "stdout: " <> str1 + putStrLn @String $ "stderr: " <> str2 + case exit of + ExitSuccess -> pure () + ExitFailure ec -> throwError $ OpenSslE "root" ec str1 str2 + + (exit', str1', str2') <- writeIntermediateCert $ DeriveCertificate { applicantConfPath = intCaConfPath + , applicantKeyPath = intCaKeyPath + , applicantCertPath = intCaCertPath + , signingConfPath = caConfPath + , signingKeyPath = caKeyPath + , signingCertPath = caCertPath + , duration = 3650 + } + liftIO $ do + putStrLn @Text "openssl logs" + putStrLn @Text "exit code: " + print exit' + putStrLn @String $ "stdout: " <> str1' + putStrLn @String $ "stderr: " <> str2' + case exit' of + ExitSuccess -> pure () + ExitFailure ec -> throwError $ OpenSslE "intermediate" ec str1' str2' + + + liftIO $ BS.writeFile entConfPath (domain_CSR_CONF hostname) + + (exit'', str1'', str2'') <- writeLeafCert + DeriveCertificate { applicantConfPath = entConfPath + , applicantKeyPath = entKeyPath + , applicantCertPath = entCertPath + , signingConfPath = intCaConfPath + , signingKeyPath = intCaKeyPath + , signingCertPath = intCaCertPath + , duration = 365 + } + hostname + torAddr + + liftIO $ do + putStrLn @Text "openssl logs" + putStrLn @Text "exit code: " + print exit'' + putStrLn @String $ "stdout: " <> str1'' + putStrLn @String $ "stderr: " <> str2'' + case exit'' of + ExitSuccess -> pure () + ExitFailure ec -> throwError $ OpenSslE "leaf" ec str1' str2' + + readSystemPath' rootCaCertPath diff --git a/agent/src/Handler/Register/Tor.hs b/agent/src/Handler/Register/Tor.hs new file mode 100644 index 000000000..d93f3c24e --- /dev/null +++ b/agent/src/Handler/Register/Tor.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} +module Handler.Register.Tor where + +import Startlude hiding ( ask ) + +import Control.Effect.Reader.Labelled +import qualified Data.ByteString as BS +import System.Directory +import System.Process +import Lib.SystemCtl +import Lib.SystemPaths +import Lib.Tor + +bootupTor :: (HasFilesystemBase sig m, MonadIO m) => ByteString -> m (Maybe Text) +bootupTor torKeyFileContents = do + base <- ask @"filesystemBase" + writeTorPrivateKeyFile torKeyFileContents + + putStrLn @Text "restarting tor" + liftIO . void $ systemCtl RestartService "tor" + putStrLn @Text "restarted tor" + + liftIO . fmap (join . hush) $ race + (threadDelay 30_000_000) + (runMaybeT . asum . repeat $ MaybeT . fmap hush $ try @SomeException + (threadDelay 100_000 *> injectFilesystemBase base getAgentHiddenServiceUrl) + ) + +writeTorPrivateKeyFile :: (MonadIO m, HasFilesystemBase sig m) => ByteString -> m () +writeTorPrivateKeyFile contents = do + directory <- fmap toS . getAbsoluteLocationFor $ agentTorHiddenServiceDirectory + privateKeyFilePath <- fmap toS . getAbsoluteLocationFor $ agentTorHiddenServicePrivateKeyPath + liftIO $ do + -- Clean out directory + removePathForcibly directory + createDirectory directory + + -- write private key file + BS.writeFile privateKeyFilePath contents + + -- Set ownership and permissions so tor executable can generate other files + callCommand $ "chown -R debian-tor:debian-tor " <> directory + callCommand $ "chmod 2700 " <> directory \ No newline at end of file diff --git a/agent/src/Handler/SelfUpdate.hs b/agent/src/Handler/SelfUpdate.hs new file mode 100644 index 000000000..c94da316d --- /dev/null +++ b/agent/src/Handler/SelfUpdate.hs @@ -0,0 +1,51 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} + +module Handler.SelfUpdate where + +import Startlude + +import Control.Carrier.Error.Either +import Data.Aeson +import Yesod.Core + +import Foundation +import Lib.Algebra.State.RegistryUrl +import Lib.Error +import Lib.External.Registry +import Lib.SystemPaths +import Lib.Types.Emver + +newtype UpdateAgentReq = UpdateAgentReq { updateAgentVersionSpecification :: VersionRange } deriving (Eq, Show) + +instance FromJSON UpdateAgentReq where + parseJSON = withObject "update agent request" $ fmap UpdateAgentReq . (.: "version") + +newtype UpdateAgentRes = UpdateAgentRes { status :: UpdateInitStatus } deriving (Eq) +instance ToJSON UpdateAgentRes where + toJSON (UpdateAgentRes status) = object ["status" .= status] + +instance ToTypedContent UpdateAgentRes where + toTypedContent = toTypedContent . toJSON +instance ToContent UpdateAgentRes where + toContent = toContent . toJSON + + +data UpdateInitStatus = UpdatingAlreadyInProgress | UpdatingCommence deriving (Show, Eq) +instance ToJSON UpdateInitStatus where + toJSON UpdatingAlreadyInProgress = String "UPDATING_ALREADY_IN_PROGRESS" + toJSON UpdatingCommence = String "UPDATING_COMMENCE" + +postUpdateAgentR :: Handler UpdateAgentRes +postUpdateAgentR = handleS9ErrT $ do + settings <- getsYesod appSettings + avs <- updateAgentVersionSpecification <$> requireCheckJsonBody + mVersion <- interp settings $ getLatestAgentVersionForSpec avs + + when (isNothing mVersion) $ throwE $ NoCompliantAgentE avs + + updateSpecBox <- getsYesod appSelfUpdateSpecification + success <- liftIO $ tryPutMVar updateSpecBox avs + + if success then pure $ UpdateAgentRes UpdatingCommence else pure $ UpdateAgentRes UpdatingAlreadyInProgress + where interp s = ExceptT . liftIO . runError . injectFilesystemBaseFromContext s . runRegistryUrlIOC diff --git a/agent/src/Handler/SshKeys.hs b/agent/src/Handler/SshKeys.hs new file mode 100644 index 000000000..6224bb1e2 --- /dev/null +++ b/agent/src/Handler/SshKeys.hs @@ -0,0 +1,39 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +module Handler.SshKeys where + +import Startlude + +import Yesod.Core +import Yesod.Core.Types ( JSONResponse(..) ) + +import Foundation +import Lib.Error +import Lib.Ssh +import Util.Function +import Handler.Types.V0.Ssh + +postSshKeysR :: Handler SshKeyFingerprint +postSshKeysR = handleS9ErrT $ do + settings <- getsYesod appSettings + key <- sshKey <$> requireCheckJsonBody + case fingerprint key of + Left e -> throwE $ InvalidSshKeyE (toS e) + Right fp -> do + runReaderT (createSshKey key) settings + pure $ uncurry3 SshKeyFingerprint fp + +deleteSshKeyByFingerprintR :: Text -> Handler () +deleteSshKeyByFingerprintR key = handleS9ErrT $ do + settings <- getsYesod appSettings + runReaderT (deleteSshKey key) settings >>= \case + True -> pure () + False -> throwE $ NotFoundE "sshKey" key + +getSshKeysR :: Handler (JSONResponse [SshKeyFingerprint]) -- deprecated in 0.2.0 +getSshKeysR = handleS9ErrT $ do + settings <- getsYesod appSettings + keys <- runReaderT getSshKeys settings + JSONResponse <$> case traverse fingerprint keys of + Left e -> throwE $ InvalidSshKeyE (toS e) + Right as -> pure $ uncurry3 SshKeyFingerprint <$> as diff --git a/agent/src/Handler/Status.hs b/agent/src/Handler/Status.hs new file mode 100644 index 000000000..4066ccce5 --- /dev/null +++ b/agent/src/Handler/Status.hs @@ -0,0 +1,71 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +module Handler.Status where + +import Startlude + +import Control.Carrier.Error.Either +import Data.Aeson.Encoding +import Git.Embed +import Yesod.Core.Handler +import Yesod.Core.Json +import Yesod.Core.Types + +import Constants +import Daemon.ZeroConf +import Foundation +import Handler.Types.Metrics +import Handler.Types.V0.Specs +import Handler.Types.V0.Base +import Lib.Algebra.State.RegistryUrl +import Lib.Error +import Lib.External.Metrics.Df +import qualified Lib.External.Registry as Reg +import Lib.External.Specs.CPU +import Lib.External.Specs.Memory +import Lib.Metrics +import Lib.SystemPaths hiding ( () ) +import Lib.Tor +import Settings +import Control.Carrier.Lift ( runM ) + +getVersionR :: Handler AppVersionRes +getVersionR = pure . AppVersionRes $ agentVersion + +getVersionLatestR :: Handler VersionLatestRes +getVersionLatestR = handleS9ErrT $ do + s <- getsYesod appSettings + v <- interp s $ Reg.getLatestAgentVersion + pure $ VersionLatestRes v + where interp s = ExceptT . liftIO . runError . injectFilesystemBaseFromContext s . runRegistryUrlIOC + + +getSpecsR :: Handler Encoding -- deprecated in 0.2.0 +getSpecsR = handleS9ErrT $ do + settings <- getsYesod appSettings + specsCPU <- liftIO getCpuInfo + specsMem <- liftIO getMem + specsDisk <- fmap show . metricDiskSize <$> getDfMetrics + specsNetworkId <- lift . runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostname + specsTorAddress <- lift . runM . injectFilesystemBaseFromContext settings $ getAgentHiddenServiceUrl + + let specsAgentVersion = agentVersion + returnJsonEncoding SpecsRes { .. } + +getMetricsR :: Handler (JSONResponse MetricsRes) +getMetricsR = do + app <- getYesod + fmap (JSONResponse . MetricsRes) . handleS9ErrT . getServerMetrics $ app + +embassyNamePath :: SystemPath +embassyNamePath = "/root/agent/name.txt" + +patchServerR :: Handler () +patchServerR = do + PatchServerReq { patchServerReqName } <- requireCheckJsonBody @_ @PatchServerReq + base <- getsYesod $ appFilesystemBase . appSettings + liftIO $ writeFile (toS $ embassyNamePath `relativeTo` base) patchServerReqName + +getGitR :: Handler Text +getGitR = pure $embedGitRevision + diff --git a/agent/src/Handler/Tor.hs b/agent/src/Handler/Tor.hs new file mode 100644 index 000000000..a12f9b6bf --- /dev/null +++ b/agent/src/Handler/Tor.hs @@ -0,0 +1,24 @@ +module Handler.Tor where + +import Startlude + +import Data.Aeson +import Yesod.Core + +import Foundation +import Lib.SystemPaths +import Lib.Tor +import Control.Carrier.Lift ( runM ) + +newtype GetTorRes = GetTorRes { unGetTorRes :: Text } +instance ToJSON GetTorRes where + toJSON a = object ["torAddress" .= unGetTorRes a] +instance ToContent GetTorRes where + toContent = toContent . toJSON +instance ToTypedContent GetTorRes where + toTypedContent = toTypedContent . toJSON + +getTorAddressR :: Handler GetTorRes +getTorAddressR = do + settings <- getsYesod appSettings + runM $ GetTorRes <$> injectFilesystemBaseFromContext settings getAgentHiddenServiceUrl diff --git a/agent/src/Handler/Types/Apps.hs b/agent/src/Handler/Types/Apps.hs new file mode 100644 index 000000000..548db4b02 --- /dev/null +++ b/agent/src/Handler/Types/Apps.hs @@ -0,0 +1,178 @@ +{-# LANGUAGE StandaloneKindSignatures #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +module Handler.Types.Apps where + +import Startlude + +import Data.Aeson +import Data.Aeson.Flatten +import Data.Singletons + +import Lib.TyFam.ConditionalData +import Lib.Types.Core +import Lib.Types.Emver +import Lib.Types.Emver.Orphans ( ) +import Lib.Types.NetAddress +data AppBase = AppBase + { appBaseId :: AppId + , appBaseTitle :: Text + , appBaseIconUrl :: Text + } + deriving (Eq, Show) +instance ToJSON AppBase where + toJSON AppBase {..} = object ["id" .= appBaseId, "title" .= appBaseTitle, "iconURL" .= appBaseIconUrl] + +data AppAvailablePreview = AppAvailablePreview + { appAvailablePreviewBase :: AppBase + , appAvailablePreviewVersionLatest :: Version + , appAvailablePreviewDescriptionShort :: Text + , appAvailablePreviewInstallInfo :: Maybe (Version, AppStatus) + } + deriving (Eq, Show) +instance ToJSON AppAvailablePreview where + toJSON AppAvailablePreview {..} = mergeTo (toJSON appAvailablePreviewBase) $ object + [ "versionLatest" .= appAvailablePreviewVersionLatest + , "descriptionShort" .= appAvailablePreviewDescriptionShort + , "versionInstalled" .= (fst <$> appAvailablePreviewInstallInfo) + , "status" .= (snd <$> appAvailablePreviewInstallInfo) + ] + +data AppInstalledPreview = AppInstalledPreview + { appInstalledPreviewBase :: AppBase + , appInstalledPreviewStatus :: AppStatus + , appInstalledPreviewVersionInstalled :: Version + , appInstalledPreviewTorAddress :: Maybe TorAddress + } + deriving (Eq, Show) +instance ToJSON AppInstalledPreview where + toJSON AppInstalledPreview {..} = mergeTo (toJSON appInstalledPreviewBase) $ object + [ "status" .= appInstalledPreviewStatus + , "versionInstalled" .= appInstalledPreviewVersionInstalled + , "torAddress" .= (unTorAddress <$> appInstalledPreviewTorAddress) + ] + +data InstallNewAppReq = InstallNewAppReq + { installNewAppVersion :: Version + , installNewAppDryRun :: Bool + } + deriving (Eq, Show) +instance FromJSON InstallNewAppReq where + parseJSON = withObject "Install New App Request" $ \o -> do + installNewAppVersion <- o .: "version" + installNewAppDryRun <- o .:? "dryRun" .!= False + pure InstallNewAppReq { .. } + +data AppAvailableFull = AppAvailableFull + { appAvailableFullBase :: AppBase + , appAvailableFullInstallInfo :: Maybe (Version, AppStatus) + , appAvailableFullVersionLatest :: Version + , appAvailableFullDescriptionShort :: Text + , appAvailableFullDescriptionLong :: Text + , appAvailableFullReleaseNotes :: Text + , appAvailableFullDependencyRequirements :: [Full AppDependencyRequirement] + , appAvailableFullVersions :: NonEmpty Version + } + -- deriving Eq +instance ToJSON AppAvailableFull where + toJSON AppAvailableFull {..} = mergeTo + (toJSON appAvailableFullBase) + (object + [ "versionInstalled" .= fmap fst appAvailableFullInstallInfo + , "status" .= fmap snd appAvailableFullInstallInfo + , "versionLatest" .= appAvailableFullVersionLatest + , "descriptionShort" .= appAvailableFullDescriptionShort + , "descriptionLong" .= appAvailableFullDescriptionLong + , "versions" .= appAvailableFullVersions + , "releaseNotes" .= appAvailableFullReleaseNotes + , "serviceRequirements" .= appAvailableFullDependencyRequirements + ] + ) + +type AppDependencyRequirement :: (Type ~> Type) -> Type +data AppDependencyRequirement f = AppDependencyRequirement + { appDependencyRequirementBase :: AppBase + , appDependencyRequirementReasonOptional :: Apply f (Maybe Text) + , appDependencyRequirementDefault :: Apply f Bool + , appDependencyRequirementDescription :: Maybe Text + , appDependencyRequirementViolation :: Maybe ApiDependencyViolation + , appDependencyRequirementVersionSpec :: VersionRange + } +instance ToJSON (AppDependencyRequirement Strip) where + toJSON AppDependencyRequirement {..} = mergeTo (toJSON appDependencyRequirementBase) $ object + [ "versionSpec" .= appDependencyRequirementVersionSpec + , "description" .= appDependencyRequirementDescription + , "violation" .= appDependencyRequirementViolation + ] +instance ToJSON (AppDependencyRequirement Keep) where + toJSON r = + let stripped = r { appDependencyRequirementReasonOptional = (), appDependencyRequirementDefault = () } + in + mergeTo + (toJSON @(AppDependencyRequirement Strip) stripped) + (object + [ "optional" .= appDependencyRequirementReasonOptional r + , "default" .= appDependencyRequirementDefault r + ] + ) + +-- filter non required dependencies in installed show +-- mute violations downstream of version for installing apps +data AppInstalledFull = AppInstalledFull + { appInstalledFullBase :: AppBase + , appInstalledFullStatus :: AppStatus + , appInstalledFullVersionInstalled :: Version + , appInstalledFullTorAddress :: Maybe TorAddress + , appInstalledFullInstructions :: Maybe Text + , appInstalledFullLastBackup :: Maybe UTCTime + , appInstalledFullConfiguredRequirements :: [Stripped AppDependencyRequirement] + } +instance ToJSON AppInstalledFull where + toJSON AppInstalledFull {..} = object + [ "instructions" .= appInstalledFullInstructions + , "lastBackup" .= appInstalledFullLastBackup + , "configuredRequirements" .= appInstalledFullConfiguredRequirements + , "torAddress" .= (unTorAddress <$> appInstalledFullTorAddress) + , "id" .= appBaseId appInstalledFullBase + , "title" .= appBaseTitle appInstalledFullBase + , "iconURL" .= appBaseIconUrl appInstalledFullBase + , "versionInstalled" .= appInstalledFullVersionInstalled + , "status" .= appInstalledFullStatus + ] + +data AppVersionInfo = AppVersionInfo + { appVersionInfoVersion :: Version + , appVersionInfoReleaseNotes :: Text + , appVersionInfoDependencyRequirements :: [Full AppDependencyRequirement] + } +instance ToJSON AppVersionInfo where + toJSON AppVersionInfo {..} = object + [ "version" .= appVersionInfoVersion + , "releaseNotes" .= appVersionInfoReleaseNotes + , "serviceRequirements" .= appVersionInfoDependencyRequirements + ] + +data ApiDependencyViolation + = Missing + | IncompatibleVersion + | IncompatibleConfig [Text] -- rule violations + | IncompatibleStatus AppStatus + +instance ToJSON ApiDependencyViolation where + toJSON Missing = object ["name" .= ("missing" :: Text)] + toJSON IncompatibleVersion = object ["name" .= ("incompatible-version" :: Text)] + toJSON (IncompatibleConfig ruleViolations) = + object ["name" .= ("incompatible-config" :: Text), "ruleViolations" .= ruleViolations] + toJSON (IncompatibleStatus status) = object ["name" .= ("incompatible-status" :: Text), "status" .= status] + +data WithBreakages a = WithBreakages [AppBase] a +instance {-# Overlappable #-} ToJSON a => ToJSON (WithBreakages a) where + toJSON (WithBreakages breakages thing) = mergeTo (toJSON thing) (object ["breakages" .= breakages]) +instance ToJSON (WithBreakages ()) where + toJSON (WithBreakages breakages _) = object ["breakages" .= breakages] + +newtype AutoconfigureChangesRes = AutoconfigureChangesRes + { autoconfigureChangesConfig :: Maybe Value + } +instance ToJSON AutoconfigureChangesRes where + toJSON AutoconfigureChangesRes {..} = object ["config" .= autoconfigureChangesConfig] diff --git a/agent/src/Handler/Types/HmacSig.hs b/agent/src/Handler/Types/HmacSig.hs new file mode 100644 index 000000000..73a0bf624 --- /dev/null +++ b/agent/src/Handler/Types/HmacSig.hs @@ -0,0 +1,28 @@ +{-# LANGUAGE RecordWildCards #-} +module Handler.Types.HmacSig where + +import Startlude + +import Crypto.Hash +import Data.Aeson +import Data.ByteArray.Encoding +import Data.ByteArray.Sized +import Yesod.Core + +import Handler.Types.Parse + +data HmacSig = HmacSig + { sigHmac :: Digest SHA256 + , sigMessage :: Text + , sigSalt :: SizedByteArray 16 ByteString + } + deriving (Eq, Show) + +instance ToJSON HmacSig where + toJSON (HmacSig {..}) = + object ["hmac" .= fromUnsizedBs Base16 sigHmac, "message" .= sigMessage, "salt" .= fromSizedBs Base16 sigSalt] + +instance ToTypedContent HmacSig where + toTypedContent = toTypedContent . toJSON +instance ToContent HmacSig where + toContent = toContent . toJSON diff --git a/agent/src/Handler/Types/Hosts.hs b/agent/src/Handler/Types/Hosts.hs new file mode 100644 index 000000000..20b18b6e1 --- /dev/null +++ b/agent/src/Handler/Types/Hosts.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE RecordWildCards #-} +module Handler.Types.Hosts where + +import Startlude + +import Crypto.Hash +import Data.Aeson +import Data.ByteArray.Encoding +import Data.ByteArray.Sized +import Yesod.Core + +import Handler.Types.Parse +import Handler.Types.Register +import Lib.Error + +data HostsParams = HostsParams + { hostsParamsHmac :: Digest SHA256 -- hmac of an expiration timestamp + , hostsParamsExpiration :: Text -- This is a UTC time text string. we leave it as text as it is precisely this which is signed by the above hmac. + , hostsParamsSalt :: SizedByteArray 16 ByteString + } + +data HostsRes = NullReply | HostsRes RegisterRes + deriving (Eq, Show) + +instance ToJSON HostsRes where + toJSON NullReply = Null + toJSON (HostsRes registerRes) = toJSON registerRes + +instance ToTypedContent HostsRes where + toTypedContent = toTypedContent . toJSON +instance ToContent HostsRes where + toContent = toContent . toJSON + +extractHostsQueryParams :: MonadHandler m => S9ErrT m HostsParams +extractHostsQueryParams = do + hostsParamsHmac <- lookupGetParam "hmac" <&> (>>= sizedBs @32 Base16 >=> digestFromByteString) >>= orThrow400 "hmac" + hostsParamsSalt <- lookupGetParam "salt" <&> (>>= sizedBs @16 Base16) >>= orThrow400 "salt" + hostsParamsExpiration <- lookupGetParam "message" >>= orThrow400 "message" + + pure HostsParams { .. } + where + orThrow400 desc = \case + Nothing -> throwE $ HostsParamsE desc + Just p -> pure p diff --git a/agent/src/Handler/Types/Metrics.hs b/agent/src/Handler/Types/Metrics.hs new file mode 100644 index 000000000..9427179b5 --- /dev/null +++ b/agent/src/Handler/Types/Metrics.hs @@ -0,0 +1,26 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} +module Handler.Types.Metrics where + +import Startlude + +import Lib.Metrics + +import Data.Aeson +import Yesod.Core.Content + +newtype MetricsRes = MetricsRes { unMetricsRes :: ServerMetrics } +instance ToJSON MetricsRes where + toJSON = toJSON . unMetricsRes + toEncoding = toEncoding . unMetricsRes +instance ToTypedContent MetricsRes where + toTypedContent = toTypedContent . toJSON +instance ToContent MetricsRes where + toContent = toContent . toJSON + +newtype PatchServerReq = PatchServerReq { patchServerReqName :: Text } +instance FromJSON PatchServerReq where + parseJSON = withObject "Patch Server Request" $ \o -> do + patchServerReqName <- o .: "name" + pure $ PatchServerReq { patchServerReqName } diff --git a/agent/src/Handler/Types/Parse.hs b/agent/src/Handler/Types/Parse.hs new file mode 100644 index 000000000..6ddba1f32 --- /dev/null +++ b/agent/src/Handler/Types/Parse.hs @@ -0,0 +1,32 @@ +module Handler.Types.Parse where + +import Startlude + +import Control.Monad.Fail +import Data.Aeson.Types +import Data.ByteArray +import Data.ByteArray.Encoding +import Data.ByteArray.Sized + +mToParser :: String -> Maybe a -> Parser a +mToParser failureText = \case + Nothing -> fail failureText + Just t -> pure t + +toUnsizedBs :: String -> Base -> Text -> Parser ByteString +toUnsizedBs failureText base = mToParser failureText . unsizedBs base + +unsizedBs :: Base -> Text -> Maybe ByteString +unsizedBs base = hush . convertFromBase base . encodeUtf8 + +toSizedBs :: KnownNat n => String -> Base -> Text -> Parser (SizedByteArray n ByteString) +toSizedBs failureText base = mToParser failureText . sizedBs base + +sizedBs :: KnownNat n => Base -> Text -> Maybe (SizedByteArray n ByteString) +sizedBs base = sizedByteArray <=< unsizedBs base + +fromUnsizedBs :: ByteArrayAccess ba => Base -> ba -> Text +fromUnsizedBs base = decodeUtf8 . convertToBase base + +fromSizedBs :: (KnownNat n, ByteArrayAccess ba) => Base -> SizedByteArray n ba -> Text +fromSizedBs b = fromUnsizedBs b . unSizedByteArray diff --git a/agent/src/Handler/Types/Register.hs b/agent/src/Handler/Types/Register.hs new file mode 100644 index 000000000..ccc78f28c --- /dev/null +++ b/agent/src/Handler/Types/Register.hs @@ -0,0 +1,65 @@ +{-# LANGUAGE RecordWildCards #-} +module Handler.Types.Register where + +import Startlude + +import Data.Aeson +import Data.ByteArray.Encoding +import Data.ByteArray.Sized +import Yesod.Core + +import Handler.Types.HmacSig +import Handler.Types.Parse + +data RegisterReq = RegisterReq + { registerTorKey :: SizedByteArray 96 ByteString -- Represents a tor private key along with tor private key file prefix. + , registerTorCtrCounter :: SizedByteArray 16 ByteString + , registerTorKdfSalt :: SizedByteArray 16 ByteString + , registerPassword :: ByteString -- Encrypted password + , registerPasswordCtrCounter :: SizedByteArray 16 ByteString + , registerPasswordKdfSalt :: SizedByteArray 16 ByteString + , registerRsa :: ByteString -- Encrypted RSA key + , registerRsaCtrCounter :: SizedByteArray 16 ByteString + , registerRsaKdfSalt :: SizedByteArray 16 ByteString + } + deriving (Eq, Show) + + +data RegisterRes = RegisterRes + { registerResClaimedAt :: UTCTime + , registerResTorAddressSig :: HmacSig + , registerResCertSig :: HmacSig + , registerResCertName :: Text + , registerResLanAddress :: Text + } + deriving (Eq, Show) + +instance FromJSON RegisterReq where + parseJSON = withObject "Register Tor Request" $ \o -> do + registerTorKey <- o .: "torkey" >>= toSizedBs "Invalid torkey encryption" Base16 + registerTorCtrCounter <- o .: "torkeyCounter" >>= toSizedBs "Invalid torkey ctr counter" Base16 + registerTorKdfSalt <- o .: "torkeySalt" >>= toSizedBs "Invalid torkey pbkdf2 salt" Base16 + + registerPassword <- o .: "password" >>= toUnsizedBs "Invalid password encryption" Base16 + registerPasswordCtrCounter <- o .: "passwordCounter" >>= toSizedBs "Invalid password ctr counter" Base16 + registerPasswordKdfSalt <- o .: "passwordSalt" >>= toSizedBs "Invalid password pbkdf2 salt" Base16 + + registerRsa <- o .: "rsaKey" >>= toUnsizedBs "Invalid rsa encryption" Base16 + registerRsaCtrCounter <- o .: "rsaCounter" >>= toSizedBs "Invalid rsa ctr counter" Base16 + registerRsaKdfSalt <- o .: "rsaSalt" >>= toSizedBs "Invalid rsa pbkdf2 salt" Base16 + + pure RegisterReq { .. } + +instance ToJSON RegisterRes where + toJSON (RegisterRes {..}) = object + [ "claimedAt" .= registerResClaimedAt + , "torAddressSig" .= registerResTorAddressSig + , "certSig" .= registerResCertSig + , "certName" .= registerResCertName + , "lanAddress" .= registerResLanAddress + ] + +instance ToTypedContent RegisterRes where + toTypedContent = toTypedContent . toJSON +instance ToContent RegisterRes where + toContent = toContent . toJSON diff --git a/agent/src/Handler/Types/V0/Base.hs b/agent/src/Handler/Types/V0/Base.hs new file mode 100644 index 000000000..d4fda857a --- /dev/null +++ b/agent/src/Handler/Types/V0/Base.hs @@ -0,0 +1,77 @@ +{-# LANGUAGE RecordWildCards #-} +module Handler.Types.V0.Base where + +import Startlude + +import Data.Aeson +import Database.Persist +import Yesod.Core + +import Handler.Types.V0.Ssh +import Handler.Types.V0.Specs +import Handler.Types.V0.Wifi +import Lib.Types.Core +import Lib.Types.Emver +import Model + +data VersionLatestRes = VersionLatestRes + { versionLatestVersion :: Version + } + deriving (Eq, Show) +instance ToJSON VersionLatestRes where + toJSON VersionLatestRes {..} = object $ ["versionLatest" .= versionLatestVersion] +instance ToTypedContent VersionLatestRes where + toTypedContent = toTypedContent . toJSON +instance ToContent VersionLatestRes where + toContent = toContent . toJSON + +data ServerRes = ServerRes + { serverId :: Text + , serverName :: Text + , serverStatus :: Maybe AppStatus + , serverStatusAt :: UTCTime + , serverVersionInstalled :: Version + , serverNotifications :: [Entity Notification] + , serverWifi :: WifiList + , serverSsh :: [SshKeyFingerprint] + , serverAlternativeRegistryUrl :: Maybe Text + , serverSpecs :: SpecsRes + } + deriving (Eq, Show) + +type JsonEncoding a = Encoding +jsonEncode :: (Monad m, ToJSON a) => a -> m (JsonEncoding a) +jsonEncode = returnJsonEncoding + +instance ToJSON ServerRes where + toJSON ServerRes {..} = object + [ "serverId" .= serverId + , "name" .= serverName + , "status" .= case serverStatus of + Nothing -> String "UPDATING" + Just stat -> toJSON stat + , "versionInstalled" .= serverVersionInstalled + , "versionLatest" .= Null + , "notifications" .= serverNotifications + , "wifi" .= serverWifi + , "ssh" .= serverSsh + , "alternativeRegistryUrl" .= serverAlternativeRegistryUrl + , "specs" .= serverSpecs + ] +instance ToTypedContent ServerRes where + toTypedContent = toTypedContent . toJSON +instance ToContent ServerRes where + toContent = toContent . toJSON + +newtype AppVersionRes = AppVersionRes + { unAppVersionRes :: Version } deriving (Eq, Show) +instance ToJSON AppVersionRes where + toJSON AppVersionRes { unAppVersionRes } = object ["version" .= unAppVersionRes] +instance FromJSON AppVersionRes where + parseJSON = withObject "app version response" $ \o -> do + av <- o .: "version" + pure $ AppVersionRes av +instance ToContent AppVersionRes where + toContent = toContent . toJSON +instance ToTypedContent AppVersionRes where + toTypedContent = toTypedContent . toJSON diff --git a/agent/src/Handler/Types/V0/Specs.hs b/agent/src/Handler/Types/V0/Specs.hs new file mode 100644 index 000000000..954cfff75 --- /dev/null +++ b/agent/src/Handler/Types/V0/Specs.hs @@ -0,0 +1,45 @@ +{-# LANGUAGE RecordWildCards #-} +module Handler.Types.V0.Specs where + +import Startlude + +import Lib.Types.Emver +import Lib.Types.Emver.Orphans ( ) + +import Data.Aeson +import Yesod.Core + +data SpecsRes = SpecsRes + { specsCPU :: Text + , specsMem :: Text + , specsDisk :: Maybe Text + , specsNetworkId :: Text + , specsAgentVersion :: Version + , specsTorAddress :: Text + } + deriving (Eq, Show) + +instance ToJSON SpecsRes where + toJSON SpecsRes {..} = object + [ "EmbassyOS Version" .= specsAgentVersion + , "Tor Address" .= specsTorAddress + , "Network ID" .= specsNetworkId + , "CPU" .= specsCPU + , "Memory" .= specsMem + , "Disk" .= specsDisk + ] + toEncoding SpecsRes {..} = + pairs + . fold + $ [ "EmbassyOS Version" .= specsAgentVersion + , "Tor Address" .= specsTorAddress + , "Network ID" .= specsNetworkId + , "CPU" .= specsCPU + , "Memory" .= specsMem + , "Disk" .= specsDisk + ] + +instance ToTypedContent SpecsRes where + toTypedContent = toTypedContent . toJSON +instance ToContent SpecsRes where + toContent = toContent . toJSON diff --git a/agent/src/Handler/Types/V0/Ssh.hs b/agent/src/Handler/Types/V0/Ssh.hs new file mode 100644 index 000000000..35dd6c8dc --- /dev/null +++ b/agent/src/Handler/Types/V0/Ssh.hs @@ -0,0 +1,25 @@ +{-# LANGUAGE RecordWildCards #-} +module Handler.Types.V0.Ssh where + +import Startlude + +import Lib.Ssh + +import Data.Aeson +import Yesod.Core + +newtype SshKeyModReq = SshKeyModReq { sshKey :: Text } deriving (Eq, Show) +instance FromJSON SshKeyModReq where + parseJSON = withObject "ssh key" $ fmap SshKeyModReq . (.: "sshKey") + +data SshKeyFingerprint = SshKeyFingerprint + { sshKeyAlg :: SshAlg + , sshKeyHash :: Text + , sshKeyHostname :: Text + } deriving (Eq, Show) +instance ToJSON SshKeyFingerprint where + toJSON SshKeyFingerprint {..} = object ["alg" .= sshKeyAlg, "hash" .= sshKeyHash, "hostname" .= sshKeyHostname] +instance ToTypedContent SshKeyFingerprint where + toTypedContent = toTypedContent . toJSON +instance ToContent SshKeyFingerprint where + toContent = toContent . toJSON diff --git a/agent/src/Handler/Types/V0/Wifi.hs b/agent/src/Handler/Types/V0/Wifi.hs new file mode 100644 index 000000000..e52193c6d --- /dev/null +++ b/agent/src/Handler/Types/V0/Wifi.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE RecordWildCards #-} +module Handler.Types.V0.Wifi where + +import Startlude + +import Data.Aeson +import Yesod.Core + +data AddWifiReq = AddWifiReq + { addWifiSsid :: Text + , addWifiPassword :: Text + , addWifiCountry :: Text + , skipConnect :: Bool + } deriving (Eq, Show) +instance FromJSON AddWifiReq where + parseJSON = withObject "AddWifiReq" $ \o -> do + addWifiSsid <- o .: "ssid" + addWifiPassword <- o .: "password" + addWifiCountry <- o .:? "country" .!= "US" + skipConnect <- o .:? "skipConnect" .!= False + pure AddWifiReq { .. } + +data WifiList = WifiList + { wifiListCurrent :: Maybe Text + , wifiListSsids :: [Text] + } deriving (Eq, Show) +instance ToJSON WifiList where + toJSON WifiList {..} = object ["current" .= wifiListCurrent, "ssids" .= wifiListSsids] +instance ToTypedContent WifiList where + toTypedContent = toTypedContent . toJSON +instance ToContent WifiList where + toContent = toContent . toJSON diff --git a/agent/src/Handler/Util.hs b/agent/src/Handler/Util.hs new file mode 100644 index 000000000..5349b3dc9 --- /dev/null +++ b/agent/src/Handler/Util.hs @@ -0,0 +1,16 @@ +module Handler.Util where + +import Startlude + +import Data.IORef +import Yesod.Core + +import Foundation +import Lib.Error + +disableEndpointOnFailedUpdate :: Handler a -> Handler a +disableEndpointOnFailedUpdate m = handleS9ErrT $ do + updateFailed <- getsYesod appIsUpdateFailed >>= liftIO . readIORef + case updateFailed of + Just e -> throwE e + Nothing -> lift m diff --git a/agent/src/Handler/V0.hs b/agent/src/Handler/V0.hs new file mode 100644 index 000000000..c2b010cc5 --- /dev/null +++ b/agent/src/Handler/V0.hs @@ -0,0 +1,120 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +module Handler.V0 where + +import Startlude hiding ( runReader ) + +import Control.Carrier.Lift ( runM ) +import Data.Aeson +import Data.IORef +import qualified Data.Text as T +import Database.Persist +import Yesod.Core.Handler +import Yesod.Persist.Core +import Yesod.Core.Json + +import Constants +import Daemon.ZeroConf +import Foundation +import Handler.Types.V0.Specs +import Handler.Types.V0.Ssh +import Handler.Types.V0.Base +import Handler.Types.V0.Wifi +import Lib.Error +import Lib.External.Metrics.Df +import Lib.External.Specs.CPU +import Lib.External.Specs.Memory +import qualified Lib.External.WpaSupplicant as WpaSupplicant +import Lib.Notifications +import Lib.SystemPaths +import Lib.Ssh +import Lib.Tor +import Lib.Types.Core +import Model +import Settings +import Util.Function + + +getServerR :: Handler (JsonEncoding ServerRes) +getServerR = handleS9ErrT $ do + settings <- getsYesod appSettings + now <- liftIO getCurrentTime + isUpdating <- getsYesod appIsUpdating >>= liftIO . readIORef + + let status = if isJust isUpdating then Nothing else Just Running + + notifs <- case isUpdating of + Nothing -> lift . runDB $ do + notif <- selectList [NotificationArchivedAt ==. Nothing] [Desc NotificationCreatedAt] + void . archive . fmap entityKey $ notif + pure notif + Just _ -> pure [] + + alternativeRegistryUrl <- runM $ injectFilesystemBaseFromContext settings $ readSystemPath altRegistryUrlPath + name <- runM $ injectFilesystemBaseFromContext settings $ readSystemPath serverNamePath + ssh <- readFromPath settings sshKeysFilePath >>= parseSshKeys + wifi <- WpaSupplicant.runWlan0 $ liftA2 WifiList WpaSupplicant.getCurrentNetwork WpaSupplicant.listNetworks + specs <- getSpecs settings + let sid = T.drop 7 $ specsNetworkId specs + + jsonEncode ServerRes { serverId = specsNetworkId specs + , serverName = fromMaybe ("Embassy:" <> sid) name + , serverStatus = AppStatusAppMgr <$> status + , serverStatusAt = now + , serverVersionInstalled = agentVersion + , serverNotifications = notifs + , serverWifi = wifi + , serverSsh = ssh + , serverAlternativeRegistryUrl = alternativeRegistryUrl + , serverSpecs = specs + } + where + parseSshKeys :: Text -> S9ErrT Handler [SshKeyFingerprint] + parseSshKeys keysContent = do + let keys = lines . T.strip $ keysContent + case traverse fingerprint keys of + Left e -> throwE $ InvalidSshKeyE (toS e) + Right as -> pure $ uncurry3 SshKeyFingerprint <$> as + +getSpecs :: MonadIO m => AppSettings -> S9ErrT m SpecsRes +getSpecs settings = do + specsCPU <- liftIO getCpuInfo + specsMem <- liftIO getMem + specsDisk <- fmap show . metricDiskSize <$> getDfMetrics + specsNetworkId <- runM $ injectFilesystemBaseFromContext settings getStart9AgentHostname + specsTorAddress <- runM $ injectFilesystemBaseFromContext settings getAgentHiddenServiceUrl + + let specsAgentVersion = agentVersion + pure $ SpecsRes { .. } + +readFromPath :: MonadIO m => AppSettings -> SystemPath -> S9ErrT m Text +readFromPath settings sp = runM (injectFilesystemBaseFromContext settings (readSystemPath sp)) >>= \case + Nothing -> throwE $ MissingFileE sp + Just res -> pure res + +--------------------- UPDATES TO SERVER ------------------------- + +newtype PatchReq = PatchReq { patchValue :: Text } deriving(Eq, Show) +instance FromJSON PatchReq where + parseJSON = withObject "Patch Request" $ \o -> PatchReq <$> o .: "value" + +newtype NullablePatchReq = NullablePatchReq { mpatchValue :: Maybe Text } deriving(Eq, Show) +instance FromJSON NullablePatchReq where + parseJSON = withObject "Nullable Patch Request" $ \o -> NullablePatchReq <$> o .:? "value" + +patchNameR :: Handler () +patchNameR = patchFile serverNamePath + +patchFile :: SystemPath -> Handler () +patchFile path = do + settings <- getsYesod appSettings + PatchReq val <- requireCheckJsonBody + runM $ injectFilesystemBaseFromContext settings $ writeSystemPath path val + +patchNullableFile :: SystemPath -> Handler () +patchNullableFile path = do + settings <- getsYesod appSettings + NullablePatchReq mVal <- requireCheckJsonBody + runM $ injectFilesystemBaseFromContext settings $ case mVal of + Just val -> writeSystemPath path $ T.strip val + Nothing -> deleteSystemPath path diff --git a/agent/src/Handler/Wifi.hs b/agent/src/Handler/Wifi.hs new file mode 100644 index 000000000..0b973ca87 --- /dev/null +++ b/agent/src/Handler/Wifi.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +module Handler.Wifi where + +import Startlude + +import Data.String.Interpolate.IsString +import qualified Data.Text as T +import Network.HTTP.Types +import Yesod.Core + +import Constants +import Foundation +import Handler.Types.V0.Wifi +import Lib.Error +import qualified Lib.External.WpaSupplicant as WpaSupplicant + +getWifiR :: Handler WifiList +getWifiR = WpaSupplicant.runWlan0 $ liftA2 WifiList WpaSupplicant.getCurrentNetwork WpaSupplicant.listNetworks + +postWifiR :: Handler () +postWifiR = handleS9ErrT $ do + AddWifiReq { addWifiSsid, addWifiPassword, addWifiCountry, skipConnect } <- requireCheckJsonBody + unless (T.all isAscii addWifiSsid) $ throwE InvalidSsidE + unless (T.all isAscii addWifiPassword) $ throwE InvalidPskE + + _ <- liftIO . forkIO . WpaSupplicant.runWlan0 $ do + lift $ withAgentVersionLog_ [i|Adding new WiFi network: '#{addWifiSsid}'|] + WpaSupplicant.addNetwork addWifiSsid addWifiPassword addWifiCountry + unless skipConnect $ do + mCurrent <- WpaSupplicant.getCurrentNetwork + connected <- WpaSupplicant.selectNetwork addWifiSsid addWifiCountry + unless connected do + lift $ withAgentVersionLog_ [i|Failed to add new WiFi network: '#{addWifiSsid}'|] + WpaSupplicant.removeNetwork addWifiSsid + case mCurrent of + Nothing -> pure () + Just current -> void $ WpaSupplicant.selectNetwork current addWifiSsid + sendResponseStatus status200 () + + +postWifiBySsidR :: Text -> Handler () +postWifiBySsidR ssid = handleS9ErrT $ do + unless (T.all isAscii ssid) $ throwE InvalidSsidE + + -- TODO: Front end never sends this on switching between networks. This means that we can only + -- switch to US networks. + country <- fromMaybe "US" <$> lookupGetParam "country" + _ <- liftIO . forkIO . WpaSupplicant.runWlan0 $ do + mCurrent <- WpaSupplicant.getCurrentNetwork + connected <- WpaSupplicant.selectNetwork ssid country + if connected + then lift $ withAgentVersionLog_ [i|Successfully connected to WiFi: #{ssid}|] + else do + lift $ withAgentVersionLog_ [i|Failed to add new WiFi network: '#{ssid}'|] + case mCurrent of + Nothing -> lift $ withAgentVersionLog_ [i|No WiFi to revert to!|] + Just current -> void $ WpaSupplicant.selectNetwork current country + sendResponseStatus status200 () + +deleteWifiBySsidR :: Text -> Handler () +deleteWifiBySsidR ssid = handleS9ErrT $ do + unless (T.all isAscii ssid) $ throwE InvalidSsidE + WpaSupplicant.runWlan0 $ do + current <- WpaSupplicant.getCurrentNetwork + case current of + Nothing -> deleteIt + Just ssid' -> if ssid == ssid' + then do + eth0 <- WpaSupplicant.isConnectedToEthernet + if eth0 + then deleteIt + else lift $ throwE WifiOrphaningE + else deleteIt + where deleteIt = void $ WpaSupplicant.removeNetwork ssid diff --git a/agent/src/Lib/Algebra/Domain/AppMgr.hs b/agent/src/Lib/Algebra/Domain/AppMgr.hs new file mode 100644 index 000000000..445d596e5 --- /dev/null +++ b/agent/src/Lib/Algebra/Domain/AppMgr.hs @@ -0,0 +1,469 @@ +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} -- because of my sheer laziness in dealing with conditional data +{-# OPTIONS_GHC -fno-show-valid-hole-fits #-} -- to not make dev'ing this module cripplingly slow +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneKindSignatures #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE NoMonomorphismRestriction #-} +module Lib.Algebra.Domain.AppMgr + ( module Lib.Algebra.Domain.AppMgr + , module Lib.Algebra.Domain.AppMgr.Types + , module Lib.Algebra.Domain.AppMgr.TH + ) +where + +import Startlude + +import Control.Algebra +import Control.Effect.Error +import Control.Effect.TH +import Data.Aeson +import Data.Aeson.Types ( Parser ) +import qualified Data.HashMap.Strict as HM +import Data.Singletons.Prelude hiding ( Error ) +import Data.Singletons.Prelude.Either +import qualified Data.String as String +import Exinst + +import Lib.Algebra.Domain.AppMgr.Types +import Lib.Algebra.Domain.AppMgr.TH +import Lib.Error +import Lib.External.AppManifest +import Lib.TyFam.ConditionalData +import Lib.Types.Core ( AppId(..) + , AppContainerStatus(..) + ) +import Lib.Types.NetAddress +import Lib.Types.Emver +import Control.Monad.Trans.Class ( MonadTrans ) +import qualified Data.ByteString.Lazy as LBS +import System.Process.Typed +import Data.String.Interpolate.IsString + ( i ) +import Control.Monad.Base ( MonadBase(..) ) +import Control.Monad.Fail ( MonadFail(fail) ) +import Control.Monad.Trans.Resource ( MonadResource(..) ) +import Control.Monad.Trans.Control ( defaultLiftBaseWith + , defaultRestoreM + , MonadTransControl(..) + , MonadBaseControl(..) + ) +import qualified Data.ByteString.Char8 as C8 + + +type InfoRes :: Either OnlyInfoFlag [IncludeInfoFlag] -> Type +data InfoRes a = InfoRes + { infoResTitle :: Include (IsRight a) Text + , infoResVersion :: Include (IsRight a) Version + , infoResTorAddress :: Include (IsRight a) (Maybe TorAddress) + , infoResIsConfigured :: Include (IsRight a) Bool + , infoResIsRecoverable :: Include (IsRight a) Bool + , infoResNeedsRestart :: Include (IsRight a) Bool + , infoResConfig :: Include (Either_ (DefaultEqSym1 'OnlyConfig) (ElemSym1 'IncludeConfig) a) Value + , infoResDependencies + :: Include + (Either_ (DefaultEqSym1 'OnlyDependencies) (ElemSym1 'IncludeDependencies) a) + (HM.HashMap AppId DependencyInfo) + , infoResManifest + :: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) (Some1 AppManifest) + , infoResStatus :: Include (Either_ (DefaultEqSym1 'OnlyStatus) (ElemSym1 'IncludeStatus) a) AppContainerStatus + } +instance SingI (a :: Either OnlyInfoFlag [IncludeInfoFlag]) => FromJSON (InfoRes a) where + parseJSON = withObject "AppMgr Info/List Response" $ \o -> do + let recurse :: forall (a :: [IncludeInfoFlag]) . SingI a => Value -> Parser (InfoRes ( 'Right a)) + recurse = parseJSON @(InfoRes ( 'Right a)) + let infoResConfig = () + let infoResDependencies = () + let infoResManifest = () + let infoResStatus = () + case sing @a of + SLeft f -> do + let infoResTitle = () + let infoResVersion = () + let infoResTorAddress = () + let infoResIsConfigured = () + let infoResIsRecoverable = () + let infoResNeedsRestart = () + case f of + SOnlyConfig -> let infoResConfig = (Object o) in pure InfoRes { .. } + SOnlyDependencies -> parseJSON (Object o) >>= \infoResDependencies -> pure InfoRes { .. } + SOnlyManifest -> parseJSON (Object o) >>= \infoResManifest -> pure InfoRes { .. } + SOnlyStatus -> o .: "status" >>= \infoResStatus -> pure InfoRes { .. } + SRight ls -> do + infoResTitle <- o .: "title" + infoResVersion <- o .: "version" + infoResTorAddress <- TorAddress <<$>> o .: "tor-address" + infoResIsConfigured <- o .: "configured" + infoResIsRecoverable <- o .:? "recoverable" .!= False + infoResNeedsRestart <- o .:? "needs-restart" .!= False + let base = (InfoRes { .. } :: InfoRes ( 'Right '[])) + case ls of + SNil -> pure base + SCons SIncludeConfig (rest :: Sing b) -> do + InfoRes {..} <- withSingI rest $ recurse @b (Object o) + infoResConfig <- o .: "config" + pure InfoRes { .. } + SCons SIncludeDependencies (rest :: Sing b) -> do + InfoRes {..} <- withSingI rest $ recurse @b (Object o) + infoResDependencies <- o .: "dependencies" + pure InfoRes { .. } + SCons SIncludeManifest (rest :: Sing b) -> do + InfoRes {..} <- withSingI rest $ recurse @b (Object o) + infoResManifest <- o .: "manifest" + pure InfoRes { .. } + SCons SIncludeStatus (rest :: Sing b) -> do + InfoRes {..} <- withSingI rest $ recurse @b (Object o) + infoResStatus <- o .: "status" + pure InfoRes { .. } + +data DependencyInfo = DependencyInfo + { dependencyInfoVersionSpec :: VersionRange + , dependencyInfoReasonOptional :: Maybe Text + , dependencyInfoDescription :: Maybe Text + , dependencyInfoConfigRules :: [ConfigRule] + , dependencyInfoRequired :: Bool + , dependencyInfoError :: Maybe DependencyViolation + } + deriving (Eq, Show) +instance FromJSON DependencyInfo where + parseJSON = withObject "AppMgr DependencyInfo" $ \o -> do + dependencyInfoVersionSpec <- o .: "version" + dependencyInfoReasonOptional <- o .: "optional" + dependencyInfoDescription <- o .: "description" + dependencyInfoConfigRules <- o .: "config" + dependencyInfoRequired <- o .: "required" + dependencyInfoError <- o .:? "error" + pure DependencyInfo { .. } + +data ConfigRule = ConfigRule + { configRuleRule :: Text + , configRuleDescription :: Text + , configRuleSuggestions :: [ConfigRuleSuggestion] + } + deriving (Eq, Show) +instance FromJSON ConfigRule where + parseJSON = withObject "AppMgr Config Rule" $ \o -> do + configRuleRule <- o .: "rule" + configRuleDescription <- o .: "description" + configRuleSuggestions <- o .: "suggestions" + pure ConfigRule { .. } +data ConfigRuleSuggestion + = SuggestionPush Text Value + | SuggestionSet Text Target + | SuggestionDelete Text + deriving (Eq, Show) +instance FromJSON ConfigRuleSuggestion where + parseJSON = withObject "AppMgr ConfigRule Suggestion" $ \o -> do + let push = do + o' <- o .: "PUSH" + t <- o' .: "to" + v <- o' .: "value" + pure $ SuggestionPush t v + let set = do + o' <- o .: "SET" + v <- o' .: "var" + t <- parseJSON (Object o') + pure $ SuggestionSet v t + let delete = SuggestionDelete <$> o .: "DELETE" + push <|> set <|> delete + +data Target + = To Text + | ToValue Value + | ToEntropy Text Word16 + deriving (Eq, Show) +instance FromJSON Target where + parseJSON = withObject "Suggestion SET Target" $ \o -> do + (To <$> o .: "to") <|> (ToValue <$> o .: "to-value") <|> do + o' <- o .: "to-entropy" + ToEntropy <$> o' .: "charset" <*> o' .: "len" + +data DependencyError + = Violation DependencyViolation + | PointerUpdateError Text + | Other Text + deriving (Eq, Show) +instance FromJSON DependencyError where + parseJSON v = (Violation <$> parseJSON v) <|> case v of + Object o -> (PointerUpdateError <$> o .: "pointer-update-error") <|> (Other <$> o .: "other") + other -> fail $ "Invalid DependencyError. Expected Object, got " <> (show other) + +data DependencyViolation + = NotInstalled + | NotRunning + | InvalidVersion VersionRange Version + | UnsatisfiedConfig [Text] + deriving (Eq, Show) +instance FromJSON DependencyViolation where + parseJSON (String "not-installed") = pure NotInstalled + parseJSON (String "not-running" ) = pure NotRunning + parseJSON (Object o) = + let version = do + o' <- o .: "incorrect-version" + s <- o' .: "expected" + v <- o' .: "received" + pure $ InvalidVersion s v + config = UnsatisfiedConfig <$> o .: "config-unsatisfied" + in version <|> config + parseJSON other = fail $ "Invalid Dependency Violation" <> show other + +data AutoconfigureRes = AutoconfigureRes + { autoconfigureConfigRes :: ConfigureRes + , autoconfigureChanged :: HM.HashMap AppId Value + } +instance FromJSON AutoconfigureRes where + parseJSON = withObject "AppMgr AutoconfigureRes" $ \o -> do + autoconfigureConfigRes <- parseJSON (Object o) + autoconfigureChanged <- o .: "changed" + pure AutoconfigureRes { .. } + +data ConfigureRes = ConfigureRes + { configureResNeedsRestart :: [AppId] + , configureResStopped :: HM.HashMap AppId (AppId, DependencyError) -- TODO: Consider making this nested hashmaps + } + deriving Eq +instance FromJSON ConfigureRes where + parseJSON = withObject "AppMgr ConfigureRes" $ \o -> do + configureResNeedsRestart <- o .: "needs-restart" + configureResStopped' <- o .: "stopped" + configureResStopped <- for + configureResStopped' + \v -> do + depId <- v .: "dependency" + depError <- v .: "error" + pure (depId, depError) + pure ConfigureRes { .. } + +newtype BreakageMap = BreakageMap { unBreakageMap :: HM.HashMap AppId (AppId, DependencyError) } +instance FromJSON BreakageMap where + parseJSON = withObject "Breakage Map" $ \o -> do + fmap (BreakageMap . HM.fromList) $ for (HM.toList o) $ \(k, v) -> do + case v of + Object v' -> do + depId <- v' .: "dependency" + depError <- v' .: "error" + pure (AppId k, (depId, depError)) + otherwise -> fail $ "Expected Breakage Object, got" <> show otherwise + +data AppMgr (m :: Type -> Type) k where + -- Backup ::_ + CheckDependencies ::LocalOnly -> AppId -> Maybe VersionRange -> AppMgr m (HM.HashMap AppId DependencyInfo) + Configure ::DryRun -> AppId -> Maybe Value -> AppMgr m ConfigureRes + Autoconfigure ::DryRun -> AppId -> AppId -> AppMgr m AutoconfigureRes + -- Disks ::_ + Info ::Sing (flags :: Either OnlyInfoFlag [IncludeInfoFlag]) -> AppId -> AppMgr m (Maybe (InfoRes flags)) + InfoRaw ::OnlyInfoFlag -> AppId -> AppMgr m (Maybe Text) + -- Inspect ::_ + Install ::NoCache -> AppId -> Maybe VersionRange -> AppMgr m () + Instructions ::AppId -> AppMgr m (Maybe Text) + List ::Sing ('Right (flags :: [IncludeInfoFlag])) -> AppMgr m (HM.HashMap AppId (InfoRes ('Right flags))) + -- Logs ::_ + -- Notifications ::_ + -- Pack ::_ + Remove ::Either DryRun Purge -> AppId -> AppMgr m BreakageMap + Restart ::AppId -> AppMgr m () + -- SelfUpdate ::_ + -- Semver ::_ + Start ::AppId -> AppMgr m () + Stop ::DryRun -> AppId -> AppMgr m BreakageMap + -- Tor ::_ + Update ::DryRun -> AppId -> Maybe VersionRange -> AppMgr m BreakageMap + -- Verify ::_ +makeSmartConstructors ''AppMgr + +newtype AppMgrCliC m a = AppMgrCliC { runAppMgrCliC :: m a } + deriving newtype (Functor, Applicative, Monad, MonadIO) +instance MonadTrans AppMgrCliC where + lift = AppMgrCliC +instance MonadResource m => MonadResource (AppMgrCliC m) where + liftResourceT = lift . liftResourceT +instance MonadBase IO m => MonadBase IO (AppMgrCliC m) where + liftBase = AppMgrCliC . liftBase +instance MonadTransControl AppMgrCliC where + type StT AppMgrCliC a = a + liftWith f = AppMgrCliC $ f $ runAppMgrCliC + restoreT = AppMgrCliC +instance MonadBaseControl IO m => MonadBaseControl IO (AppMgrCliC m) where + type StM (AppMgrCliC m) a = StM m a + liftBaseWith = defaultLiftBaseWith + restoreM = defaultRestoreM + +instance (Has (Error S9Error) sig m, Algebra sig m, MonadIO m) => Algebra (AppMgr :+: sig) (AppMgrCliC m) where + alg hdl sig ctx = case sig of + (L (CheckDependencies (LocalOnly b) appId version)) -> do + let local = if b then ("--local-only" :) else id + args = "check-dependencies" : local [versionSpec version (show appId), "--json"] + (ec, out) <- readProcessInheritStderr "appmgr" args "" + res <- case ec of + ExitSuccess -> case eitherDecodeStrict out of + Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e + Right x -> pure x + ExitFailure 6 -> throwError $ NotFoundE "appId@version" (versionSpec version (show appId)) + ExitFailure n -> throwError $ AppMgrE "check-dependencies" n + pure $ ctx $> res + (L (Configure (DryRun b) appId cfg)) -> do + let dryrun = if b then ("--dry-run" :) else id + let input = case cfg of + Nothing -> "" + Just x -> LBS.toStrict $ encode x + let args = "configure" : dryrun [show appId, "--json", "--stdin"] + (ec, out, e) <- readProcessWithExitCode' "appmgr" args input + res <- case ec of + ExitSuccess -> case eitherDecodeStrict out of + Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e + Right x -> pure x + ExitFailure 4 -> throwError $ (AppMgrInvalidConfigE . decodeUtf8) e -- doesn't match spec + ExitFailure 5 -> throwError $ (AppMgrInvalidConfigE . decodeUtf8) e -- doesn't match rules + ExitFailure n -> throwError $ AppMgrE "configure" n + pure $ ctx $> res + (L (Autoconfigure (DryRun dry) dependent dependency)) -> do + let flags = (if dry then ("--dry-run" :) else id) . ("--json" :) + let args = "autoconfigure-dependency" : flags [show dependent, show dependency] + (ec, out) <- readProcessInheritStderr "appmgr" args "" + res <- case ec of + ExitSuccess -> case eitherDecodeStrict out of + Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e + Right a -> pure a + ExitFailure n -> throwError $ AppMgrE "autoconfigure-dependency" n + pure $ ctx $> res + (L (Info fs appId)) -> do + let args = case fromSing fs of + Left o -> ["info", genExclusiveFlag o, show appId, "--json"] + Right ls -> "info" : ((genInclusiveFlag <$> ls) <> [show appId, "--json"]) + (ec, out) <- readProcessInheritStderr "appmgr" args "" + res <- case ec of + ExitSuccess -> case withSingI fs $ eitherDecodeStrict out of + Left e -> throwError $ AppMgrParseE (show args) (decodeUtf8 out) e + Right x -> pure $ Just x + ExitFailure 6 -> pure Nothing + ExitFailure n -> throwError $ AppMgrE "info" n + pure $ ctx $> res + (L (InfoRaw f appId)) -> do + let args = ["info", genExclusiveFlag f, show appId, "--json"] + (ec, out) <- readProcessInheritStderr "appmgr" args "" + res <- case ec of + ExitSuccess -> pure (Just $ decodeUtf8 out) + ExitFailure 6 -> pure Nothing + ExitFailure n -> throwError $ AppMgrE "info (raw)" n + pure $ ctx $> res + (L (Install (NoCache b) appId version)) -> do + let nocache = if b then ("--no-cache" :) else id + let versionSpec :: (IsString a, Semigroup a, ConvertText String a) => a -> a + versionSpec = case version of + Nothing -> id + Just x -> (<> [i|@#{x}|]) + let args = "install" : nocache [versionSpec (show appId)] + (ec, _) <- readProcessInheritStderr "appmgr" args "" + case ec of + ExitSuccess -> pure ctx + ExitFailure 6 -> throwError $ NotFoundE "appId" (show appId) + ExitFailure n -> throwError $ AppMgrE "install" n + (L (Instructions appId)) -> do + (ec, out) <- readProcessInheritStderr "appmgr" ["instructions", show appId] "" + case ec of + ExitSuccess -> pure $ ctx $> Just (decodeUtf8 out) + ExitFailure 6 -> pure $ ctx $> Nothing + ExitFailure n -> throwError $ AppMgrE "instructions" n + (L (List (SRight flags))) -> do + let renderedFlags = (genInclusiveFlag <$> fromSing flags) <> ["--json"] + let args = "list" : renderedFlags + (ec, out) <- readProcessInheritStderr "appmgr" args "" + res <- case ec of + ExitSuccess -> case withSingI flags $ eitherDecodeStrict out of + Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e + Right x -> pure x + ExitFailure n -> throwError $ AppMgrE "list" n + pure $ ctx $> res + (L (Remove dryorpurge appId)) -> do + let args = "remove" : case dryorpurge of + Left (DryRun True) -> ["--dry-run", show appId, "--json"] + Right (Purge True) -> ["--purge", show appId, "--json"] + _ -> [show appId] + (ec, out) <- readProcessInheritStderr "appmgr" args "" + res <- case ec of + ExitSuccess -> case eitherDecodeStrict out of + Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e + Right x -> pure x + ExitFailure 6 -> throwError $ NotFoundE "appId" (show appId) + ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n + pure $ ctx $> res + (L (Restart appId)) -> do + (ec, _) <- readProcessInheritStderr "appmgr" ["restart", show appId] "" + case ec of + ExitSuccess -> pure ctx + ExitFailure 6 -> throwError $ NotFoundE "appId" (show appId) + ExitFailure n -> throwError $ AppMgrE "restart" n + (L (Start appId)) -> do + (ec, _) <- readProcessInheritStderr "appmgr" ["start", show appId] "" + case ec of + ExitSuccess -> pure ctx + ExitFailure 6 -> throwError $ NotFoundE "appId" (show appId) + ExitFailure n -> throwError $ AppMgrE "start" n + (L (Stop (DryRun dry) appId)) -> do + let args = "stop" : (if dry then ("--dry-run" :) else id) [show appId, "--json"] + (ec, out) <- readProcessInheritStderr "appmgr" args "" + case ec of + ExitSuccess -> case eitherDecodeStrict out of + Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e + Right x -> pure $ ctx $> x + ExitFailure 6 -> throwError $ NotFoundE "appId" (show appId) + ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n + (L (Update (DryRun dry) appId version)) -> do + let args = "update" : (if dry then ("--dry-run" :) else id) [versionSpec version (show appId), "--json"] + (ec, out) <- readProcessInheritStderr "appmgr" args "" + case ec of + ExitSuccess -> + let output = if not dry then fromMaybe "" $ lastMay (C8.lines out) else out + in case eitherDecodeStrict output of + Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e + Right x -> pure $ ctx $> x + ExitFailure 6 -> + throwError $ NotFoundE "appId@version" ([i|#{appId}#{maybe "" (('@':) . show) version}|]) + ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n + R other -> AppMgrCliC $ alg (runAppMgrCliC . hdl) other ctx + where + versionSpec :: (IsString a, Semigroup a, ConvertText String a) => Maybe VersionRange -> a -> a + versionSpec v = case v of + Nothing -> id + Just x -> (<> [i|@#{x}|]) + {-# INLINE alg #-} + +genInclusiveFlag :: IncludeInfoFlag -> String +genInclusiveFlag = \case + IncludeConfig -> "-c" + IncludeDependencies -> "-d" + IncludeManifest -> "-m" + IncludeStatus -> "-s" + +genExclusiveFlag :: OnlyInfoFlag -> String +genExclusiveFlag = \case + OnlyConfig -> "-C" + OnlyDependencies -> "-D" + OnlyManifest -> "-M" + OnlyStatus -> "-S" + +readProcessInheritStderr :: MonadIO m => String -> [String] -> ByteString -> m (ExitCode, ByteString) +readProcessInheritStderr a b c = liftIO $ do + let pc = + setStdin (byteStringInput $ LBS.fromStrict c) + $ setStderr inherit + $ setEnvInherit + $ setStdout byteStringOutput + $ (System.Process.Typed.proc a b) + withProcessWait pc + $ \process -> atomically $ liftA2 (,) (waitExitCodeSTM process) (fmap LBS.toStrict $ getStdout process) + +readProcessWithExitCode' :: MonadIO m => String -> [String] -> ByteString -> m (ExitCode, ByteString, ByteString) +readProcessWithExitCode' a b c = liftIO $ do + let pc = + setStdin (byteStringInput $ LBS.fromStrict c) + $ setStderr byteStringOutput + $ setEnvInherit + $ setStdout byteStringOutput + $ (System.Process.Typed.proc a b) + withProcessWait pc $ \process -> atomically $ liftA3 (,,) + (waitExitCodeSTM process) + (fmap LBS.toStrict $ getStdout process) + (fmap LBS.toStrict $ getStderr process) diff --git a/agent/src/Lib/Algebra/Domain/AppMgr/TH.hs b/agent/src/Lib/Algebra/Domain/AppMgr/TH.hs new file mode 100644 index 000000000..bf516c54d --- /dev/null +++ b/agent/src/Lib/Algebra/Domain/AppMgr/TH.hs @@ -0,0 +1,43 @@ +{-# LANGUAGE TemplateHaskell #-} +module Lib.Algebra.Domain.AppMgr.TH where + +import Startlude + +import Data.Singletons +import Data.String +import Language.Haskell.TH.Syntax +import Language.Haskell.TH.Quote ( QuasiQuoter(..) ) + +import Lib.Algebra.Domain.AppMgr.Types + +flags :: QuasiQuoter +flags = QuasiQuoter + { quoteType = \s -> + let + w = Data.String.words s + additive [] = Just [] + additive (f : fs) = case f of + "-s" -> ('IncludeStatus :) <$> additive fs + "-c" -> ('IncludeConfig :) <$> additive fs + "-d" -> ('IncludeDependencies :) <$> additive fs + "-m" -> ('IncludeManifest :) <$> additive fs + _ -> Nothing + exclusive [f] = case f of + "-S" -> Just 'OnlyStatus + "-C" -> Just 'OnlyConfig + "-D" -> Just 'OnlyDependencies + "-M" -> Just 'OnlyManifest + _ -> Nothing + exclusive _ = Nothing + typ = case eitherA (exclusive w) (additive w) of + Nothing -> panic $ "Invalid Flags: '" <> toS s <> "'" + Just (Left o ) -> pure $ AppT (PromotedT 'Left) (PromotedT $ o) + Just (Right ls) -> pure $ AppT + (PromotedT 'Right) + (foldr (\f fs -> AppT (AppT PromotedConsT . PromotedT $ f) fs) PromotedNilT ls) + in + typ + , quoteExp = \s -> AppTypeE (VarE 'sing) <$> quoteType flags s + , quotePat = panic "appmgr 'flags' cannot be used in patterns" + , quoteDec = panic "appmgr 'flags' cannot be used in declarations" + } diff --git a/agent/src/Lib/Algebra/Domain/AppMgr/Types.hs b/agent/src/Lib/Algebra/Domain/AppMgr/Types.hs new file mode 100644 index 000000000..6947edb92 --- /dev/null +++ b/agent/src/Lib/Algebra/Domain/AppMgr/Types.hs @@ -0,0 +1,29 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE UndecidableInstances #-} +module Lib.Algebra.Domain.AppMgr.Types where + +import Startlude + +import Data.Singletons.TH + +newtype LocalOnly = LocalOnly { unLocalOnly :: Bool } +newtype NoCache = NoCache { unNoCache :: Bool } +newtype Purge = Purge { unPurge :: Bool } +newtype DryRun = DryRun { unDryRun :: Bool } + +$(singletons [d| + data IncludeInfoFlag + = IncludeConfig + | IncludeDependencies + | IncludeManifest + | IncludeStatus deriving (Eq, Show) |]) + +$(singletons [d| + data OnlyInfoFlag + = OnlyConfig + | OnlyDependencies + | OnlyManifest + | OnlyStatus deriving (Eq, Show) |]) + diff --git a/agent/src/Lib/Algebra/State/RegistryUrl.hs b/agent/src/Lib/Algebra/State/RegistryUrl.hs new file mode 100644 index 000000000..683b75227 --- /dev/null +++ b/agent/src/Lib/Algebra/State/RegistryUrl.hs @@ -0,0 +1,84 @@ +{-# LANGUAGE UndecidableInstances #-} +module Lib.Algebra.State.RegistryUrl where + +import Startlude hiding ( State + , get + , put + ) + +import Control.Algebra +import Control.Effect.State +import Control.Monad.Catch +import Control.Monad.Trans.Class +import Control.Monad.Trans.Resource +import qualified Data.Text as T + +import Lib.SystemPaths +import Lib.Types.Url +import Control.Monad.Trans.Control +import Control.Monad.Base + +data RegistryUrl (m :: Type -> Type) k where + GetRegistryUrl ::RegistryUrl m (Maybe Url) + PutRegistryUrl ::Url -> RegistryUrl m () + +getRegistryUrl :: Has RegistryUrl sig m => m (Maybe Url) +getRegistryUrl = send GetRegistryUrl + +putRegistryUrl :: Has RegistryUrl sig m => Url -> m () +putRegistryUrl = send . PutRegistryUrl + + +newtype RegistryUrlIOC m a = RegistryUrlIOC { runRegistryUrlIOC :: m a } + deriving newtype (Functor, Applicative, Monad, MonadIO) + +instance MonadTrans RegistryUrlIOC where + lift = RegistryUrlIOC + +instance MonadThrow m => MonadThrow (RegistryUrlIOC m) where + throwM = lift . throwM + +instance MonadResource m => MonadResource (RegistryUrlIOC m) where + liftResourceT = lift . liftResourceT + +instance MonadTransControl RegistryUrlIOC where + type StT RegistryUrlIOC a = a + liftWith f = RegistryUrlIOC $ f $ runRegistryUrlIOC + restoreT = RegistryUrlIOC +instance MonadBase IO m => MonadBase IO (RegistryUrlIOC m) where + liftBase = RegistryUrlIOC . liftBase +instance MonadBaseControl IO m => MonadBaseControl IO (RegistryUrlIOC m) where + type StM (RegistryUrlIOC m) a = StM m a + liftBaseWith = defaultLiftBaseWith + restoreM = defaultRestoreM + +-- the semantics of this are currently as follows, url fetches will fail with an empty value if the path does not exist +-- as well as if the url in the file desired does not parse as a url +instance (MonadIO m, Algebra sig m, HasFilesystemBase sig m) => Algebra (RegistryUrl :+: sig) (RegistryUrlIOC m) where + alg hdl sig ctx = case sig of + L GetRegistryUrl -> do + result <- readSystemPath altRegistryUrlPath + case result of + Nothing -> pure $ ctx $> Nothing + Just raw -> + let stripped = T.strip raw + in case parseUrl stripped of + Left _ -> do + putStrLn @Text $ "Could not parse alternate registry url: " <> stripped + pure $ ctx $> Nothing + Right url -> pure $ ctx $> (Just url) + L (PutRegistryUrl url) -> do + writeSystemPath altRegistryUrlPath (show url) + pure ctx + R other -> RegistryUrlIOC $ alg (runRegistryUrlIOC . hdl) other ctx + {-# INLINE alg #-} + + +newtype RegistryUrlStateC m a = RegistryUrlStateC { runRegistryUrlStateC :: m a } + deriving newtype (Functor, Applicative, Monad, MonadIO) +instance (Monad m, Has (State (Maybe Url)) sig m) => Algebra (RegistryUrl :+: sig) (RegistryUrlStateC m) where + alg hdl sig ctx = case sig of + L GetRegistryUrl -> (ctx $>) <$> get + L (PutRegistryUrl url) -> (ctx $>) <$> put (Just url) + R other -> RegistryUrlStateC $ alg (runRegistryUrlStateC . hdl) other ctx + diff --git a/agent/src/Lib/Avahi.hs b/agent/src/Lib/Avahi.hs new file mode 100644 index 000000000..ca74aea65 --- /dev/null +++ b/agent/src/Lib/Avahi.hs @@ -0,0 +1,68 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TypeApplications #-} +module Lib.Avahi where + +import Startlude hiding ( (<.>) ) + +import Data.String.Interpolate.IsString +import qualified Data.Text as T +import System.Directory + +import Lib.Error +import Lib.SystemCtl +import Lib.SystemPaths +import Settings + +avahiConf :: Text -> Text +avahiConf hostname = T.drop 1 $ [i| +[server] +host-name=#{hostname} +domain-name=local +use-ipv4=yes +use-ipv6=no +allow-interfaces=wlan0,eth0 +ratelimit-interval-usec=100000 +ratelimit-burst=1000 + +[wide-area] +enable-wide-area=yes + +[publish] + +[reflector] + +[rlimits] +|] + +data WildcardReplacement = + WildcardsEnabled + | WildcardsDisabled + deriving (Eq, Show) + +serviceConfig :: (WildcardReplacement, Text) -> Text -> Word16 -> Text +serviceConfig (wildcards, name) protocol port = T.drop 1 $ [i| + + + + #{name} + + #{protocol} + #{port} + +|] + +createService :: (MonadReader AppSettings m, MonadIO m) => Text -> (WildcardReplacement, Text) -> Text -> Word16 -> m () +createService title params proto port = do + base <- asks appFilesystemBase + liftIO $ writeFile (toS $ avahiServicePath title `relativeTo` base) $ serviceConfig params proto port + +createDaemonConf :: Text -> IO () +createDaemonConf = writeFile "/etc/avahi/avahi-daemon.conf" . avahiConf + +listServices :: IO [FilePath] +listServices = listDirectory "/etc/avahi/services" + +reload :: IO () +reload = do + ec <- systemCtl RestartService "avahi-daemon" + unless (ec == ExitSuccess) $ throwIO . AvahiE $ "systemctl restart avahi-daemon" <> show ec diff --git a/agent/src/Lib/Background.hs b/agent/src/Lib/Background.hs new file mode 100644 index 000000000..c6fa11e06 --- /dev/null +++ b/agent/src/Lib/Background.hs @@ -0,0 +1,46 @@ +module Lib.Background where + +import Startlude hiding ( mapMaybe ) + +import Data.HashMap.Strict +import Data.Singletons +import Data.Singletons.Decide +import Exinst + +import Lib.Types.Core +import Lib.Types.ServerApp + +type JobMetadata :: AppTmpStatus -> Type +data JobMetadata a where + Install ::StoreApp -> StoreAppVersionInfo -> JobMetadata 'Installing + Backup ::JobMetadata 'CreatingBackup + Restore ::JobMetadata 'RestoringBackup + StopApp ::JobMetadata 'StoppingT + RestartApp ::JobMetadata 'RestartingT + +jobType :: JobMetadata a -> SAppTmpStatus a +jobType = \case + Install _ _ -> SInstalling + Backup -> SCreatingBackup + Restore -> SRestoringBackup + StopApp -> SStoppingT + RestartApp -> SRestartingT + +newtype JobCache = JobCache { unJobCache :: HashMap AppId (Some1 JobMetadata, ThreadId) } + +inspect :: SAppTmpStatus a -> JobCache -> HashMap AppId (JobMetadata a, ThreadId) +inspect stat (JobCache cache) = flip mapMaybe cache $ \(Some1 sa jm, tid) -> case stat %~ sa of + Proved Refl -> Just (jm, tid) + Disproved _ -> Nothing + +statuses :: JobCache -> HashMap AppId AppTmpStatus +statuses (JobCache cache) = some1SingRep . fst <$> cache + +installInfo :: JobMetadata 'Installing -> (StoreApp, StoreAppVersionInfo) +installInfo (Install a b) = (a, b) + +insertJob :: AppId -> JobMetadata a -> ThreadId -> JobCache -> JobCache +insertJob appId jm tid = JobCache . insert appId (withSingI (jobType jm) (some1 jm), tid) . unJobCache + +deleteJob :: AppId -> JobCache -> JobCache +deleteJob appId = JobCache . delete appId . unJobCache diff --git a/agent/src/Lib/ClientManifest.hs b/agent/src/Lib/ClientManifest.hs new file mode 100644 index 000000000..1cc3f6059 --- /dev/null +++ b/agent/src/Lib/ClientManifest.hs @@ -0,0 +1,297 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TupleSections #-} +module Lib.ClientManifest where + +import Startlude hiding ( takeWhile + , toList + ) +import qualified Protolude.Base as P + +import Control.Error.Util +import Control.Monad.Fail +import Data.Aeson +import Data.Attoparsec.Text +import Data.HashMap.Strict +import qualified Data.Map.Strict as Map + ( toList ) +import Data.Singletons.TypeLits +import Data.String.Interpolate.IsString +import qualified Data.Text as T +import qualified Data.Yaml as Yaml +import Exinst +import Network.Mime +import Numeric.Natural +import Streaming.Prelude as Stream + hiding ( show + , for + , toList + , cons + ) +import System.IO ( hClose ) + +import Lib.Error +import Lib.SystemPaths +import Lib.Types.NetAddress +import Lib.Types.Core +import Lib.Types.Emver + +data ClientManifest (n :: Nat) where + V0 ::ClientManifestV0 -> ClientManifest 0 + +deriving instance Show (ClientManifest a) + +instance Dict1 Show ClientManifest where + dict1 sn = case sn of + SNat -> Dict + +data ClientManifestV0 = ClientManifestV0 + { clientManifestV0AppId :: AppId + , clientManifestV0AppVersion :: Version + , clientManifestV0Main :: SystemPath + , clientManifestV0UriRewrites :: HashMap UriPattern LanExp + , clientManifestV0ErrorFiles :: HashMap Int FilePath + , clientManifestV0MimeRules :: MimeMap + , clientManifestV0MimeDefault :: MimeType + } + deriving Show + +data UriPattern = MatchExact Text | MatchPrefix Text + deriving (Eq, Show, Generic, Hashable) +newtype LanExp = LanExp { unLanExp :: (AppId, LanIp -> Text) } +instance Show LanExp where + show (LanExp (AppId appId, f)) = toS . f . LanIp $ "{{" <> appId <> "}}" + +parseUriPattern :: Parser UriPattern +parseUriPattern = do + cons <- char '=' *> pure MatchExact <|> pure MatchPrefix + cons . toS <$> takeWhile1 (not . isSpace) + +parseUriRewrite :: Parser (UriPattern, LanExp) +parseUriRewrite = do + pat <- parseUriPattern + skipSpace + void $ char '-' *> char '>' + skipSpace + tgt <- parseUriTarget + pure (pat, tgt) + +parseUriTarget :: Parser LanExp +parseUriTarget = do + proto <- (string "https" <|> string "http") + opener <- string "://" <* string "{{" + host <- takeWhile1 (not . (== '}')) + closer <- string "}}" *> string ":" + port <- decimal @Word16 + path <- takeWhile1 (not . isSpace) + pure . LanExp $ (AppId host, \ip -> proto <> opener <> unLanIp ip <> closer <> show port <> path) + +instance FromJSON (Some1 ClientManifest) where + parseJSON = withObject "Client Manifest" $ \o -> do + v <- o .: "manifest-version" + case (v :: Natural) of + 0 -> some1 . V0 <$> parseJSON (Object o) + _ -> fail $ "Unsupported Manifest Version: " <> show v + +instance FromJSON ClientManifestV0 where + parseJSON = withObject "Client Manifest V0" $ \o -> do + clientManifestV0AppId <- o .: "app-id" + clientManifestV0AppVersion <- o .: "app-version" + clientManifestV0Main <- relBase <$> o .: "main-is" + clientManifestV0UriRewrites <- fmap fromList $ o .: "uri-rewrites" >>= \rewrites -> do + for (fmap (parseOnly parseUriRewrite) rewrites) $ \case + Right r -> pure r + Left e -> fail $ "Invalid Rewrite Rule: " <> e + clientManifestV0ErrorFiles <- fromMaybe mempty <$> o .: "error-pages" + clientManifestV0MimeRules <- encodeUtf8 <<$>> o .: "mime-types" + clientManifestV0MimeDefault <- encodeUtf8 <$> o .: "mime-default" + pure ClientManifestV0 { .. } + +testClientManifest :: ByteString +testClientManifest = [i| +manifest-version: 0 +app-id: start9-ambassador +app-version: 0.2.0 +main-is: /index.html +uri-rewrites: + - =/api -> http://{{start9-ambassador}}:5959/authenticate + - /api -> http://{{start9-ambassador}}:5959/ +error-pages: + 404: /err404.html +mime-types: + bin: application/octet-stream + json: application/json +mime-default: text/plain +|] + +data NginxSiteConf = NginxSiteConf + { nginxSiteConfAppId :: AppId + , nginxSiteConfAppVersion :: Version + , nginxSiteConfRoot :: SystemPath + , nginxSiteConfListen :: Word16 + , nginxSiteConfServerName :: [Text] + , nginxSiteConfLocations :: [NginxLocation] + , nginxSiteConfIndex :: SystemPath + , nginxSiteConfMimeMappings :: HashMap MimeType [Extension] + , nginxSiteConfErrorPages :: HashMap Int SystemPath + , nginxSiteConfDefaultMime :: MimeType + , nginxSiteConfSsl :: Maybe NginxSsl + } + deriving Show + +data NginxLocation = NginxLocation + { nginxLocationPattern :: UriPattern + , nginxLocationTarget :: Text + } + deriving Show + +data NginxSsl = NginxSsl + { nginxSslKeyPath :: SystemPath + , nginxSslCertPath :: SystemPath + , nginxSslOnlyServerNames :: [Text] + } + deriving Show + +transpileV0ToNginx :: MonadReader (HashMap AppId (TorAddress, LanIp)) m => ClientManifest 0 -> S9ErrT m NginxSiteConf +transpileV0ToNginx (V0 ClientManifestV0 {..}) = do + hm <- ask + let nginxSiteConfAppId = clientManifestV0AppId + let nginxSiteConfAppVersion = clientManifestV0AppVersion + let nginxSiteConfRoot = "/var/www/html" <> relBase (unAppId clientManifestV0AppId) + let nginxSiteConfListen = 80 + nginxSiteConfServerName <- + pure . unTorAddress . fst <$> lookup clientManifestV0AppId hm ?? (EnvironmentValE clientManifestV0AppId) + nginxSiteConfLocations <- for (toList clientManifestV0UriRewrites) $ \(pat, (LanExp (appId, tgt))) -> do + lan <- snd <$> lookup appId hm ?? EnvironmentValE appId + pure $ NginxLocation pat (tgt lan) + let nginxSiteConfIndex = clientManifestV0Main + let nginxSiteConfErrorPages = fmap fromString clientManifestV0ErrorFiles + let nginxSiteConfMimeMappings = + flip execState Data.HashMap.Strict.empty $ for (Map.toList clientManifestV0MimeRules) $ \(ext, mime) -> do + modify (alter (maybe (Just [ext]) (Just . (ext :))) mime) + let nginxSiteConfDefaultMime = clientManifestV0MimeDefault + let nginxSiteConfSsl = Nothing + pure NginxSiteConf { .. } + +-- TODO WRONG, this caching disabled for all uri rewrites +-- this hack is ok for ambassador-ui, but does not generalize +-- we might want to deprecate this means of cachine anyway though +-- see: https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker#cache_then_network +nginxConfGen :: MonadState Int m => NginxSiteConf -> Stream (Of Text) m () +nginxConfGen NginxSiteConf {..} = do + emit "server {" + indent $ do + emit $ "root " <> nginxSiteConfRoot `relativeTo` "/" <> ";" + + case nginxSiteConfSsl of + Nothing -> emit $ "listen " <> show nginxSiteConfListen <> ";" + Just _ -> emit $ "listen " <> show nginxSiteConfListen <> " ssl;" + + emit $ "server_name " <> (T.intercalate " " nginxSiteConfServerName) <> ";" + + case nginxSiteConfSsl of + Nothing -> pure () + Just NginxSsl {..} -> do + emit $ "ssl_certificate " <> (nginxSslCertPath `relativeTo` "/") <> ";" + emit $ "ssl_certificate_key " <> (nginxSslKeyPath `relativeTo` "/") <> ";" + + for_ nginxSiteConfLocations $ \(NginxLocation pat tgt) -> do + case pat of + MatchExact p -> emit $ "location = " <> p <> " {" + MatchPrefix p -> emit $ "location " <> p <> " {" + indent $ do + emit $ "proxy_pass " <> tgt <> ";" + emit $ "proxy_set_header Host $host;" + emit "}" + emit "location = / {" + indent $ do + emit $ "add_header X-Consulate-App-ID " <> (show nginxSiteConfAppId) <> ";" + emit $ "add_header X-Consulate-App-Version " <> (show nginxSiteConfAppVersion) <> ";" + emit $ "add_header Cache-Control private;" + emit $ "expires 86400;" + emit $ "etag on;" + emit $ "index " <> nginxSiteConfIndex `relativeTo` "/" <> ";" + emit "}" + for_ (toList nginxSiteConfErrorPages) $ \(ec, path) -> do + emit $ "error_page " <> show ec <> " " <> (path `relativeTo` "/") <> ";" + emit $ "location = " <> path `relativeTo` "/" <> " {" + indent $ do + emit $ "add_header X-Consulate-App-ID " <> (show nginxSiteConfAppId) <> ";" + emit $ "add_header X-Consulate-App-Version " <> (show nginxSiteConfAppVersion) <> ";" + emit "internal;" + emit "}" + emit "location / {" + indent $ do + emit $ "add_header X-Consulate-App-ID " <> (show nginxSiteConfAppId) <> ";" + emit $ "add_header X-Consulate-App-Version " <> (show nginxSiteConfAppVersion) <> ";" + emit $ "add_header Cache-Control private;" + emit $ "expires 86400;" + emit $ "etag on;" + emit "}" + emit "types {" + indent $ for_ (toList nginxSiteConfMimeMappings) $ \(typ, exts) -> do + emit $ decodeUtf8 typ <> " " <> T.unwords exts <> ";" + emit "}" + emit $ "default_type " <> decodeUtf8 nginxSiteConfDefaultMime <> ";" + emit "}" + case nginxSslOnlyServerNames <$> nginxSiteConfSsl of + Nothing -> pure () + Just [] -> pure () + Just ls -> do + emit "server {" + indent $ do + emit "listen 80;" + emit $ "server_name " <> T.intercalate " " ls <> ";" + emit $ "return 301 https://$host$request_uri;" + emit "}" + where + emit :: MonadState Int m => Text -> Stream (Of Text) m () + emit t = get >>= \n -> yield $ T.replicate n "\t" <> t + indent :: MonadState Int m => m a -> m a + indent m = modify (+ (1 :: Int)) *> m <* modify (subtract (1 :: Int)) + +data NginxSiteConfOverride = NginxSiteConfOverride + { nginxSiteConfOverrideAdditionalServerName :: Text + , nginxSiteConfOverrideListen :: Word16 + , nginxSiteConfOverrideSsl :: Maybe NginxSsl + } +overrideNginx :: NginxSiteConfOverride -> NginxSiteConf -> NginxSiteConf +overrideNginx NginxSiteConfOverride {..} nginxSiteConf = nginxSiteConf + { nginxSiteConfServerName = previousServerNames <> [nginxSiteConfOverrideAdditionalServerName] + , nginxSiteConfListen = nginxSiteConfOverrideListen + , nginxSiteConfSsl = nginxSiteConfOverrideSsl + } + where previousServerNames = nginxSiteConfServerName nginxSiteConf + +-- takes if' app-manifest, converts it to an nginx conf, writes it to of' +transpile :: (MonadReader (HashMap AppId (TorAddress, LanIp)) m, MonadIO m) + => Maybe NginxSiteConfOverride + -> FilePath + -> FilePath + -> m Bool +transpile mOverride if' of' = do + oh <- liftIO $ openFile of' WriteMode + hm <- ask + contents <- liftIO $ toS <$> Startlude.readFile if' + case Yaml.decodeEither' (encodeUtf8 contents) :: Either Yaml.ParseException (Some1 ClientManifest) of + Left e -> do + Startlude.print e + liftIO $ hClose oh + pure False + Right (Some1 _ cm) -> case cm of + cmv0@(V0 _) -> case runExceptT (fmap overrides $ transpileV0ToNginx cmv0) hm of + Left e -> do + Startlude.print e + liftIO $ hClose oh + pure False + Right nsc -> do + flip (evalStateT @_ @Int) 0 $ Stream.toHandle oh $ Stream.toHandle stdout $ Stream.copy + (Stream.map toS $ nginxConfGen nsc) + liftIO $ hClose oh + pure True + where + overrides = case mOverride of + Nothing -> id + Just o -> overrideNginx o + diff --git a/agent/src/Lib/Crypto.hs b/agent/src/Lib/Crypto.hs new file mode 100644 index 000000000..205356e7b --- /dev/null +++ b/agent/src/Lib/Crypto.hs @@ -0,0 +1,53 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE ScopedTypeVariables #-} +module Lib.Crypto where + +import Startlude + +import Control.Arrow +import Crypto.Cipher.AES +import Crypto.Cipher.Types +import Crypto.Error +import Crypto.Hash as Hash +import Crypto.KDF.PBKDF2 +import Crypto.MAC.HMAC +import Crypto.Random +import Data.Maybe +import Data.ByteArray.Sized as BA +import Data.ByteString as BS + +-- expands given key by pbkdf2 +computeHmac :: Text -> Text -> SizedByteArray 16 ByteString -> Digest SHA256 +computeHmac key message salt = hmacGetDigest $ hmac (pbkdf2 salt' key) (encodeUtf8 message) + where salt' = unSizedByteArray salt + +mkAesKey :: SizedByteArray 16 ByteString -> Text -> Maybe AES256 +mkAesKey salt = pbkdf2 salt' >>> cipherInit >>> \case + CryptoPassed k -> Just k + CryptoFailed _ -> Nothing + where salt' = unSizedByteArray salt + +pbkdf2 :: ByteString -> Text -> ByteString +pbkdf2 salt key = fastPBKDF2_SHA256 pbkdf2Parameters (encodeUtf8 key) salt + where pbkdf2Parameters = Parameters 100000 32 -- 32 is the length in *bytes* of the output key + +encryptAes256Ctr :: AES256 -> IV AES256 -> ByteString -> ByteString +encryptAes256Ctr = ctrCombine + +decryptAes256Ctr :: AES256 -> IV AES256 -> ByteString -> ByteString +decryptAes256Ctr = encryptAes256Ctr + +random16 :: MonadIO m => m (SizedByteArray 16 ByteString) +random16 = randomBytes +random8 :: MonadIO m => m (SizedByteArray 8 ByteString) +random8 = randomBytes +random32 :: MonadIO m => m (SizedByteArray 32 ByteString) +random32 = randomBytes + +randomBytes :: forall m n . (MonadIO m, KnownNat n) => m (SizedByteArray n ByteString) +randomBytes = liftIO $ fromJust . sizedByteArray <$> getRandomBytes byteCount + where + casing :: SizedByteArray n ByteString + casing = BA.zero + byteCount = BS.length $ unSizedByteArray casing diff --git a/agent/src/Lib/Database.hs b/agent/src/Lib/Database.hs new file mode 100644 index 000000000..043b05eeb --- /dev/null +++ b/agent/src/Lib/Database.hs @@ -0,0 +1,53 @@ +module Lib.Database where + +import Startlude hiding ( throwIO + , Reader + ) + +import Control.Effect.Reader.Labelled +import Control.Monad.Logger +import Database.Persist.Sql +import System.Directory + +import Constants +import Lib.Migration +import Lib.SystemPaths +import Lib.Types.Emver +import Model +import Util.Function + +------------------------------------------------------------------------------------------------------------------------ +-- Migrations +------------------------------------------------------------------------------------------------------------------------ + +data UpMigrationHistory = UpMigrationHistory (Maybe Version) (Maybe Version) -- previous db version, current db version. + +type Logger = Loc -> LogSource -> LogLevel -> LogStr -> IO () + +ensureCoherentDbVersion :: (HasFilesystemBase sig m, HasLabelled "sqlDatabase" (Reader Text) sig m, MonadIO m) + => ConnectionPool + -> Logger + -> m UpMigrationHistory +ensureCoherentDbVersion pool logFunc = do + db <- dbPath + mDbVersion <- liftIO $ doesFileExist (toS db) >>= \case + True -> runSqlPool getCurrentDbVersion pool -- get db version if db exists + False -> pure Nothing + + liftIO $ case mDbVersion of + Nothing -> initializeDb agentVersion pool logFunc + Just dbVersion -> upMigration pool dbVersion agentVersion + +initializeDb :: Version -> ConnectionPool -> Logger -> IO UpMigrationHistory +initializeDb av = runLoggingT .* runSqlPool $ do + now <- liftIO getCurrentTime + runMigration migrateAll + void . insertEntity $ ExecutedMigration now now av av + pure $ UpMigrationHistory Nothing (Just agentVersion) + +upMigration :: ConnectionPool -> Version -> Version -> IO UpMigrationHistory +upMigration pool dbVersion currentAgentVersion = if dbVersion < currentAgentVersion + then do + ioMigrationDbVersion pool dbVersion currentAgentVersion + pure $ UpMigrationHistory (Just dbVersion) (Just currentAgentVersion) + else pure $ UpMigrationHistory (Just dbVersion) Nothing diff --git a/agent/src/Lib/Error.hs b/agent/src/Lib/Error.hs new file mode 100644 index 000000000..6a687b5b7 --- /dev/null +++ b/agent/src/Lib/Error.hs @@ -0,0 +1,283 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} + +module Lib.Error where + +import Startlude + +import Control.Carrier.Error.Church +import Data.Aeson hiding ( Error ) +import Data.String.Interpolate.IsString +import qualified Data.Yaml as Yaml +import qualified GHC.Show ( Show(..) ) +import Network.HTTP.Types +import System.Process +import Yesod.Core hiding ( ErrorResponse ) + +import Lib.SystemPaths +import Lib.Types.Core +import Lib.Types.Emver + + +type S9ErrT m = ExceptT S9Error m + +data S9Error = + ProductKeyE + | RegistrationE + | NoCompliantAgentE VersionRange + | PersistentE Text + | WifiConnectionE + | AppMgrParseE Text Text String + | AppMgrInvalidConfigE Text + | AppMgrE Text Int + | AvahiE Text + | MetricE Text + | AppMgrVersionE Version VersionRange + | RegistryUnreachableE + | RegistryParseE Text Text + | AppNotInstalledE AppId + | AppStateActionIncompatibleE AppId AppStatus AppAction + | UpdateSelfE UpdateSelfStep Text + | InvalidSshKeyE Text + | InvalidSsidE + | InvalidPskE + | InvalidRequestE Value Text + | NotFoundE Text Text + | UpdateInProgressE + | TemporarilyForbiddenE AppId Text Text + | TorServiceTimeoutE + | NginxSslE Text + | WifiOrphaningE + | NoPasswordExistsE + | HostsParamsE Text + | MissingFileE SystemPath + | ClientCryptographyE Text + | TTLExpirationE Text + | ManifestParseE AppId Yaml.ParseException + | EnvironmentValE AppId + | InternalE Text + | BackupE AppId Text + | BackupPassInvalidE + | OpenSslE Text Int String String +data UpdateSelfStep = + GetLatestCompliantVersion + | GetYoungAgentBinary + | ShutdownWeb + | StartupYoungAgent + | PingYoungAgent ProcessHandle +instance Show S9Error where + show = show . toError + +instance Exception S9Error + +newtype InternalS9Error = InternalS9Error Text deriving (Eq, Show) +instance Exception InternalS9Error + +-- | Redact any sensitive data in this function +toError :: S9Error -> ErrorResponse +toError = \case + ProductKeyE -> ErrorResponse PRODUCT_KEY_ERROR "The product key is invalid" + RegistrationE -> ErrorResponse REGISTRATION_ERROR "The product already has an owner" + NoCompliantAgentE spec -> ErrorResponse AGENT_UPDATE_ERROR [i|No valid agent version for spec #{spec}|] + PersistentE t -> ErrorResponse DATABASE_ERROR t + WifiConnectionE -> ErrorResponse WIFI_ERROR "Could not connect to wifi" + AppMgrInvalidConfigE e -> ErrorResponse APPMGR_CONFIG_ERROR e + AppMgrParseE cmd result e -> + ErrorResponse APPMGR_PARSE_ERROR [i|"appmgr #{cmd}" yielded an unparseable result:#{result}\nError: #{e}|] + AppMgrE cmd code -> ErrorResponse APPMGR_ERROR [i|"appmgr #{cmd}" exited with #{code}|] + AppMgrVersionE av avs -> + ErrorResponse APPMGR_ERROR [i|"appmgr version #{av}" fails to satisfy requisite spec #{avs}|] + AvahiE e -> ErrorResponse AVAHI_ERROR [i|#{e}|] + MetricE m -> ErrorResponse METRICS_ERROR [i|failed to provide metrics: #{m}|] + RegistryUnreachableE -> ErrorResponse REGISTRY_ERROR [i|registry is unreachable|] + RegistryParseE path msg -> ErrorResponse REGISTRY_ERROR [i|registry "#{path}" failed to parse: #{msg}|] + AppNotInstalledE appId -> ErrorResponse APP_NOT_INSTALLED [i|#{appId} is not installed|] + AppStateActionIncompatibleE appId status action -> ErrorResponse APP_ACTION_FORBIDDEN $ case (status, action) of + (AppStatusAppMgr Dead, _) -> [i|#{appId} cannot be #{action}ed because it is dead...contact support?|] + (AppStatusAppMgr Removing, _) -> [i|#{appId} cannot be #{action}ed because it is being removed|] + (AppStatusAppMgr Running, Start) -> [i|#{appId} is already running|] + (AppStatusAppMgr Stopped, Stop) -> [i|#{appId} is already stopped|] + (AppStatusAppMgr Restarting, Start) -> [i|#{appId} is already running|] + (AppStatusAppMgr Running, Stop) -> [i|Running apps should be stoppable, this is a bug, contact support|] + (AppStatusAppMgr Stopped, Start) -> [i|Stopped apps should be startable, this is a bug, contact support|] + (AppStatusAppMgr Restarting, Stop) -> [i|Restarting apps should be stoppable, this is a bug, contact support|] + (AppStatusAppMgr Paused, _) -> [i|Paused is not an externally visible state, this is a bug, contact support|] + (AppStatusTmp NeedsConfig, Start) -> [i|#{appId} cannot be started because it is not configured|] + (AppStatusTmp NeedsConfig, Stop) -> [i|#{appId} is already stopped|] + (AppStatusTmp BrokenDependencies, Start) -> [i|Cannot start service: Dependency Issue|] + (AppStatusTmp _, _) -> [i|Cannot issue control actions to apps in temporary states|] + UpdateSelfE step e -> ErrorResponse SELF_UPDATE_ERROR $ case step of + GetLatestCompliantVersion -> [i|could not find a compliant version for the specification|] + GetYoungAgentBinary -> [i|could not get young agent binary: #{e}|] + ShutdownWeb -> [i|could not shutdown web: #{e}|] + StartupYoungAgent -> [i|could not startup young agent: #{e}|] + PingYoungAgent _ -> [i|could not ping young agent: #{e}|] + InvalidSshKeyE key -> ErrorResponse INVALID_SSH_KEY [i|The ssh key "#{key}" is invalid|] + InvalidSsidE -> ErrorResponse INVALID_SSID [i|The ssid is invalid. Only ASCII characters allowed.|] + InvalidPskE -> ErrorResponse INVALID_SSID [i|The wifi password is invalid. Only ASCII characters allowed.|] + InvalidRequestE val reason -> ErrorResponse INVALID_REQUEST [i|The body #{encode val} is invalid: #{reason}|] + NotFoundE resource val -> ErrorResponse RESOURCE_NOT_FOUND [i|The #{resource} #{val} was not found|] + UpdateInProgressE -> + ErrorResponse UPDATE_IN_PROGRESS [i|Your request could not be completed because your server is updating|] + TemporarilyForbiddenE appId action st -> + ErrorResponse APP_ACTION_FORBIDDEN [i|The #{action} for #{appId} is temporarily forbidden because it is #{st}|] + TorServiceTimeoutE -> + ErrorResponse INTERNAL_ERROR [i|The MeshOS Tor Service could not be started...contact support|] + NginxSslE e -> ErrorResponse INTERNAL_ERROR [i|MeshOS could not be started with SSL #{e}|] + WifiOrphaningE -> ErrorResponse + WIFI_ERROR + [i|You cannot delete the wifi network you are currently connected to unless on ethernet|] + ManifestParseE appId e -> + ErrorResponse INTERNAL_ERROR [i|There was an error inspecting the manifest for #{appId}: #{e}|] + NoPasswordExistsE -> ErrorResponse REGISTRATION_ERROR [i|Unauthorized. No password has been registered|] + MissingFileE sp -> ErrorResponse RESOURCE_NOT_FOUND [i|File not found as #{leaf sp}|] + ClientCryptographyE desc -> ErrorResponse REGISTRATION_ERROR [i|Cryptography failure: #{desc}|] + TTLExpirationE desc -> ErrorResponse REGISTRATION_ERROR [i|TTL Expiration failure: #{desc}|] + EnvironmentValE appId -> ErrorResponse SYNCHRONIZATION_ERROR [i|Could not read environment values for #{appId}|] + HostsParamsE key -> ErrorResponse REGISTRATION_ERROR [i|Missing or invalid parameter #{key}|] + InternalE msg -> ErrorResponse INTERNAL_ERROR msg + BackupE appId reason -> ErrorResponse BACKUP_ERROR [i|Backup failed for #{appId}: #{reason}|] + BackupPassInvalidE -> ErrorResponse BACKUP_ERROR [i|Password provided for backups is invalid|] + OpenSslE cert ec stdout' stderr' -> + ErrorResponse OPENSSL_ERROR [i|OPENSSL ERROR: #{cert} - #{show ec <> "\n" <> stdout' <> "\n" <> stderr'}|] + +data ErrorCode = + PRODUCT_KEY_ERROR + | REGISTRATION_ERROR + | AGENT_UPDATE_ERROR + | DATABASE_ERROR + | WIFI_ERROR + | APPMGR_CONFIG_ERROR + | APPMGR_PARSE_ERROR + | APPMGR_ERROR + | AVAHI_ERROR + | REGISTRY_ERROR + | APP_NOT_INSTALLED + | APP_NOT_CONFIGURED + | APP_ACTION_FORBIDDEN + | SELF_UPDATE_ERROR + | INVALID_SSH_KEY + | INVALID_SSID + | INVALID_PSK + | INVALID_REQUEST + | INVALID_HEADER + | MISSING_HEADER + | METRICS_ERROR + | RESOURCE_NOT_FOUND + | UPDATE_IN_PROGRESS + | INTERNAL_ERROR + | SYNCHRONIZATION_ERROR + | BACKUP_ERROR + | OPENSSL_ERROR + deriving (Eq, Show) +instance ToJSON ErrorCode where + toJSON = String . show + +data ErrorResponse = ErrorResponse + { errorCode :: ErrorCode + , errorMessage :: Text + } + deriving (Eq, Show) +instance ToJSON ErrorResponse where + toJSON ErrorResponse {..} = object ["code" .= errorCode, "message" .= errorMessage] +instance ToContent ErrorResponse where + toContent = toContent . toJSON +instance ToTypedContent ErrorResponse where + toTypedContent = toTypedContent . toJSON + +instance ToTypedContent S9Error where + toTypedContent = toTypedContent . toJSON . toError +instance ToContent S9Error where + toContent = toContent . toJSON . toError + +toStatus :: S9Error -> Status +toStatus = \case + ProductKeyE -> status401 + RegistrationE -> status403 + NoCompliantAgentE _ -> status404 + PersistentE _ -> status500 + WifiConnectionE -> status500 + AppMgrParseE _ _ _ -> status500 + AppMgrInvalidConfigE _ -> status400 + AppMgrE _ _ -> status500 + AppMgrVersionE _ _ -> status500 + AvahiE _ -> status500 + MetricE _ -> status500 + RegistryUnreachableE -> status500 + RegistryParseE _ _ -> status500 + AppNotInstalledE _ -> status404 + AppStateActionIncompatibleE _ status action -> case (status, action) of + (AppStatusAppMgr Dead , _ ) -> status500 + (AppStatusAppMgr Removing , _ ) -> status403 + (AppStatusAppMgr Running , Start) -> status200 + (AppStatusAppMgr Running , Stop ) -> status200 + (AppStatusAppMgr Restarting , Start) -> status200 + (AppStatusAppMgr Restarting , Stop ) -> status200 + (AppStatusAppMgr Stopped , Start) -> status200 + (AppStatusAppMgr Stopped , Stop ) -> status200 + (AppStatusAppMgr Paused , _ ) -> status403 + (AppStatusTmp NeedsConfig, Start) -> status403 + (AppStatusTmp NeedsConfig, Stop ) -> status200 + (AppStatusTmp _ , _ ) -> status403 + UpdateSelfE _ _ -> status500 + InvalidSshKeyE _ -> status400 + InvalidSsidE -> status400 + InvalidPskE -> status400 + InvalidRequestE _ _ -> status400 + NotFoundE _ _ -> status404 + UpdateInProgressE -> status403 + TemporarilyForbiddenE _ _ _ -> status403 + TorServiceTimeoutE -> status500 + NginxSslE _ -> status500 + WifiOrphaningE -> status403 + ManifestParseE _ _ -> status500 + NoPasswordExistsE -> status401 + MissingFileE _ -> status500 + ClientCryptographyE _ -> status401 + TTLExpirationE _ -> status403 + EnvironmentValE _ -> status500 + HostsParamsE _ -> status400 + BackupE _ _ -> status500 + BackupPassInvalidE -> status403 + InternalE _ -> status500 + OpenSslE _ _ _ _ -> status500 + +handleS9ErrC :: (MonadHandler m, MonadLogger m) => ErrorC S9Error m a -> m a +handleS9ErrC action = + let handleIt e = do + $logError $ show e + toStatus >>= sendResponseStatus $ e + in runErrorC action handleIt pure + +handleS9ErrT :: (MonadHandler m, MonadLogger m) => S9ErrT m a -> m a +handleS9ErrT action = do + runExceptT action >>= \case + Left e -> do + $logError $ show e + toStatus >>= sendResponseStatus $ e + Right a -> pure a + +runS9ErrT :: MonadIO m => S9ErrT m a -> m (Either S9Error a) +runS9ErrT = runExceptT + +logS9ErrT :: (MonadIO m, MonadLogger m) => S9ErrT m a -> m (Maybe a) +logS9ErrT x = runS9ErrT x >>= \case + Left e -> do + $logError $ show e + pure Nothing + Right a -> pure $ Just a + +handleS9ErrNuclear :: MonadIO m => S9ErrT m a -> m a +handleS9ErrNuclear action = runExceptT action >>= \case + Left e -> throwIO e + Right a -> pure a + +orThrowM :: Has (Error e) sig m => m (Maybe a) -> e -> m a +orThrowM action e = action >>= maybe (throwError e) pure +{-# INLINE orThrowM #-} + +orThrowPure :: Has (Error e) sig m => Maybe a -> e -> m a +orThrowPure thing e = maybe (throwError e) pure thing +{-# INLINE orThrowPure #-} + diff --git a/agent/src/Lib/External/AppManifest.hs b/agent/src/Lib/External/AppManifest.hs new file mode 100644 index 000000000..b35dccfeb --- /dev/null +++ b/agent/src/Lib/External/AppManifest.hs @@ -0,0 +1,100 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +module Lib.External.AppManifest where + +import Startlude hiding ( ask ) + +import Control.Effect.Reader.Labelled +import Data.Aeson +import Data.Singletons.TypeLits +import qualified Data.HashMap.Strict as HM +import qualified Data.Yaml as Yaml +import Exinst + +import Lib.Error +import Lib.SystemPaths +import Lib.Types.Core +import Lib.Types.Emver +import Lib.Types.Emver.Orphans ( ) +import Control.Monad.Fail ( MonadFail(fail) ) + +data ImageType = ImageTypeTar + deriving (Eq, Show) + +instance FromJSON ImageType where + parseJSON = withText "Image Type" $ \case + "tar" -> pure ImageTypeTar + wat -> fail $ "Unknown Image Type: " <> toS wat + +data OnionVersion = OnionV2 | OnionV3 + deriving (Eq, Ord, Show) + +instance FromJSON OnionVersion where + parseJSON = withText "Onion Version" $ \case + "v2" -> pure OnionV2 + "v3" -> pure OnionV3 + wat -> fail $ "Unknown Onion Version: " <> toS wat + +data AssetMapping = AssetMapping + { assetMappingSource :: FilePath + , assetMappingDest :: FilePath + , assetMappingOverwrite :: Bool + } + deriving (Eq, Show) + +instance FromJSON AssetMapping where + parseJSON = withObject "Asset Mapping" $ \o -> do + assetMappingSource <- o .: "src" + assetMappingDest <- o .: "dst" + assetMappingOverwrite <- o .: "overwrite" + pure $ AssetMapping { .. } + +data AppManifest (n :: Nat) where + AppManifestV0 ::{ appManifestV0Id :: AppId + , appManifestV0Version :: Version + , appManifestV0Title :: Text + , appManifestV0DescShort :: Text + , appManifestV0DescLong :: Text + , appManifestV0ReleaseNotes :: Text + , appManifestV0PortMapping :: HM.HashMap Word16 Word16 + , appManifestV0ImageType :: ImageType + , appManifestV0Mount :: FilePath + , appManifestV0Assets :: [AssetMapping] + , appManifestV0OnionVersion :: OnionVersion + , appManifestV0Dependencies :: HM.HashMap AppId VersionRange + } -> AppManifest 0 + +instance FromJSON (Some1 AppManifest) where + parseJSON = withObject "App Manifest" $ \o -> do + o .: "compat" >>= \case + ("v0" :: Text) -> Some1 (SNat @0) <$> parseJSON (Object o) + compat -> fail $ "Unknown Manifest Version: " <> toS compat + +instance FromJSON (AppManifest 0) where + parseJSON = withObject "App Manifest V0" $ \o -> do + appManifestV0Id <- o .: "id" + appManifestV0Version <- o .: "version" + appManifestV0Title <- o .: "title" + appManifestV0DescShort <- o .: "description" >>= (.: "short") + appManifestV0DescLong <- o .: "description" >>= (.: "long") + appManifestV0ReleaseNotes <- o .: "release-notes" + appManifestV0PortMapping <- o .: "ports" >>= fmap HM.fromList . traverse parsePortMapping + appManifestV0ImageType <- o .: "image" >>= (.: "type") + appManifestV0Mount <- o .: "mount" + appManifestV0Assets <- o .: "assets" >>= traverse parseJSON + appManifestV0OnionVersion <- o .: "hidden-service-version" + appManifestV0Dependencies <- o .:? "dependencies" .!= HM.empty >>= traverse parseDepInfo + pure $ AppManifestV0 { .. } + where + parsePortMapping = withObject "Port Mapping" $ \o -> liftA2 (,) (o .: "tor") (o .: "internal") + parseDepInfo = withObject "Dep Info" $ (.: "version") + +getAppManifest :: (MonadIO m, HasFilesystemBase sig m) => AppId -> S9ErrT m (Maybe (Some1 AppManifest)) +getAppManifest appId = do + base <- ask @"filesystemBase" + ExceptT $ first (ManifestParseE appId) <$> liftIO + (Yaml.decodeFileEither . toS $ (appMgrAppPath appId <> "manifest.yaml") `relativeTo` base) + +uiAvailable :: AppManifest n -> Bool +uiAvailable = \case + AppManifestV0 { appManifestV0PortMapping } -> elem 80 (HM.keys appManifestV0PortMapping) diff --git a/agent/src/Lib/External/AppMgr.hs b/agent/src/Lib/External/AppMgr.hs new file mode 100644 index 000000000..da3ad3054 --- /dev/null +++ b/agent/src/Lib/External/AppMgr.hs @@ -0,0 +1,291 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} + +module Lib.External.AppMgr where + +import Startlude hiding ( hPutStrLn + , toS + ) + +import Control.Monad.Fail +import Data.Aeson +import qualified Data.ByteString as BS +import qualified Data.ByteString.Lazy as LBS +import qualified Data.HashMap.Strict as HM +import Data.String.Interpolate.IsString +import Data.Text ( unpack ) +import qualified Data.Yaml as Yaml +import Exinst +import Numeric.Natural +import System.IO.Error +import System.Process +import System.Process.Typed hiding ( createPipe ) + +import Lib.Error +import Lib.SystemPaths +import Lib.Types.Core +import Lib.Types.NetAddress +import Lib.Types.Emver +import qualified Data.ByteString.Char8 as B8 +import qualified Data.Attoparsec.Text as Atto + +readProcessWithExitCode' :: MonadIO m => String -> [String] -> ByteString -> m (ExitCode, ByteString, ByteString) +readProcessWithExitCode' a b c = liftIO $ do + let pc = + setStdin (byteStringInput $ LBS.fromStrict c) + $ setStderr byteStringOutput + $ setEnvInherit + $ setStdout byteStringOutput + $ (System.Process.Typed.proc a b) + withProcessWait pc $ \process -> atomically $ liftA3 (,,) + (waitExitCodeSTM process) + (fmap LBS.toStrict $ getStdout process) + (fmap LBS.toStrict $ getStderr process) + +readProcessInheritStderr :: MonadIO m => String -> [String] -> ByteString -> m (ExitCode, ByteString) +readProcessInheritStderr a b c = liftIO $ do + let pc = + setStdin (byteStringInput $ LBS.fromStrict c) + $ setStderr inherit + $ setEnvInherit + $ setStdout byteStringOutput + $ (System.Process.Typed.proc a b) + withProcessWait pc + $ \process -> atomically $ liftA2 (,) (waitExitCodeSTM process) (fmap LBS.toStrict $ getStdout process) + +torRepair :: MonadIO m => m ExitCode +torRepair = liftIO $ system "appmgr tor repair" + +getConfigurationAndSpec :: MonadIO m => AppId -> S9ErrT m Text +getConfigurationAndSpec appId = fmap decodeUtf8 $ do + (ec, out) <- readProcessInheritStderr "appmgr" ["info", show appId, "-C", "--json"] "" + case ec of + ExitSuccess -> pure out + ExitFailure n -> throwE $ AppMgrE [i|info #{appId} -C \--json|] n + +getAppMgrVersion :: MonadIO m => S9ErrT m Version +getAppMgrVersion = do + (code, out) <- liftIO $ readProcessInheritStderr "appmgr" ["semver"] "" + case code of + ExitSuccess -> case hush $ Atto.parseOnly parseVersion $ decodeUtf8 out of + Nothing -> throwE $ AppMgrParseE "semver" "" (B8.unpack out) + Just av -> pure av + ExitFailure n -> throwE $ AppMgrE "semver" n + +installNewAppMgr :: MonadIO m => VersionRange -> S9ErrT m Version +installNewAppMgr avs = do + getAppMgrVersion >>= \case + Version (0, 1, 0, _) -> void $ readProcessInheritStderr "appmgr" ["self-update", "=0.1.1"] "" + _ -> pure () + (ec, _) <- readProcessInheritStderr "appmgr" ["self-update", show avs] "" + case ec of + ExitSuccess -> getAppMgrVersion + ExitFailure n -> throwE $ AppMgrE [i|self-update #{avs}|] n + +torShow :: MonadIO m => AppId -> S9ErrT m (Maybe Text) +torShow appId = do + (ec, out) <- liftIO $ readProcessInheritStderr "appmgr" ["tor", "show", show appId] "" + case ec of + ExitSuccess -> pure $ Just (decodeUtf8 out) + ExitFailure n -> case n of + 6 -> pure Nothing + n' -> throwE $ AppMgrE "tor show" n' + +getAppLogs :: MonadIO m => AppId -> m Text +getAppLogs appId = liftIO $ do + (pipeRead, pipeWrite) <- createPipe + (_, _, _, handleProcess) <- createProcess (System.Process.proc "appmgr" ["logs", "--tail", "40", show appId]) + { std_out = UseHandle pipeWrite + , std_err = UseHandle pipeWrite + } + void $ waitForProcess handleProcess + content <- BS.hGetContents pipeRead + pure $ decodeUtf8 content + +notifications :: MonadIO m => AppId -> S9ErrT m [AppMgrNotif] +notifications appId = do + (ec, bs) <- readProcessInheritStderr "appmgr" ["notifications", show appId, "--json"] "" + case ec of + ExitSuccess -> case eitherDecodeStrict bs of + Left e -> throwE $ AppMgrParseE "notifications" (decodeUtf8 bs) e + Right x -> pure x + ExitFailure n -> throwE $ AppMgrE [i|notifications #{appId} \--json|] n + +stats :: MonadIO m => AppId -> S9ErrT m Text +stats appId = fmap decodeUtf8 $ do + (ec, out) <- readProcessInheritStderr "appmgr" ["stats", show appId, "--json"] "" + case ec of + ExitSuccess -> pure out + ExitFailure n -> throwE $ AppMgrE [i|stats #{appId} \--json|] n + +torReload :: MonadIO m => S9ErrT m () +torReload = do + (ec, _) <- readProcessInheritStderr "appmgr" ["tor", "reload"] "" + case ec of + ExitSuccess -> pure () + ExitFailure n -> throwE $ AppMgrE "tor reload" n + +diskShow :: MonadIO m => S9ErrT m [DiskInfo] +diskShow = do + (ec, bs) <- readProcessInheritStderr "appmgr" ["disks", "show", "--json"] "" + case ec of + ExitSuccess -> case eitherDecodeStrict bs of + Left e -> throwE $ AppMgrParseE "disk info" (decodeUtf8 bs) e + Right x -> pure x + ExitFailure n -> throwE $ AppMgrE "disk show" n + +backupCreate :: MonadIO m => Maybe Text -> AppId -> FilePath -> S9ErrT m () +backupCreate password appId disk = do + let args = case password of + Nothing -> ["backup", "create", "-p", "\"\"", show appId, disk] + Just p' -> ["backup", "create", "-p", unpack p', show appId, disk] + (ec, _) <- readProcessInheritStderr "appmgr" args "" + case ec of + ExitFailure n | n < 0 -> throwE $ BackupE appId "Interrupted" + | n == 7 -> throwE $ BackupPassInvalidE + | otherwise -> throwE $ AppMgrE "backup" n + ExitSuccess -> pure () + +backupRestore :: MonadIO m => Maybe Text -> AppId -> FilePath -> S9ErrT m () +backupRestore password appId disk = do + let args = case password of + Nothing -> ["backup", "restore", "-p", "\"\"", show appId, disk] + Just p' -> ["backup", "restore", "-p", unpack p', show appId, disk] + (ec, _) <- readProcessInheritStderr "appmgr" args "" + case ec of + ExitFailure n | n < 0 -> throwE $ BackupE appId "Interrupted" + | n == 7 -> throwE $ BackupPassInvalidE + | otherwise -> throwE $ AppMgrE "backup" n + ExitSuccess -> pure () + +data AppMgrLevel = + INFO + | SUCCESS + | WARN + | ERROR + deriving (Eq, Show, Read) + +instance FromJSON AppMgrLevel where + parseJSON = withText "Level" $ \t -> case readMaybe t of + Nothing -> fail $ "Invalid Level: " <> unpack t + Just x -> pure x + +data AppMgrNotif = AppMgrNotif + { appMgrNotifTime :: Rational + , appMgrNotifLevel :: AppMgrLevel + , appMgrNotifCode :: Natural + , appMgrNotifTitle :: Text + , appMgrNotifMessage :: Text + } + deriving (Eq, Show) + +instance FromJSON AppMgrNotif where + parseJSON = withObject "appmgr notification res" $ \o -> do + appMgrNotifTime <- o .: "time" + appMgrNotifLevel <- o .: "level" + appMgrNotifCode <- o .: "code" + appMgrNotifTitle <- o .: "title" + appMgrNotifMessage <- o .: "message" + pure AppMgrNotif { .. } + +type Manifest = Some1 ManifestStructure +data ManifestStructure (n :: Nat) where + ManifestV0 ::{ manifestTitle :: Text + } -> ManifestStructure 0 + +instance FromJSON (Some1 ManifestStructure) where + parseJSON = withObject "app manifest" $ \o -> do + o .: "compat" >>= \t -> case (t :: Text) of + "v0" -> some1 <$> parseJSON @(ManifestStructure 0) (Object o) + other -> fail $ "Unknown Compat Version" <> unpack other + +instance FromJSON (ManifestStructure 0) where + parseJSON = withObject "manifest v0" $ \o -> do + manifestTitle <- o .: "title" + pure $ ManifestV0 { .. } + +torrcBase :: SystemPath +torrcBase = "/root/appmgr/tor/torrc" + +torServicesYaml :: SystemPath +torServicesYaml = "/root/appmgr/tor/services.yaml" + +appMgrAppsDirectory :: SystemPath +appMgrAppsDirectory = "/root/appmgr/apps" + +readLanIps :: (MonadReader Text m, MonadIO m) => S9ErrT m (HM.HashMap AppId LanIp) +readLanIps = do + base <- ask + contents <- + liftIO $ (Just <$> readFile (unpack $ torServicesYaml `relativeTo` base)) `catch` \(e :: IOException) -> + if isDoesNotExistError e then pure Nothing else throwIO e + case contents of + Nothing -> pure HM.empty + Just contents' -> do + val <- case Yaml.decodeEither' (encodeUtf8 contents') of + Left e -> throwE $ AppMgrParseE "lan ip" contents' (show e) + Right a -> pure a + case Yaml.parseEither parser val of + Left e -> throwE $ AppMgrParseE "lan ip" (show val) e + Right a -> pure a + where + parser :: Value -> Yaml.Parser (HM.HashMap AppId LanIp) + parser = withObject "Tor Services Yaml" $ \o -> do + hm <- o .: "map" + let (services, infos) = unzip $ HM.toList hm + ips <- traverse ipParser infos + pure . HM.fromList $ zip (AppId <$> services) ips + ipParser :: Value -> Yaml.Parser LanIp + ipParser = withObject "Service Info" $ \o -> do + ip <- o .: "ip" + pure $ LanIp ip + +data DiskInfo = DiskInfo + { diskInfoDescription :: Maybe Text + , diskInfoSize :: Text + , diskInfoLogicalName :: FilePath + , diskInfoPartitions :: [PartitionInfo] + } + deriving (Eq, Show) +instance FromJSON DiskInfo where + parseJSON = withObject "Disk Info" $ \o -> do + diskInfoDescription <- o .: "description" + diskInfoSize <- o .: "size" + diskInfoLogicalName <- o .: "logicalname" + diskInfoPartitions <- o .: "partitions" + pure DiskInfo { .. } +instance ToJSON DiskInfo where + toJSON DiskInfo {..} = object + [ "description" .= diskInfoDescription + , "size" .= diskInfoSize + , "logicalname" .= diskInfoLogicalName + , "partitions" .= diskInfoPartitions + ] + +data PartitionInfo = PartitionInfo + { partitionInfoLogicalName :: FilePath + , partitionInfoSize :: Maybe Text + , partitionInfoIsMounted :: Bool + , partitionInfoLabel :: Maybe Text + } + deriving (Eq, Show) +instance FromJSON PartitionInfo where + parseJSON = withObject "Partition Info" $ \o -> do + partitionInfoLogicalName <- o .: "logicalname" + partitionInfoSize <- o .: "size" + partitionInfoIsMounted <- o .: "is-mounted" + partitionInfoLabel <- o .: "label" + pure PartitionInfo { .. } +instance ToJSON PartitionInfo where + toJSON PartitionInfo {..} = object + [ "logicalname" .= partitionInfoLogicalName + , "size" .= partitionInfoSize + , "isMounted" .= partitionInfoIsMounted + , "label" .= partitionInfoLabel + ] diff --git a/agent/src/Lib/External/Metrics/Df.hs b/agent/src/Lib/External/Metrics/Df.hs new file mode 100644 index 000000000..cc9bd8d53 --- /dev/null +++ b/agent/src/Lib/External/Metrics/Df.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE RecordWildCards #-} + +module Lib.External.Metrics.Df where + +import Startlude + +import System.Process + +import Lib.Error +import Lib.External.Metrics.Types + +-- Disk :: Size Used Avail Use% +data DfMetrics = DfMetrics + { metricDiskSize :: Maybe Gigabytes + , metricDiskUsed :: Maybe Gigabytes + , metricDiskAvailable :: Maybe Gigabytes + , metricDiskUsedPercentage :: Maybe Percentage + } deriving (Eq, Show) + +getDfMetrics :: MonadIO m => S9ErrT m DfMetrics +getDfMetrics = fmap parseDf runDf + +runDf :: MonadIO m => S9ErrT m Text +runDf = do + (_, output, err') <- liftIO $ readProcessWithExitCode "df" ["-a", "/"] "" + unless (null err') $ throwE . MetricE $ "df command failed with " <> toS err' + pure $ toS output + +parseDf :: Text -> DfMetrics +parseDf t = + let dataLine = words <$> lines t `atMay` 1 + metricDiskSize = fmap oneKBlocksToGigs . readMaybe =<< (`atMay` 1) =<< dataLine + metricDiskUsed = fmap oneKBlocksToGigs . readMaybe =<< (`atMay` 2) =<< dataLine + metricDiskAvailable = fmap oneKBlocksToGigs . readMaybe =<< (`atMay` 3) =<< dataLine + metricDiskUsedPercentage = readMaybe =<< (`atMay` 4) =<< dataLine + in DfMetrics { .. } + +oneKBlocksToGigs :: Double -> Gigabytes +oneKBlocksToGigs s = Gigabytes $ s / 1e6 diff --git a/agent/src/Lib/External/Metrics/Iotop.hs b/agent/src/Lib/External/Metrics/Iotop.hs new file mode 100644 index 000000000..306892b78 --- /dev/null +++ b/agent/src/Lib/External/Metrics/Iotop.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE FlexibleContexts #-} + +module Lib.External.Metrics.Iotop where + +import Startlude + +import qualified Data.HashMap.Strict as HM +import System.Process + +import Lib.Error +import Lib.External.Metrics.Types +import Lib.External.Util +import Util.Text + +data IotopMetrics = IotopMetrics + { metricCurrentRead :: Maybe BytesPerSecond + , metricCurrentWrite :: Maybe BytesPerSecond + , metricTotalRead :: Maybe BytesPerSecond + , metricTotalWrite :: Maybe BytesPerSecond + } deriving (Eq, Show) + +getIotopMetrics :: MonadIO m => S9ErrT m IotopMetrics +getIotopMetrics = fmap parseIotop runIotop + +runIotop :: MonadIO m => S9ErrT m Text +runIotop = do + (_, output, err') <- liftIO $ readProcessWithExitCode "iotop" ["-bn1"] "" + unless (null err') $ throwE . MetricE $ "iotop command failed with " <> toS err' + pure $ toS output + +parseIotop :: Text -> IotopMetrics +parseIotop t = IotopMetrics { metricCurrentRead = BytesPerSecond . fst <$> current + , metricCurrentWrite = BytesPerSecond . snd <$> current + , metricTotalRead = BytesPerSecond . fst <$> total + , metricTotalWrite = BytesPerSecond . snd <$> total + } + where + iotopLines = lines t + current = getHeaderAggregates currentHeader iotopLines + total = getHeaderAggregates totalHeader iotopLines + +currentHeader :: Text +currentHeader = "Current" + +totalHeader :: Text +totalHeader = "Total" + +getHeaderAggregates :: Text -> [Text] -> Maybe (Double, Double) +getHeaderAggregates header iotopLines = do + actualLine <- getLineByHeader header iotopLines + let stats = HM.fromList . getStats $ actualLine + r <- HM.lookup "READ" stats + w <- HM.lookup "WRITE" stats + pure (r, w) +getStats :: Text -> [(Text, Double)] +getStats = mapMaybe (parseToPair readMaybe . words . gsub ":" "") . getMatches statRegex + where statRegex = "([\x21-\x7E]+)[ ]{0,}:[ ]{1,}([\x21-\x7E]+)" + diff --git a/agent/src/Lib/External/Metrics/ProcDev.hs b/agent/src/Lib/External/Metrics/ProcDev.hs new file mode 100644 index 000000000..aba8910c8 --- /dev/null +++ b/agent/src/Lib/External/Metrics/ProcDev.hs @@ -0,0 +1,118 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TupleSections #-} + +module Lib.External.Metrics.ProcDev where + +import Startlude + +import Lib.External.Util +import Lib.External.Metrics.Types +import Lib.Error +import Util.Text + +data ProcDevMetrics = ProcDevMetrics + { metricRBytesPerSecond :: Maybe BytesPerSecond + , metricRPacketsPerSecond :: Maybe BytesPerSecond + , metricRErrorsPerSecond :: Maybe BytesPerSecond + , metricTBytesPerSecond :: Maybe BytesPerSecond + , metricTPacketsPerSecond :: Maybe BytesPerSecond + , metricTErrorsPerSecond :: Maybe BytesPerSecond + , metricFrom :: UTCTime -- time range across which the above rates were calculated + , metricTo :: UTCTime + } deriving Show + +getProcDevMetrics :: MonadIO m + => (UTCTime, ProcDevMomentStats) + -> S9ErrT m (UTCTime, ProcDevMomentStats, ProcDevMetrics) +getProcDevMetrics oldMomentStats = do + newMomentStats@(newTime, newStats) <- newProcDevMomentStats + let metrics = computeProcDevMetrics oldMomentStats newMomentStats + pure (newTime, newStats, metrics) + +newProcDevMomentStats :: MonadIO m => S9ErrT m (UTCTime, ProcDevMomentStats) +newProcDevMomentStats = do + res <- runProcDev + now <- liftIO getCurrentTime + pure $ parseProcDev now res + +runProcDev :: MonadIO m => S9ErrT m Text +runProcDev = do + eOutput <- liftIO . try @SomeException $ readFile "/proc/net/dev" + case eOutput of + Left e -> throwE . MetricE $ "ProcDev proc file could not be read with " <> show e + Right output -> pure . toS $ output + +parseProcDev :: UTCTime -> Text -> (UTCTime, ProcDevMomentStats) +parseProcDev now t = do + (now, ) . fold . foreach filteredLines $ \l -> + let ws = words l + procDevRBytes = ws `atMay` 1 >>= readMaybe + procDevRPackets = ws `atMay` 2 >>= readMaybe + procDevRErrors = ws `atMay` 3 >>= readMaybe + + procDevTBytes = ws `atMay` 9 >>= readMaybe + procDevTPackets = ws `atMay` 10 >>= readMaybe + procDevTErrors = ws `atMay` 11 >>= readMaybe + in ProcDevMomentStats { .. } + where + wlanRegex = "^[ ]{0,}wlan0" + ethRegex = "^[ ]{0,}eth0" + + isWlan = containsMatch wlanRegex + isEth = containsMatch ethRegex + + filteredLines = filter (liftA2 (||) isWlan isEth) $ lines t + +computeProcDevMetrics :: (UTCTime, ProcDevMomentStats) -> (UTCTime, ProcDevMomentStats) -> ProcDevMetrics +computeProcDevMetrics (fromTime, fromStats) (toTime, toStats) = + let metricRBytesPerSecond = getMetric (procDevRBytes fromStats, fromTime) (procDevRBytes toStats, toTime) + metricRPacketsPerSecond = getMetric (procDevRPackets fromStats, fromTime) (procDevRPackets toStats, toTime) + metricRErrorsPerSecond = getMetric (procDevRErrors fromStats, fromTime) (procDevRErrors toStats, toTime) + metricTBytesPerSecond = getMetric (procDevTBytes fromStats, fromTime) (procDevTBytes toStats, toTime) + metricTPacketsPerSecond = getMetric (procDevTPackets fromStats, fromTime) (procDevTPackets toStats, toTime) + metricTErrorsPerSecond = getMetric (procDevTErrors fromStats, fromTime) (procDevTErrors toStats, toTime) + metricFrom = fromTime + metricTo = toTime + in ProcDevMetrics { .. } + +getMetric :: (Maybe Integer, UTCTime) -> (Maybe Integer, UTCTime) -> Maybe BytesPerSecond +getMetric (Just fromMetric, fromTime) (Just toMetric, toTime) = Just . BytesPerSecond $ if timeDiff == 0 + then 0 + else truncateTo @Double 10 . fromRational $ (fromIntegral $ toMetric - fromMetric) / (toRational timeDiff) + where timeDiff = diffUTCTime toTime fromTime +getMetric _ _ = Nothing + +data ProcDevMomentStats = ProcDevMomentStats + { procDevRBytes :: Maybe Integer + , procDevRPackets :: Maybe Integer + , procDevRErrors :: Maybe Integer + , procDevTBytes :: Maybe Integer + , procDevTPackets :: Maybe Integer + , procDevTErrors :: Maybe Integer + } deriving (Eq, Show) + +(?+?) :: Num a => Maybe a -> Maybe a -> Maybe a +(?+?) Nothing Nothing = Nothing +(?+?) m1 m2 = Just $ fromMaybe 0 m1 + fromMaybe 0 m2 + +(?-?) :: Num a => Maybe a -> Maybe a -> Maybe a +(?-?) Nothing Nothing = Nothing +(?-?) m1 m2 = Just $ fromMaybe 0 m1 - fromMaybe 0 m2 + +instance Semigroup ProcDevMomentStats where + m1 <> m2 = ProcDevMomentStats rBytes rPackets rErrors tBytes tPackets tErrors + where + rBytes = procDevRBytes m1 ?+? procDevRBytes m2 + rPackets = procDevRPackets m1 ?+? procDevRPackets m2 + rErrors = procDevRErrors m1 ?+? procDevRErrors m2 + tBytes = procDevTBytes m1 ?+? procDevTBytes m2 + tPackets = procDevTPackets m1 ?+? procDevTPackets m2 + tErrors = procDevTErrors m1 ?+? procDevTErrors m2 +instance Monoid ProcDevMomentStats where + mempty = ProcDevMomentStats (Just 0) (Just 0) (Just 0) (Just 0) (Just 0) (Just 0) + +getDefaultProcDevMetrics :: MonadIO m => m ProcDevMetrics +getDefaultProcDevMetrics = do + now <- liftIO getCurrentTime + pure $ ProcDevMetrics Nothing Nothing Nothing Nothing Nothing Nothing now now diff --git a/agent/src/Lib/External/Metrics/Temperature.hs b/agent/src/Lib/External/Metrics/Temperature.hs new file mode 100644 index 000000000..7a5664da6 --- /dev/null +++ b/agent/src/Lib/External/Metrics/Temperature.hs @@ -0,0 +1,22 @@ +module Lib.External.Metrics.Temperature where + +import Startlude + +import qualified Data.Attoparsec.Text as A +import qualified Data.Text as T +import Lib.External.Metrics.Types +import System.Process.Text + +-- Pi4 Specific +getTemperature :: MonadIO m => m (Maybe Celsius) +getTemperature = liftIO $ do + (ec, tempString, errlog) <- readProcessWithExitCode "/opt/vc/bin/vcgencmd" ["measure_temp"] "" + unless (T.null errlog) $ putStrLn errlog + case ec of + ExitFailure _ -> pure Nothing + ExitSuccess -> case A.parse tempParser tempString of + A.Done _ c -> pure $ Just c + _ -> pure Nothing + +tempParser :: A.Parser Celsius +tempParser = A.asciiCI "temp=" *> fmap Celsius A.double <* "'C" <* A.endOfLine diff --git a/agent/src/Lib/External/Metrics/Top.hs b/agent/src/Lib/External/Metrics/Top.hs new file mode 100644 index 000000000..115421fd2 --- /dev/null +++ b/agent/src/Lib/External/Metrics/Top.hs @@ -0,0 +1,114 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} + +module Lib.External.Metrics.Top where + +import Startlude + +import qualified Data.HashMap.Strict as HM +import System.Process + +import Lib.Error +import Lib.External.Metrics.Types +import Lib.External.Util +import Util.Text + +data TopMetrics = TopMetrics + { metricMemPercentageUsed :: Maybe Percentage + , metricMemFree :: Maybe MebiBytes + , metricMemUsed :: Maybe MebiBytes + + , metricSwapTotal :: Maybe MebiBytes + , metricSwapUsed :: Maybe MebiBytes + + , metricCpuIdle :: Maybe Percentage + , metricCpuUserSpace :: Maybe Percentage + , metricWait :: Maybe Percentage + , metricCpuPercentageUsed :: Maybe Percentage + } deriving (Eq, Show) + +getTopMetrics :: MonadIO m => S9ErrT m TopMetrics +getTopMetrics = fmap parseTop runTop + +runTop :: MonadIO m => S9ErrT m Text +runTop = do + (_, output, err') <- liftIO $ readProcessWithExitCode "top" ["-bn1"] "" + unless (null err') $ throwE . MetricE $ "top command failed with " <> toS err' + pure $ toS output + +parseTop :: Text -> TopMetrics +parseTop t = TopMetrics { metricMemPercentageUsed = getMemPercentageUsed <$> mem + , metricMemFree = MebiBytes . memFree <$> mem + , metricMemUsed = MebiBytes . memUsed <$> mem + , metricSwapTotal = MebiBytes . memTotal <$> swapS + , metricSwapUsed = MebiBytes . memUsed <$> swapS + , metricCpuIdle = cpuId <$> cpu + , metricCpuUserSpace = cpuUs <$> cpu + , metricWait = cpuWa <$> cpu + , metricCpuPercentageUsed = getCpuPercentageUsed <$> cpu + } + where + topLines = lines t + cpu = getCpuAggregates topLines + mem = getMemAggregates memHeader topLines + swapS = getMemAggregates swapHeader topLines + +memHeader :: Text +memHeader = "MiB Mem" + +swapHeader :: Text +swapHeader = "MiB Swap" + +data TopMemAggregates = TopMemAggregates + { memTotal :: Double + , memFree :: Double + , memUsed :: Double + } deriving (Eq, Show) + +cpuHeader :: Text +cpuHeader = "%Cpu(s)" + +data TopCpuAggregates = TopCpuAggregates + { cpuUs :: Percentage + , cpuSy :: Percentage + , cpuNi :: Percentage + , cpuId :: Percentage + , cpuWa :: Percentage + , cpuHi :: Percentage + , cpuSi :: Percentage + , cpuSt :: Percentage + } deriving (Eq, Show) + +getMemAggregates :: Text -> [Text] -> Maybe TopMemAggregates +getMemAggregates header topRes = do + memLine <- getLineByHeader header topRes + let stats = HM.fromList $ getStats readMaybe memLine + memTotal <- HM.lookup "total" stats + memFree <- HM.lookup "free" stats + memUsed <- HM.lookup "used" stats + pure TopMemAggregates { .. } + +getCpuAggregates :: [Text] -> Maybe TopCpuAggregates +getCpuAggregates topRes = do + memLine <- getLineByHeader cpuHeader topRes + let stats = HM.fromList $ getStats (mkPercentage <=< readMaybe) memLine + cpuUs <- HM.lookup "us" stats + cpuSy <- HM.lookup "sy" stats + cpuNi <- HM.lookup "ni" stats + cpuId <- HM.lookup "id" stats + cpuWa <- HM.lookup "wa" stats + cpuHi <- HM.lookup "hi" stats + cpuSi <- HM.lookup "si" stats + cpuSt <- HM.lookup "st" stats + pure TopCpuAggregates { .. } + +getCpuPercentageUsed :: TopCpuAggregates -> Percentage +getCpuPercentageUsed TopCpuAggregates {..} = Percentage (100 - unPercent cpuId) + +getMemPercentageUsed :: TopMemAggregates -> Percentage +getMemPercentageUsed TopMemAggregates {..} = Percentage . truncateTo @Double 10 . (* 100) $ memUsed / memTotal + +getStats :: (Text -> Maybe a) -> Text -> [(Text, a)] +getStats parseData = mapMaybe (parseToPair parseData) . fmap (words . toS) . getMatches statRegex . toS + where statRegex = "[0-9]+(.[0-9][0-9]?)? ([\x21-\x7E][^(,|.)]+)" diff --git a/agent/src/Lib/External/Metrics/Types.hs b/agent/src/Lib/External/Metrics/Types.hs new file mode 100644 index 000000000..acb2840d7 --- /dev/null +++ b/agent/src/Lib/External/Metrics/Types.hs @@ -0,0 +1,89 @@ +module Lib.External.Metrics.Types where + +import Startlude + +import Data.Aeson +import qualified GHC.Read ( Read(..) + , readsPrec + ) +import qualified GHC.Show ( Show(..) ) + +import Lib.External.Util + +class Metric a where + mUnit :: a -> Text + mValue :: a -> Double + +toMetricJson :: Metric a => a -> Value +toMetricJson x = object ["value" .= truncateToS 2 (mValue x), "unit" .= mUnit x] +toMetricShow :: Metric a => a -> String +toMetricShow a = show (mValue a) <> " " <> toS (mUnit a) + +newtype Percentage = Percentage { unPercent :: Double } deriving (Eq) +instance Metric Percentage where + mValue (Percentage p) = p + mUnit _ = "%" +instance ToJSON Percentage where + toJSON = toMetricJson +instance Show Percentage where + show = toMetricShow +instance Read Percentage where + readsPrec _ s = case reverse s of + '%' : rest -> case GHC.Read.readsPrec 0 (reverse rest) of + [(result, "")] -> case mkPercentage result of + Just p -> [(p, "")] + _ -> [] + _ -> [] + _ -> [] + +mkPercentage :: Double -> Maybe Percentage +mkPercentage s | 0 <= s && s <= 100 = Just $ Percentage s + | otherwise = Nothing + +newtype MebiBytes = MebiBytes Double + deriving stock Eq + deriving newtype Num + +instance Metric MebiBytes where + mValue (MebiBytes p) = p + mUnit _ = "MiB" +instance ToJSON MebiBytes where + toJSON = toMetricJson +instance Show MebiBytes where + show = toMetricShow + +newtype BytesPerSecond = BytesPerSecond Double + deriving stock Eq + deriving newtype Num + +instance Metric BytesPerSecond where + mValue (BytesPerSecond p) = p + mUnit _ = "B/s" +instance ToJSON BytesPerSecond where + toJSON = toMetricJson +instance Show BytesPerSecond where + show = toMetricShow + +newtype Gigabytes = Gigabytes Double + deriving stock Eq + deriving newtype Num + +instance Metric Gigabytes where + mValue (Gigabytes p) = p + mUnit _ = "Gb" +instance ToJSON Gigabytes where + toJSON = toMetricJson +instance Show Gigabytes where + show = toMetricShow + +newtype Celsius = Celsius { unCelsius :: Double } + deriving stock Eq + deriving newtype Num + +instance Metric Celsius where + mValue (Celsius c) = c + mUnit _ = "°C" +instance ToJSON Celsius where + toJSON = toMetricJson +instance Show Celsius where + show = toMetricShow diff --git a/agent/src/Lib/External/Registry.hs b/agent/src/Lib/External/Registry.hs new file mode 100644 index 000000000..c1225d04d --- /dev/null +++ b/agent/src/Lib/External/Registry.hs @@ -0,0 +1,196 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE UndecidableInstances #-} +module Lib.External.Registry where + +import Startlude hiding ( (<.>) + , Reader + , ask + , runReader + ) +import Startlude.ByteStream hiding ( count ) + +import Conduit +import Control.Algebra +import Control.Effect.Lift +import Control.Effect.Error +import Control.Effect.Reader.Labelled +import Control.Monad.Fail ( fail ) +import Control.Monad.Trans.Resource +import qualified Data.ByteString.Streaming.HTTP + as S +import qualified Data.HashMap.Strict as HM +import Data.Maybe ( fromJust ) +import Data.String.Interpolate.IsString +import Data.Yaml +import Network.HTTP.Client.Conduit ( Manager ) +import Network.HTTP.Simple +import System.Directory +import System.Process + +import Constants +import Lib.Algebra.State.RegistryUrl +import Lib.Error +import Lib.SystemPaths +import Lib.Types.Core +import Lib.Types.Emver +import Lib.Types.ServerApp + +newtype AppManifestRes = AppManifestRes + { storeApps :: [StoreApp] } deriving (Eq, Show) + +newtype RegistryVersionForSpecRes = RegistryVersionForSpecRes + { registryVersionForSpec :: Maybe Version } deriving (Eq, Show) + +instance FromJSON RegistryVersionForSpecRes where + parseJSON Null = pure (RegistryVersionForSpecRes Nothing) + parseJSON (Object o) = do + registryVersionForSpec <- o .:? "version" + pure . RegistryVersionForSpecRes $ registryVersionForSpec + parseJSON _ = fail "expected null or object" + +tmpAgentFileName :: Text +tmpAgentFileName = "agent-tmp" + +agentFileName :: Text +agentFileName = "agent" + +userAgentHeader :: ByteString +userAgentHeader = [i|EmbassyOS/#{agentVersion}|] + +setUserAgent :: Request -> Request +setUserAgent = setRequestHeader "User-Agent" [userAgentHeader] + +getYoungAgentBinary :: (Has RegistryUrl sig m, HasLabelled "filesystemBase" (Reader Text) sig m, Has (Lift IO) sig m) + => VersionRange + -> m () +getYoungAgentBinary avs = do + base <- ask @"filesystemBase" + let tmpAgentPath = toS $ executablePath `relativeTo` base tmpAgentFileName + tmpExists <- sendIO $ doesPathExist tmpAgentPath + when tmpExists $ sendIO $ removeFile tmpAgentPath + url <- registryAppAgentUrl avs + request <- sendIO . fmap setUserAgent . parseRequestThrow $ toS url + sendIO $ runConduitRes $ httpSource request getResponseBody .| sinkFile tmpAgentPath + sendIO $ void $ readProcessWithExitCode "chmod" ["700", tmpAgentPath] "" + +getLifelineBinary :: (Has RegistryUrl sig m, HasFilesystemBase sig m, MonadIO m) => VersionRange -> m () +getLifelineBinary avs = do + base <- ask @"filesystemBase" + let lifelineTarget = lifelineBinaryPath `relativeTo` base + url <- registryUrl + request <- liftIO . fmap setUserAgent . parseRequestThrow $ toS (url "sys/lifeline?spec=" <> show avs) + liftIO $ runConduitRes $ httpSource request getResponseBody .| sinkFile (toS lifelineTarget) + liftIO $ void $ readProcessWithExitCode "chmod" ["700", toS lifelineTarget] "" + +getAppManifest :: (MonadIO m, Has (Error S9Error) sig m, Has RegistryUrl sig m) => m AppManifestRes +getAppManifest = do + manifestPath <- registryManifestUrl + req <- liftIO $ fmap setUserAgent . parseRequestThrow $ toS manifestPath + val <- (liftIO . try @SomeException) (httpBS req) >>= \case + Left _ -> throwError RegistryUnreachableE + Right a -> pure $ getResponseBody a + parseBsManifest val >>= \case + Left e -> throwError $ RegistryParseE manifestPath . toS $ e + Right a -> pure a + + +getStoreAppInfo :: (MonadIO m, Has RegistryUrl sig m, Has (Error S9Error) sig m) => AppId -> m (Maybe StoreApp) +getStoreAppInfo name = find ((== name) . storeAppId) . storeApps <$> getAppManifest + +parseBsManifest :: Has RegistryUrl sig m => ByteString -> m (Either String AppManifestRes) +parseBsManifest bs = do + parseRegistryRes' <- parseRegistryRes + pure $ parseEither parseRegistryRes' . fromJust . decodeThrow $ bs + +parseRegistryRes :: Has RegistryUrl sig m => m (Value -> Parser AppManifestRes) +parseRegistryRes = do + parseAppData' <- parseAppData + pure $ withObject "app registry response" $ \obj -> do + let keyVals = HM.toList obj + let mManifestApps = fmap (\(k, v) -> parseMaybe (parseAppData' (AppId k)) v) keyVals + pure . AppManifestRes . catMaybes $ mManifestApps + +registryUrl :: (Has RegistryUrl sig m) => m Text +registryUrl = maybe "https://registry.start9labs.com:443" show <$> getRegistryUrl + +registryManifestUrl :: Has RegistryUrl sig m => m Text +registryManifestUrl = registryUrl <&> ( "apps") + +registryAppAgentUrl :: Has RegistryUrl sig m => VersionRange -> m Text +registryAppAgentUrl avs = registryUrl <&> ( ("sys/agent?spec=" <> show avs)) + +registryCheckVersionForSpecUrl :: Has RegistryUrl sig m => VersionRange -> m Text +registryCheckVersionForSpecUrl avs = registryUrl <&> ( ("sys/version/agent?spec=" <> show avs)) + +parseAppData :: Has RegistryUrl sig m => m (AppId -> Value -> Parser StoreApp) +parseAppData = do + url <- registryUrl + pure $ \storeAppId -> withObject "appmgr app data" $ \ad -> do + storeAppTitle <- ad .: "title" + storeAppDescriptionShort <- ad .: "description" >>= (.: "short") + storeAppDescriptionLong <- ad .: "description" >>= (.: "long") + storeAppIconUrl <- fmap (\typ -> toS $ url "icons" show storeAppId <.> typ) $ ad .: "icon-type" + storeAppVersions <- ad .: "version-info" >>= \case + [] -> fail "No Valid Version Info" + (x : xs) -> pure $ x :| xs + pure StoreApp { .. } + +getAppVersionForSpec :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) + => AppId + -> VersionRange + -> m Version +getAppVersionForSpec appId spec = do + let path = "apps/version" show appId <> "?spec=" <> show spec + val <- registryRequest path + parseOrThrow path val $ withObject "version response" $ \o -> do + v <- o .: "version" + pure v + +getLatestAgentVersion :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) => m Version +getLatestAgentVersion = do + val <- registryRequest agentVersionPath + parseOrThrow agentVersionPath val $ withObject "version response" $ \o -> do + v <- o .: "version" + pure v + where agentVersionPath = "sys/version/agent" + +getLatestAgentVersionForSpec :: (Has RegistryUrl sig m, Has (Lift IO) sig m, Has (Error S9Error) sig m) + => VersionRange + -> m (Maybe Version) +getLatestAgentVersionForSpec avs = do + url <- registryUrl + req <- sendIO $ fmap setUserAgent . parseRequestThrow . toS $ url agentVersionPath + res <- fmap (first jsonToS9Exception) . sendIO $ try @JSONException $ parseRes req + case res of + Left e -> throwError e + Right a -> pure a + where + parseRes r = registryVersionForSpec . getResponseBody <$> httpJSON r + agentVersionPath = "sys/version/agent?spec=" <> show avs + jsonToS9Exception = RegistryParseE (toS agentVersionPath) . show + +getAmbassadorUiForSpec :: (Has RegistryUrl sig m, HasLabelled "httpManager" (Reader Manager) sig m, MonadResource m) + => VersionRange + -> ByteStream m () +getAmbassadorUiForSpec avs = do + url <- lift registryUrl + manager <- lift $ ask @"httpManager" + let target = url "sys/ambassador-ui.tar.gz?spec=" <> show avs + req <- liftResourceT $ lift $ fmap setUserAgent . parseRequestThrow . toS $ target + resp <- lift $ S.http req manager + getResponseBody resp + +registryRequest :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) => Text -> m Value +registryRequest path = do + url <- registryUrl + req <- liftIO . fmap setUserAgent . parseRequestThrow . toS $ url path + (liftIO . try @SomeException) (httpJSON req) >>= \case + Left _ -> throwError RegistryUnreachableE + Right a -> pure $ getResponseBody a + +parseOrThrow :: (Has (Error S9Error) sig m) => Text -> a -> (a -> Parser b) -> m b +parseOrThrow path val parser = case parseEither parser val of + Left e -> throwError (RegistryParseE path $ toS e) + Right a -> pure a diff --git a/agent/src/Lib/External/Specs/CPU.hs b/agent/src/Lib/External/Specs/CPU.hs new file mode 100644 index 000000000..afd2d1930 --- /dev/null +++ b/agent/src/Lib/External/Specs/CPU.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE QuasiQuotes #-} +module Lib.External.Specs.CPU + ( getCpuInfo + ) +where + +import Startlude +import Protolude.Unsafe ( unsafeFromJust ) + +import Data.String.Interpolate.IsString +import System.Process + +import Lib.External.Specs.Common + +lscpu :: IO Text +lscpu = toS <$> readProcess "lscpu" [] "" + +getModelName :: Text -> Text +getModelName = unsafeFromJust . getSpec "Model name" + +getCores :: Text -> Text +getCores = unsafeFromJust . getSpec "CPU(s)" + +getClockSpeed :: Text -> Text +getClockSpeed = (<> "MHz") . unsafeFromJust . getSpec "CPU max" + +getCpuInfo :: IO Text +getCpuInfo = lscpu <&> do + model <- getModelName + cores <- getCores + clock <- getClockSpeed + pure $ [i|#{model}: #{cores} cores @ #{clock}|] diff --git a/agent/src/Lib/External/Specs/Common.hs b/agent/src/Lib/External/Specs/Common.hs new file mode 100644 index 000000000..df68ab37d --- /dev/null +++ b/agent/src/Lib/External/Specs/Common.hs @@ -0,0 +1,13 @@ +module Lib.External.Specs.Common where + +import Startlude + +import qualified Data.Text as T + +getSpec :: Text -> Text -> Maybe Text +getSpec spec output = do + mi <- modelItem + fmap T.strip $ T.splitOn ":" mi `atMay` 1 + where + items = lines output + modelItem = find (spec `T.isPrefixOf`) items diff --git a/agent/src/Lib/External/Specs/Memory.hs b/agent/src/Lib/External/Specs/Memory.hs new file mode 100644 index 000000000..0bfc20a17 --- /dev/null +++ b/agent/src/Lib/External/Specs/Memory.hs @@ -0,0 +1,12 @@ +module Lib.External.Specs.Memory where + +import Startlude +import Protolude.Unsafe ( unsafeFromJust ) + +import Lib.External.Specs.Common + +catMem :: IO Text +catMem = readFile "/proc/meminfo" + +getMem :: IO Text +getMem = unsafeFromJust . getSpec "MemTotal" <$> catMem diff --git a/agent/src/Lib/External/Util.hs b/agent/src/Lib/External/Util.hs new file mode 100644 index 000000000..17a8ce12b --- /dev/null +++ b/agent/src/Lib/External/Util.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE TupleSections #-} +module Lib.External.Util where + +import Startlude + +getLineByHeader :: Text -> [Text] -> Maybe Text +getLineByHeader t = find (isPrefixOf (toS t :: String) . toS) + +truncateTo :: RealFloat a => Int -> a -> Double +truncateTo n x = realToFrac $ fromIntegral (floor (x * t) :: Integer) / t where t = 10 ^ n + +truncateToS :: Int -> Double -> Double +truncateToS n x = fromIntegral (floor (x * t) :: Integer) / t where t = 10 ^ n + +parseToPair :: (Text -> Maybe a) -> [Text] -> Maybe (Text, a) +parseToPair parse [k, v] = ((k, ) <$> parse v) <|> ((v, ) <$> parse k) +parseToPair _ _ = Nothing diff --git a/agent/src/Lib/External/WpaSupplicant.hs b/agent/src/Lib/External/WpaSupplicant.hs new file mode 100644 index 000000000..1e961eab2 --- /dev/null +++ b/agent/src/Lib/External/WpaSupplicant.hs @@ -0,0 +1,102 @@ +{-# LANGUAGE QuasiQuotes #-} +module Lib.External.WpaSupplicant where + +import Startlude + +import Data.Bitraversable +import qualified Data.HashMap.Strict as HM +import Data.String.Interpolate.IsString +import qualified Data.Text as T +import System.Process +import Control.Concurrent.Async.Lifted + as LAsync +import Control.Monad.Trans.Control ( MonadBaseControl ) + +runWlan0 :: ReaderT Text m a -> m a +runWlan0 = flip runReaderT "wlan0" + +isConnectedToEthernet :: MonadIO m => m Bool +isConnectedToEthernet = do + liftIO $ not . null . filter (T.isInfixOf "inet ") . lines . toS <$> readProcess "ifconfig" ["eth0"] "" + +-- There be bug here: if you're in the US, and add a network in Sweden, you'll set your wpa supplicant to be looking for networks in Sweden. +-- so you won't be autoconnecting to anything in the US till you add another US guy. +addNetwork :: MonadIO m => Text -> Text -> Text -> ReaderT Interface m () +addNetwork ssid psk country = do + interface <- ask + networkId <- checkNetwork ssid >>= \case + -- If the network already exists, we will update its password. + Just nId -> do + void . liftIO $ readProcess "wpa_cli" ["-i", toS interface, "new_password", toS nId, [i|"#{psk}"|]] "" + pure nId + + -- Otherwise we create the network in the wpa_supplicant + Nothing -> do + nId <- liftIO $ T.strip . toS <$> readProcess "wpa_cli" ["-i", toS interface, "add_network"] "" + void . liftIO $ readProcess "wpa_cli" + ["-i", toS interface, "set_network", toS nId, "ssid", [i|"#{ssid}"|]] + "" + void . liftIO $ readProcess "wpa_cli" ["-i", toS interface, "set_network", toS nId, "psk", [i|"#{psk}"|]] "" + void . liftIO $ readProcess "wpa_cli" ["-i", toS interface, "set_network", toS nId, "scan_ssid", "1"] "" + pure nId + + void . liftIO $ readProcess "wpa_cli" ["-i", toS interface, "set", "country", toS country] "" + void . liftIO $ readProcess "wpa_cli" ["-i", toS interface, "enable_network", toS networkId] "" + void . liftIO $ readProcess "wpa_cli" ["-i", toS interface, "save_config"] "" + +removeNetwork :: MonadIO m => Text -> ReaderT Interface m () +removeNetwork ssid = do + interface <- ask + checkNetwork ssid >>= \case + Nothing -> pure () + Just x -> liftIO $ do + void $ readProcess "wpa_cli" ["-i", toS interface, "remove_network", [i|#{x}|]] "" + void $ readProcess "wpa_cli" ["-i", toS interface, "save_config"] "" + void $ readProcess "wpa_cli" ["-i", toS interface, "reconfigure"] "" + +listNetworks :: MonadIO m => ReaderT Interface m [Text] +listNetworks = do + interface <- ask + liftIO $ mapMaybe (`atMay` 1) . drop 1 . fmap (T.splitOn "\t") . lines . toS <$> readProcess + "wpa_cli" + ["-i", toS interface, "list_networks"] + "" + +type Interface = Text +getCurrentNetwork :: (MonadBaseControl IO m, MonadIO m) => ReaderT Interface m (Maybe Text) +getCurrentNetwork = do + interface <- ask @Text + liftIO $ guarded (/= "") . T.init . toS <$> readProcess "iwgetid" [toS interface, "--raw"] "" + +selectNetwork :: (MonadBaseControl IO m, MonadIO m) => Text -> Text -> ReaderT Interface m Bool +selectNetwork ssid country = checkNetwork ssid >>= \case + Nothing -> putStrLn @Text "SSID Not Found" *> pure False + Just nId -> do + interface <- ask + void . liftIO $ readProcess "wpa_cli" ["-i", toS interface, "select_network", toS nId] "" + void . liftIO $ readProcess "wpa_cli" ["-i", toS interface, "set", "country", toS country] "" + void . liftIO $ readProcess "wpa_cli" ["-i", toS interface, "save_config"] "" + mNew <- join . hush <$> LAsync.race (liftIO $ threadDelay 20_000_000) + (runMaybeT . asum $ repeat (MaybeT getCurrentNetwork)) + listNetworks >>= \nets -> + for_ nets $ \net -> liftIO $ readProcess "wpa_cli" ["-i", toS interface, "enable_network", toS net] "" + pure $ case mNew of + Nothing -> False + Just newCurrent -> newCurrent == ssid + +type NetworkId = Text +checkNetwork :: MonadIO m => Text -> ReaderT Interface m (Maybe NetworkId) +checkNetwork ssid = do + interface <- ask + HM.lookup ssid + . HM.fromList + . mapMaybe (bisequenceA . ((`atMay` 1) &&& (`atMay` 0))) + . drop 1 + . fmap (T.splitOn "\t") + . lines + . toS + <$> liftIO (readProcess "wpa_cli" ["-i", toS interface, "list_networks"] "") + +-- TODO: Live Testing in GHCI +runWpa :: ReaderT Interface m a -> m a +runWpa = flip runReaderT "wlp5s0" diff --git a/agent/src/Lib/IconCache.hs b/agent/src/Lib/IconCache.hs new file mode 100644 index 000000000..a0685aafb --- /dev/null +++ b/agent/src/Lib/IconCache.hs @@ -0,0 +1,94 @@ +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +module Lib.IconCache where + +import Startlude hiding ( ask + , catch + , throwIO + , Reader + ) + +import Conduit +import Control.Concurrent.STM.TVar +import Control.Effect.Reader.Labelled +import Crypto.Hash +import qualified Data.Conduit.Binary as CB +import qualified Data.HashMap.Strict as HM +import Data.String.Interpolate.IsString +import Network.HTTP.Simple +import System.Directory +import System.FilePath +import System.IO.Error +import UnliftIO.Exception + +import Lib.Error +import Lib.SystemPaths hiding ( () ) +import Lib.Types.Core +import Database.Persist.Sql ( runSqlPool + , repsert + , ConnectionPool + , delete + ) +import Model +import Control.Effect.Error +import Crypto.Hash.Conduit ( hashFile ) +import Util.File ( removeFileIfExists ) + +type HasIconTags sig m = HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m + +findIcon :: (HasFilesystemBase sig m, MonadIO m) => AppId -> m (Maybe FilePath) +findIcon appId = do + bp <- toS <$> getAbsoluteLocationFor iconBasePath + icons <- liftIO $ (listDirectory bp) `catch` \(e :: IOException) -> + if isDoesNotExistError e then createDirectoryIfMissing True bp *> pure [] else throwIO e + pure $ (bp ) <$> find ((show appId ==) . takeBaseName) icons + +saveIcon :: ( HasFilesystemBase sig m + , HasIconTags sig m + , HasLabelled "databaseConnection" (Reader ConnectionPool) sig m + , Has (Error S9Error) sig m + , MonadIO m + ) + => String + -> m () +saveIcon url = do + bp <- toS <$> getAbsoluteLocationFor iconBasePath + req <- case parseRequest url of + Nothing -> throwError $ RegistryParseE (toS url) "invalid url" + Just x -> pure x + let saveAction = runConduit $ httpSource req getResponseBody .| CB.sinkFileCautious (bp takeFileName url) + liftIO $ runResourceT $ saveAction `catch` \(e :: IOException) -> if isDoesNotExistError e + then do + liftIO $ createDirectoryIfMissing True bp + saveAction + else throwIO e + tag <- hashFile (bp takeFileName url) + saveTag (AppId . toS $ takeFileName url) tag + +saveTag :: (HasIconTags sig m, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m, MonadIO m) + => AppId + -> Digest MD5 + -> m () +saveTag appId tag = do + cache <- ask @"iconTagCache" + pool <- ask @"databaseConnection" + liftIO $ runSqlPool (repsert (IconDigestKey appId) (IconDigest tag)) pool `catch` \(e :: SomeException) -> + putStrLn @Text [i|Icon Cache Insertion Failed!: #{appId}, #{tag}, #{e}|] + liftIO $ atomically $ modifyTVar cache $ HM.insert appId tag + +clearIcon :: ( MonadIO m + , HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId v0))) sig m + , HasLabelled "databaseConnection" (Reader ConnectionPool) sig m + , HasLabelled "filesystemBase" (Reader Text) sig m + ) + => AppId + -> m () +clearIcon appId = do + db <- ask @"databaseConnection" + iconTags <- ask @"iconTagCache" + liftIO . atomically $ modifyTVar iconTags (HM.delete appId) + liftIO $ runSqlPool (delete (IconDigestKey appId)) db + findIcon appId >>= \case + Nothing -> pure () + Just x -> removeFileIfExists x diff --git a/agent/src/Lib/Metrics.hs b/agent/src/Lib/Metrics.hs new file mode 100644 index 000000000..21eac32db --- /dev/null +++ b/agent/src/Lib/Metrics.hs @@ -0,0 +1,158 @@ +{-# LANGUAGE RecordWildCards #-} + +module Lib.Metrics where + +import Startlude + +import Data.Aeson +import Data.IORef + +import Foundation +import Lib.Error +import Lib.External.Metrics.Df +import Lib.External.Metrics.Iotop +import Lib.External.Metrics.ProcDev +import Lib.External.Metrics.Temperature +import Lib.External.Metrics.Top +import Lib.External.Metrics.Types + +-- will throw only if one of '$ top', '$ iotop, '$ procDev' commands fails on the command line. +getServerMetrics :: MonadIO m => AgentCtx -> S9ErrT m ServerMetrics +getServerMetrics agentCtx = do + temp <- getTemperature + df <- getDfMetrics + top <- getTopMetrics + iotop <- getIotopMetrics + (_, _, procDev) <- liftIO . readIORef . appProcDevMomentCache $ agentCtx + + pure $ fromCommandLineMetrics (temp, df, top, iotop, procDev) + +data ServerMetrics = ServerMetrics + { serverMetricsTemperature :: Maybe Celsius + + , serverMetricMemPercentageUsed :: Maybe Percentage + , serverMetricMemFree :: Maybe MebiBytes + , serverMetricMemUsed :: Maybe MebiBytes + , serverMetricSwapTotal :: Maybe MebiBytes + , serverMetricSwapUsed :: Maybe MebiBytes + + , serverMetricCpuIdle :: Maybe Percentage + , serverMetricCpuUserSpace :: Maybe Percentage + , serverMetricWait :: Maybe Percentage + , serverMetricCpuPercentageUsed :: Maybe Percentage + + , serverMetricCurrentRead :: Maybe BytesPerSecond + , serverMetricCurrentWrite :: Maybe BytesPerSecond + , serverMetricTotalRead :: Maybe BytesPerSecond + , serverMetricTotalWrite :: Maybe BytesPerSecond + + , serverMetricRBytesPerSecond :: Maybe BytesPerSecond + , serverMetricRPacketsPerSecond :: Maybe BytesPerSecond + , serverMetricRErrorsPerSecond :: Maybe BytesPerSecond + , serverMetricTBytesPerSecond :: Maybe BytesPerSecond + , serverMetricTPacketsPerSecond :: Maybe BytesPerSecond + , serverMetricTErrorsPerSecond :: Maybe BytesPerSecond + + , serverMetricDiskSize :: Maybe Gigabytes + , serverMetricDiskUsed :: Maybe Gigabytes + , serverMetricDiskAvailable :: Maybe Gigabytes + , serverMetricDiskUsedPercentage :: Maybe Percentage + } deriving (Eq, Show) + +instance ToJSON ServerMetrics where + toJSON ServerMetrics {..} = object + [ "GENERAL" .= object ["Temperature" .= serverMetricsTemperature] + , "MEMORY" .= object + [ "Percent Used" .= serverMetricMemPercentageUsed + , "Free" .= serverMetricMemFree + , "Used" .= serverMetricMemUsed + , "Swap Used" .= serverMetricSwapUsed + , "Swap Free" .= serverMetricSwapTotal ?-? serverMetricSwapUsed + ] + , "CPU" .= object + [ "Percent Used" .= serverMetricCpuPercentageUsed + , "Percent Free" .= serverMetricCpuIdle + , "Percent User Space" .= serverMetricCpuUserSpace + , "Percent IO Wait" .= serverMetricWait + ] + , "DISK" .= object + [ "Percent Used" .= serverMetricDiskUsedPercentage + , "Size" .= serverMetricDiskSize + , "Used" .= serverMetricDiskUsed + , "Free" .= serverMetricDiskAvailable + , "Total Read" .= serverMetricTotalRead + , "Total Write" .= serverMetricTotalWrite + , "Current Read" .= serverMetricCurrentRead + , "Current Write" .= serverMetricCurrentWrite + ] + , "NETWORK" .= object + [ "Bytes Received" .= serverMetricRBytesPerSecond + , "Packets Received" .= serverMetricRPacketsPerSecond + , "Errors Received" .= serverMetricRErrorsPerSecond + , "Bytes Transmitted" .= serverMetricTBytesPerSecond + , "Packets Transmitted" .= serverMetricTPacketsPerSecond + , "Errors Transmitted" .= serverMetricTErrorsPerSecond + ] + ] + toEncoding ServerMetrics {..} = (pairs . fold) + [ "GENERAL" .= object ["Temperature" .= serverMetricsTemperature] + , "MEMORY" .= object + [ "Percent Used" .= serverMetricMemPercentageUsed + , "Free" .= serverMetricMemFree + , "Used" .= serverMetricMemUsed + , "Swap Used" .= serverMetricSwapUsed + , "Swap Free" .= serverMetricSwapTotal ?-? serverMetricSwapUsed + ] + , "CPU" .= object + [ "Percent Used" .= serverMetricCpuPercentageUsed + , "Percent Free" .= serverMetricCpuIdle + , "Percent User Space" .= serverMetricCpuUserSpace + , "Percent IO Wait" .= serverMetricWait + ] + , "DISK" .= object + [ "Percent Used" .= serverMetricDiskUsedPercentage + , "Size" .= serverMetricDiskSize + , "Used" .= serverMetricDiskUsed + , "Free" .= serverMetricDiskAvailable + , "Total Read" .= serverMetricTotalRead + , "Total Write" .= serverMetricTotalWrite + , "Current Read" .= serverMetricCurrentRead + , "Current Write" .= serverMetricCurrentWrite + ] + , "NETWORK" .= object + [ "Bytes Received" .= serverMetricRBytesPerSecond + , "Packets Received" .= serverMetricRPacketsPerSecond + , "Errors Received" .= serverMetricRErrorsPerSecond + , "Bytes Transmitted" .= serverMetricTBytesPerSecond + , "Packets Transmitted" .= serverMetricTPacketsPerSecond + , "Errors Transmitted" .= serverMetricTErrorsPerSecond + ] + ] + +fromCommandLineMetrics :: (Maybe Celsius, DfMetrics, TopMetrics, IotopMetrics, ProcDevMetrics) -> ServerMetrics +fromCommandLineMetrics (temp, DfMetrics {..}, TopMetrics {..}, IotopMetrics {..}, ProcDevMetrics {..}) = ServerMetrics + { serverMetricsTemperature = temp + , serverMetricMemPercentageUsed = metricMemPercentageUsed + , serverMetricMemFree = metricMemFree + , serverMetricMemUsed = metricMemUsed + , serverMetricSwapTotal = metricSwapTotal + , serverMetricSwapUsed = metricSwapUsed + , serverMetricCpuIdle = metricCpuIdle + , serverMetricCpuUserSpace = metricCpuUserSpace + , serverMetricWait = metricWait + , serverMetricCpuPercentageUsed = metricCpuPercentageUsed + , serverMetricCurrentRead = metricCurrentRead + , serverMetricCurrentWrite = metricCurrentWrite + , serverMetricTotalRead = metricTotalRead + , serverMetricTotalWrite = metricTotalWrite + , serverMetricRBytesPerSecond = metricRBytesPerSecond + , serverMetricRPacketsPerSecond = metricRPacketsPerSecond + , serverMetricRErrorsPerSecond = metricRErrorsPerSecond + , serverMetricTBytesPerSecond = metricTBytesPerSecond + , serverMetricTPacketsPerSecond = metricTPacketsPerSecond + , serverMetricTErrorsPerSecond = metricTErrorsPerSecond + , serverMetricDiskSize = metricDiskSize + , serverMetricDiskUsed = metricDiskUsed + , serverMetricDiskAvailable = metricDiskAvailable + , serverMetricDiskUsedPercentage = metricDiskUsedPercentage + } diff --git a/agent/src/Lib/Migration.hs b/agent/src/Lib/Migration.hs new file mode 100644 index 000000000..f6585c3a8 --- /dev/null +++ b/agent/src/Lib/Migration.hs @@ -0,0 +1,96 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} + +module Lib.Migration where + +import Data.Aeson +import Data.Aeson.Types +import Data.FileEmbed +import Data.Text ( split + , splitOn + , strip + ) +import Database.Persist.Sql +import Lib.Error +import Lib.Types.Emver +import Model +import Startlude + +ioMigrationDbVersion :: ConnectionPool -> Version -> Version -> IO () +ioMigrationDbVersion dbConn sourceVersion targetVersion = do + putStrLn @Text $ "Executing migrations from " <> show sourceVersion <> " to " <> show targetVersion + runSqlPool (migrateDbVersions sourceVersion targetVersion & handleS9ErrNuclear) dbConn + +getCurrentDbVersion :: MonadIO m => ReaderT SqlBackend m (Maybe Version) +getCurrentDbVersion = + fmap (executedMigrationTgtVersion . entityVal) <$> selectFirst [] [Desc ExecutedMigrationCreatedAt] + +getMigrations :: [MigrationFile] +getMigrations = mapMaybe toMigrationFile $(embedDir "./migrations") + +migrateDbVersions :: MonadIO m => Version -> Version -> S9ErrT (ReaderT SqlBackend m) () +migrateDbVersions sourceVersion targetVersion = case mkMigrationCollection sourceVersion targetVersion getMigrations of + Just (MigrationCollection migrations) -> lift $ traverse executeMigration migrations $> () + Nothing -> + throwE . PersistentE $ "No path of migrations from " <> show sourceVersion <> " to " <> show targetVersion + +executeMigration :: MonadIO m => MigrationFile -> ReaderT SqlBackend m () +executeMigration mf = migrateSql mf >> insertMigration mf $> () + +insertMigration :: MonadIO m => MigrationFile -> ReaderT SqlBackend m (Key ExecutedMigration) +insertMigration (MigrationFile source target _) = do + now <- liftIO getCurrentTime + fmap entityKey . insertEntity $ ExecutedMigration now now source target + +migrateSql :: MonadIO m => MigrationFile -> ReaderT SqlBackend m () +migrateSql MigrationFile { sqlContent } = do + print sqlContent' + traverse_ runIt sqlContent' + where + runIt = liftA2 (*>) (liftIO . putStrLn) $ flip (rawSql @(Single Int)) [] . (<> ";") . strip + sqlContent' = filter (/= "") . fmap strip . split (== ';') $ decodeUtf8 sqlContent + +toMigrationFile :: (FilePath, ByteString) -> Maybe MigrationFile +toMigrationFile (fp, bs) = case splitOn "::" (toS fp) of + [source, target] -> do + sourceVersion <- parseMaybe parseJSON $ String source + targetVersion <- parseMaybe parseJSON $ String target + let sqlContent = bs + pure MigrationFile { .. } + _ -> Nothing + +newtype MigrationCollection = MigrationCollection { unMigrations :: [MigrationFile] } deriving (Eq, Show) +mkMigrationCollection :: Version -> Version -> [MigrationFile] -> Maybe MigrationCollection +mkMigrationCollection source target migrations + | null migrations + = Nothing + | source == target + = Just $ MigrationCollection [] + | otherwise + = let mNext = maximumByMay targetVersion $ filter + (\m -> sourceVersion m == source && targetVersion m > source && targetVersion m <= target) + migrations + in case mNext of + Nothing -> Nothing + Just nextMig -> + MigrationCollection + . (nextMig :) + . unMigrations + <$> mkMigrationCollection (targetVersion nextMig) target migrations + where + maximumByMay :: (Foldable t, Ord b) => (a -> b) -> t a -> Maybe a + maximumByMay f as = + let reducer x acc = case acc of + Nothing -> Just x + Just y -> if f x > f y then Just x else Just y + in foldr reducer Nothing as + +data MigrationFile = MigrationFile + { sourceVersion :: Version + , targetVersion :: Version + , sqlContent :: ByteString + } + deriving (Eq, Show) diff --git a/agent/src/Lib/Notifications.hs b/agent/src/Lib/Notifications.hs new file mode 100644 index 000000000..7e826e1cd --- /dev/null +++ b/agent/src/Lib/Notifications.hs @@ -0,0 +1,109 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +module Lib.Notifications where + +import Startlude hiding ( get ) + +import Data.String.Interpolate.IsString +import Data.UUID.V4 +import Database.Persist +import Database.Persist.Sql + +import Lib.Error +import Lib.Types.Core +import Lib.Types.Emver +import Model + +emit :: MonadIO m => AppId -> Version -> AgentNotification -> SqlPersistT m (Entity Notification) +emit appId version ty = do + uuid <- liftIO nextRandom + now <- liftIO getCurrentTime + let k = (NotificationKey uuid) + let v = (Notification now Nothing appId version (toCode ty) (toTitle ty) (toMessage appId version ty)) + insertKey k v + putStrLn $ toMessage appId version ty + pure $ Entity k v + +archive :: MonadIO m => [Key Notification] -> SqlPersistT m [Entity Notification] +archive eventIds = do + now <- liftIO getCurrentTime + events <- for eventIds $ flip updateGet [NotificationArchivedAt =. Just now] + pure $ zipWith Entity eventIds events + +data AgentNotification = + InstallSuccess + | InstallFailedGetApp + | InstallFailedAppMgrExitCode Int + | InstallFailedS9Error S9Error + | BackupSucceeded + | BackupFailed S9Error + | RestoreSucceeded + | RestoreFailed S9Error + | RestartFailed S9Error + | DockerFuckening + +-- CODES +-- RULES: +-- The first digit indicates the call to action and the tone of the error code as follows +-- 0: General Information, No Action Required, Neutral Tone +-- 1: Success Message, No Action Required, Positive Tone +-- 2: Warning, Action Possible but NOT Required, Negative Tone +-- 3: Error, Action Required, Negative Tone +-- +-- The second digit indicates where the error was originated from as follows +-- 0: Originates from Agent +-- 1: Originates from App (Not presently used) +-- +-- The remaining section of the code may be as long as you want but must be at least one digit +-- EXAMPLES: +-- 100 +-- |||> Code "0" +-- ||> Originates from Agent +-- |> Success Message +-- +-- 213 +-- |||> Code "3" +-- ||> Originates from App +-- |> Warning Message +-- +toCode :: AgentNotification -> Text +toCode InstallSuccess = "100" +toCode BackupSucceeded = "101" +toCode RestoreSucceeded = "102" +toCode InstallFailedGetApp = "300" +toCode (InstallFailedAppMgrExitCode _) = "301" +toCode DockerFuckening = "302" +toCode (InstallFailedS9Error _) = "303" +toCode (BackupFailed _) = "304" +toCode (RestoreFailed _) = "305" +toCode (RestartFailed _) = "306" + +toTitle :: AgentNotification -> Text +toTitle InstallSuccess = "Install succeeded" +toTitle BackupSucceeded = "Backup succeeded" +toTitle RestoreSucceeded = "Restore succeeded" +toTitle InstallFailedGetApp = "Install failed" +toTitle (InstallFailedAppMgrExitCode _) = "Install failed" +toTitle (InstallFailedS9Error _) = "Install failed" +toTitle (BackupFailed _) = "Backup failed" +toTitle (RestoreFailed _) = "Restore failed" +toTitle (RestartFailed _) = "Restart failed" +toTitle DockerFuckening = "App unstoppable" + +toMessage :: AppId -> Version -> AgentNotification -> Text +toMessage appId version InstallSuccess = [i|Successfully installed #{appId} at version #{version}|] +toMessage appId version n@InstallFailedGetApp = + [i|Failed to install #{appId} at version #{version}, this should be impossible, contact support and give them the code #{toCode n}|] +toMessage appId version n@(InstallFailedAppMgrExitCode ec) + = [i|Failed to install #{appId} at version #{version}, many things could cause this, contact support and give them the code #{toCode n}.#{ec}|] +toMessage appId version n@(InstallFailedS9Error e) + = [i|Failed to install #{appId} at version #{version}, the dependency reverse index could not be updated, contact support and give them the code #{toCode n}.#{errorCode $ toError e}|] +toMessage appId _version DockerFuckening + = [i|Despite attempting to stop #{appId}, it is still running. This is a known issue that can only be solved by restarting the server|] +toMessage appId _version BackupSucceeded = [i|Successfully backed up #{appId}|] +toMessage appId _version RestoreSucceeded = [i|Successfully restored #{appId}|] +toMessage appId _version (BackupFailed reason) = [i|Failed to back up #{appId}: #{errorMessage $ toError reason}|] +toMessage appId _version (RestoreFailed reason) = [i|Failed to restore #{appId}: #{errorMessage $ toError reason}|] +toMessage appId _version (RestartFailed reason) = + [i|Failed to restart #{appId}: #{errorMessage $ toError reason}. Please manually restart|] diff --git a/agent/src/Lib/Password.hs b/agent/src/Lib/Password.hs new file mode 100644 index 000000000..9d3cc6454 --- /dev/null +++ b/agent/src/Lib/Password.hs @@ -0,0 +1,77 @@ +module Lib.Password where + +import Startlude +import Yesod.Auth.Util.PasswordStore ( makePassword + , verifyPassword + , passwordStrength + ) +import qualified Data.ByteString.Char8 as BS + ( pack + , unpack + ) +import Data.Text ( pack + , unpack + ) + +import Model + +-- Root account identifier +rootAccountName :: Text +rootAccountName = "embassy-root" + + +-- | Default strength used for passwords (see "Yesod.Auth.Util.PasswordStore" +-- for details). +defaultStrength :: Int +defaultStrength = 17 + +-- | The type representing account information stored in the database should +-- be an instance of this class. It just provides the getter and setter +-- used by the functions in this module. +class HasPasswordHash account where + getPasswordHash :: account -> Text + setPasswordHash :: Text -> account -> account + + {-# MINIMAL getPasswordHash, setPasswordHash #-} + + +-- | Calculate a new-style password hash using "Yesod.Auth.Util.PasswordStore". +passwordHash :: MonadIO m => Int -> Text -> m Text +passwordHash strength pwd = do + h <- liftIO $ makePassword (BS.pack $ unpack pwd) strength + return $ pack $ BS.unpack h + +-- | Set password for account, using the given strength setting. Use this +-- function, or 'setPassword', to produce a account record containing the +-- hashed password. Unlike previous versions of this module, no separate +-- salt field is required for new passwords (but it may still be required +-- for compatibility while old password hashes remain in the database). +-- +-- This function does not change the database; the calling application +-- is responsible for saving the data which is returned. +setPasswordStrength :: (MonadIO m, HasPasswordHash account) => Int -> Text -> account -> m account +setPasswordStrength strength pwd u = do + hashed <- passwordHash strength pwd + return $ setPasswordHash hashed u + +-- | As 'setPasswordStrength', but using the 'defaultStrength' +setPassword :: (MonadIO m, HasPasswordHash account) => Text -> account -> m account +setPassword = setPasswordStrength defaultStrength + +validatePass :: HasPasswordHash u => u -> Text -> Bool +validatePass account password = do + let h = getPasswordHash account + -- NB plaintext password characters are truncated to 8 bits here, + -- and also in passwordHash above (the hash is already 8 bit). + -- This is for historical compatibility, but in practice it is + -- unlikely to reduce the entropy of most users' alphabets by much. + let hash' = BS.pack $ unpack h + password' = BS.pack $ unpack password + if passwordStrength hash' > 0 + -- Will give >0 for valid hash format, else treat as if wrong password + then verifyPassword password' hash' + else False + +instance HasPasswordHash Account where + getPasswordHash = accountPassword + setPasswordHash h u = u { accountPassword = h } diff --git a/agent/src/Lib/ProductKey.hs b/agent/src/Lib/ProductKey.hs new file mode 100644 index 000000000..729197e9f --- /dev/null +++ b/agent/src/Lib/ProductKey.hs @@ -0,0 +1,12 @@ +module Lib.ProductKey where + +import Startlude +import Protolude.Unsafe ( unsafeHead ) + +import System.FilePath + +productKeyPath :: FilePath -> FilePath +productKeyPath rt = rt "root/agent/product_key" + +getProductKey :: Text -> IO Text +getProductKey rt = unsafeHead . lines <$> readFile (productKeyPath $ toS rt) diff --git a/agent/src/Lib/SelfUpdate.hs b/agent/src/Lib/SelfUpdate.hs new file mode 100644 index 000000000..36ee8d904 --- /dev/null +++ b/agent/src/Lib/SelfUpdate.hs @@ -0,0 +1,226 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +module Lib.SelfUpdate where + +import Startlude hiding ( runReader ) + +import Control.Carrier.Error.Either +import Control.Lens +import Data.Aeson +import qualified Data.ByteString.Char8 as B8 +import Data.IORef +import Data.List +import Data.String.Interpolate.IsString +import System.Posix.Files +import System.Process + +import Constants +import Foundation +import Handler.Types.V0.Base +import Lib.Algebra.State.RegistryUrl +import Lib.Error +import Lib.External.Registry +import Lib.Sound as Sound +import Lib.Synchronizers +import Lib.SystemPaths +import Lib.Types.Emver +import Lib.WebServer +import Settings + +youngAgentPort :: Word16 +youngAgentPort = 5960 + +waitForUpdateSignal :: AgentCtx -> IO () +waitForUpdateSignal foundation = do + eNewVersion <- runS9ErrT $ do + spec <- lift . takeMVar . appSelfUpdateSpecification $ foundation + let settings = appSettings foundation + v <- interp settings (getLatestAgentVersionForSpec spec) >>= \case + Nothing -> throwE $ UpdateSelfE GetLatestCompliantVersion "Not Found" + Just v -> pure v + liftIO $ writeIORef (appIsUpdating foundation) (Just v) + updateAgent foundation spec + case eNewVersion of + Right (newVersion, youngAgentProcess) -> do + putStrLn @Text $ "New agent up and running: " <> show newVersion + runReaderT replaceExecutableWithYoungAgent (appSettings foundation) + killYoungAgent youngAgentProcess + shutdownAll [] + Left e@(UpdateSelfE GetYoungAgentBinary _) -> do + logerror e + writeIORef (appIsUpdating foundation) Nothing + waitForNextUpdateSignal + Left e@(UpdateSelfE ShutdownWeb _) -> do + logerror e + writeIORef (appIsUpdating foundation) Nothing + waitForNextUpdateSignal + Left e@(UpdateSelfE StartupYoungAgent _) -> do + logerror e + writeIORef (appIsUpdating foundation) Nothing + waitForNextUpdateSignal + Left e@(UpdateSelfE (PingYoungAgent youngAgentProcess) _) -> do + logerror e + killYoungAgent youngAgentProcess + writeIORef (appIsUpdating foundation) Nothing + waitForNextUpdateSignal + Left e -> do -- unreachable + logerror e + waitForNextUpdateSignal + where + waitForNextUpdateSignal = waitForUpdateSignal foundation + logerror = putStrLn @Text . show + interp s = ExceptT . liftIO . runError . injectFilesystemBaseFromContext s . runRegistryUrlIOC + + +updateAgent :: AgentCtx -> VersionRange -> S9ErrT IO (Version, ProcessHandle) +updateAgent foundation avs = do + -- get and save the binary of the new agent app + putStrLn @Text $ "Acquiring young agent binary for specification: " <> show avs + (tryTo . interp settings . getYoungAgentBinary $ avs) >>= \case + Left e -> throwE $ UpdateSelfE GetYoungAgentBinary (show e) + Right _ -> putStrLn @Text "Succeeded" + + -- start the new agent app. This is non blocking as a success would block indefinitely + startupYoungAgentProcessHandle <- startup 5 + + putStrLn @Text $ "Beginning young agent ping attempts..." + let attemptPing = do + lift (threadDelay delayBetweenAttempts) + tryTo pingYoungAgent >>= \case + Left e -> do + putStrLn @Text (show e) + pure (Left e) + x -> pure x + retryAction attempts attemptPing >>= \case + Left e -> throwE $ UpdateSelfE (PingYoungAgent startupYoungAgentProcessHandle) (show e) + Right av -> putStrLn @Text "Succeeded" >> pure (av, startupYoungAgentProcessHandle) + where + tryTo = lift . try @SomeException + settings = appSettings foundation + attempts = 8 + delayBetweenAttempts = 5 * 1000000 :: Int -- 5 seconds + startup :: Int -> S9ErrT IO ProcessHandle + startup startupAttempts = do + putStrLn @Text $ "Starting up young agent..." + tryTo (runReaderT startupYoungAgent $ appSettings foundation) >>= \case + Left e -> if "busy" `isInfixOf` show e && startupAttempts > 0-- sometimes the file handle hasn't closed yet + then do + putStrLn @Text "agent-tmp busy, reattempting in 500ms" + liftIO (threadDelay 500_000) + startup (startupAttempts - 1) + else do + putStrLn @Text (show e) + throwE $ UpdateSelfE StartupYoungAgent (show e) + Right ph -> putStrLn @Text "Succeeded" >> pure ph + interp s = liftIO . injectFilesystemBaseFromContext s . injectFilesystemBaseFromContext s . runRegistryUrlIOC + + + +retryAction :: Monad m => Integer -> m (Either e a) -> m (Either e a) +retryAction 1 action = action +retryAction maxTries action = do + success <- action + case success of + Right a -> pure $ Right a + Left _ -> retryAction (maxTries - 1) action + +replaceExecutableWithYoungAgent :: (MonadReader AppSettings m, MonadIO m) => m () +replaceExecutableWithYoungAgent = do + rt <- asks appFilesystemBase + let tmpAgent = (executablePath `relativeTo` rt) tmpAgentFileName + let agent = (executablePath `relativeTo` rt) agentFileName + + liftIO $ removeLink (toS agent) + liftIO $ rename (toS tmpAgent) (toS agent) + + +-- We assume that all app versions must listen on the same port. +youngAgentUrl :: Text +youngAgentUrl = "http://localhost:" <> show youngAgentPort + +pingYoungAgent :: IO Version +pingYoungAgent = do + (code, st_out, st_err) <- readProcessWithExitCode "curl" [toS $ toS youngAgentUrl "version"] "" + putStrLn st_out + putStrLn st_err + case code of + ExitSuccess -> case decodeStrict $ B8.pack st_out of + Nothing -> throwIO . InternalS9Error $ "unparseable version: " <> toS st_out + Just (AppVersionRes av) -> pure av + ExitFailure e -> throwIO . InternalS9Error $ "curl failure with exit code: " <> show e + +startupYoungAgent :: (MonadReader AppSettings m, MonadIO m) => m ProcessHandle +startupYoungAgent = do + rt <- asks appFilesystemBase + let cmd = (proc (toS $ (executablePath `relativeTo` rt) tmpAgentFileName) ["--port", show youngAgentPort]) + { create_group = True + } + ph <- liftIO $ view _4 <$> createProcess cmd + liftIO $ threadDelay 1_000_000 -- 1 second + liftIO $ getProcessExitCode ph >>= \case + Nothing -> pure ph + Just e -> throwIO . InternalS9Error $ "young agent exited prematurely with exit code: " <> show e + +killYoungAgent :: ProcessHandle -> IO () +killYoungAgent p = do + mEC <- getProcessExitCode p + case mEC of + Nothing -> interruptProcessGroupOf p + Just _ -> pure () + threadDelay appEndEstimate + where appEndEstimate = 10 * 1000000 :: Int --10 seconds + +runSyncOps :: [SyncOp] -> ReaderT AgentCtx IO [(Bool, Bool)] +runSyncOps syncOps = do + ctx <- ask + let setUpdate b = if b + then liftIO $ writeIORef (appIsUpdating ctx) (Just agentVersion) + else liftIO $ writeIORef (appIsUpdating ctx) Nothing + res <- for syncOps $ \syncOp -> do + shouldRun <- syncOpShouldRun syncOp + putStrLn @Text [i|Sync Op "#{syncOpName syncOp}" should run: #{shouldRun}|] + when shouldRun $ do + putStrLn @Text [i|Running Sync Op: #{syncOpName syncOp}|] + setUpdate True + syncOpRun syncOp + pure $ (syncOpRequiresReboot syncOp, shouldRun) + setUpdate False + pure res + +synchronizeSystemState :: AgentCtx -> Version -> IO () +synchronizeSystemState ctx _version = handle @SomeException cleanup $ flip runReaderT ctx $ do + (restartsAndRuns, mTid) <- case synchronizer of + Synchronizer { synchronizerOperations } -> flip runStateT Nothing $ for synchronizerOperations $ \syncOp -> do + shouldRun <- lift $ syncOpShouldRun syncOp + putStrLn @Text [i|Sync Op "#{syncOpName syncOp}" should run: #{shouldRun}|] + when shouldRun $ do + whenM (isNothing <$> get) $ do + tid <- liftIO . forkIO . forever $ playSong 300 updateInProgress *> threadDelay 20_000_000 + put (Just tid) + putStrLn @Text [i|Running Sync Op: #{syncOpName syncOp}|] + setUpdate True + lift $ syncOpRun syncOp + pure $ (syncOpRequiresReboot syncOp, shouldRun) + case mTid of + Nothing -> pure () + Just tid -> liftIO $ killThread tid + setUpdate False + when (any snd restartsAndRuns) $ liftIO $ playSong 400 marioPowerUp + when (any (uncurry (&&)) restartsAndRuns) $ liftIO do + callCommand "/bin/sync" + callCommand "/sbin/reboot" + where + setUpdate :: MonadIO m => Bool -> m () + setUpdate b = if b + then liftIO $ writeIORef (appIsUpdating ctx) (Just agentVersion) + else liftIO $ writeIORef (appIsUpdating ctx) Nothing + cleanup :: SomeException -> IO () + cleanup e = do + void $ try @SomeException Sound.stop + void $ try @SomeException Sound.unexport + let e' = InternalE $ show e + flip runReaderT ctx $ cantFail $ failUpdate e' + diff --git a/agent/src/Lib/Sound.hs b/agent/src/Lib/Sound.hs new file mode 100644 index 000000000..358836b6f --- /dev/null +++ b/agent/src/Lib/Sound.hs @@ -0,0 +1,248 @@ +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} +module Lib.Sound where + +import Startlude hiding ( rotate ) + +import Control.Monad.Trans.Cont +import Control.Carrier.Writer.Strict +import System.FileLock + +import Util.Function + +-- General + +rotate :: forall a . (Enum a, Bounded a) => a -> Int -> a +rotate base step = toEnum $ (fromEnum base + step) `mod` size + (fromEnum $ minBound @a) + where size = fromEnum (maxBound @a) - fromEnum (minBound @a) + 1 +{-# INLINE rotate #-} + + +-- Interface + +export :: IO () +export = writeFile "/sys/class/pwm/pwmchip0/export" "0" + +unexport :: IO () +unexport = writeFile "/sys/class/pwm/pwmchip0/unexport" "0" + + +-- Constants + +semitoneK :: Double +semitoneK = 2 ** (1 / 12) +{-# INLINE semitoneK #-} + + +-- Data Types + +data Note = Note Semitone Word8 + deriving (Eq, Show) + +data Semitone = + C + | Db + | D + | Eb + | E + | F + | Gb + | G + | Ab + | A + | Bb + | B + deriving (Eq, Ord, Show, Enum, Bounded) + +newtype Interval = Interval Int deriving newtype (Num) + +data TimeSlice = + Sixteenth + | Eighth + | Quarter + | Half + | Whole + | Triplet TimeSlice + | Dot TimeSlice + | Tie TimeSlice TimeSlice + deriving (Eq, Show) + + +-- Theory Manipulation + +interval :: Interval -> Note -> Note +interval (Interval n) (Note step octave) = + let (o', s') = n `quotRem` 12 + newStep = step `rotate` s' + offset = if + | newStep > step && s' < 0 -> subtract 1 + | newStep < step && s' > 0 -> (+ 1) + | otherwise -> id + in Note newStep (offset $ octave + fromIntegral o') +{-# INLINE interval #-} + +minorThird :: Interval +minorThird = Interval 3 + +majorThird :: Interval +majorThird = Interval 3 + +fourth :: Interval +fourth = Interval 5 + +fifth :: Interval +fifth = Interval 7 + +circleOfFourths :: Note -> [Note] +circleOfFourths = iterate (interval fourth) + +circleOfFifths :: Note -> [Note] +circleOfFifths = iterate (interval fifth) + +-- Theory To Interface Target + +noteFreq :: Note -> Double +noteFreq (Note semi oct) = semitoneK ** (fromIntegral $ fromEnum semi) * c0 * (2 ** fromIntegral oct) + where + a4 = 440 + c0 = a4 / (semitoneK ** 9) / (2 ** 4) + +-- tempo is in quarters per minute +timeSliceToMicro :: Word16 -> TimeSlice -> Int +timeSliceToMicro tempo timeSlice = case timeSlice of + Sixteenth -> uspq `div` 4 + Eighth -> uspq `div` 2 + Quarter -> uspq + Half -> uspq * 2 + Whole -> uspq * 4 + Triplet timeSlice' -> timeSliceToMicro tempo timeSlice' * 2 `div` 3 + Dot timeSlice' -> timeSliceToMicro tempo timeSlice' * 3 `div` 2 + Tie ts1 ts2 -> timeSliceToMicro tempo ts1 + timeSliceToMicro tempo ts2 + where uspq = floor @Double $ 60 / fromIntegral tempo * 1_000_000 + + +-- Player + +periodFile :: FilePath +periodFile = "/sys/class/pwm/pwmchip0/pwm0/period" + +dutyFile :: FilePath +dutyFile = "/sys/class/pwm/pwmchip0/pwm0/duty_cycle" + +switchFile :: FilePath +switchFile = "/sys/class/pwm/pwmchip0/pwm0/enable" + +play :: Note -> IO () +play note' = do + prd' <- readFile periodFile + case prd' of + "0\n" -> writeFile periodFile "1000" + _ -> pure () + let prd = round @_ @Int $ 1 / noteFreq note' * 1_000_000_000 -- pwm needs it in nanos + writeFile dutyFile "0" + writeFile periodFile (show prd) + writeFile dutyFile (show $ prd `div` 2) + writeFile switchFile "1" + +stop :: IO () +stop = writeFile switchFile "0" + +playForDuration :: Note -> Int -> IO () +playForDuration note' duration = handle @SomeException (\e -> stop *> throwIO e) $ do + play note' + threadDelay (floor @Double $ fromIntegral duration * 0.95) + stop + threadDelay (ceiling @Double $ fromIntegral duration * 0.05) + +time :: IO () -> IO (UTCTime, UTCTime) +time action = do + t0 <- getCurrentTime + action + t1 <- getCurrentTime + pure (t0, t1) + +playSong :: Word16 -> Song -> IO () +playSong = flip runCont id .* playSong' +{-# INLINE playSong #-} + +playSongTimed :: Word16 -> Song -> IO (UTCTime, UTCTime) +playSongTimed tempo song = runCont (playSong' tempo song) time +{-# INLINE playSongTimed #-} + +playSong' :: Word16 -> Song -> Cont (IO b) (IO ()) +playSong' tempo song = cont $ \f -> bracket acquire release $ \_ -> f $ do + for_ song $ \(n, ts) -> do + let duration = timeSliceToMicro tempo ts + case n of + Nothing -> threadDelay duration + Just x -> playForDuration x duration + where + soundLock = "/root/agent/sound.lock" + acquire = do + l <- lockFile soundLock Exclusive + export + pure l + release l = do + void $ try @SomeException stop + void $ try @SomeException unexport + unlockFile l + + +-- Songs + +type Song = [(Maybe Note, TimeSlice)] + +marioDeath :: Song +marioDeath = + [ (Just $ Note B 4, Quarter) + , (Just $ Note F 5, Quarter) + , (Nothing , Quarter) + , (Just $ Note F 5, Quarter) + , (Just $ Note F 5, Triplet Half) + , (Just $ Note E 5, Triplet Half) + , (Just $ Note D 5, Triplet Half) + , (Just $ Note C 5, Quarter) + , (Just $ Note E 4, Quarter) + , (Nothing , Quarter) + , (Just $ Note E 4, Quarter) + , (Just $ Note C 4, Half) + ] + +marioPowerUp :: Song +marioPowerUp = + [ (Just $ Note G 4 , Triplet Eighth) + , (Just $ Note B 4 , Triplet Eighth) + , (Just $ Note D 5 , Triplet Eighth) + , (Just $ Note G 5 , Triplet Eighth) + , (Just $ Note B 5 , Triplet Eighth) + , (Just $ Note Ab 4, Triplet Eighth) + , (Just $ Note C 5 , Triplet Eighth) + , (Just $ Note Eb 5, Triplet Eighth) + , (Just $ Note Ab 5, Triplet Eighth) + , (Just $ Note C 5 , Triplet Eighth) + , (Just $ Note Bb 4, Triplet Eighth) + , (Just $ Note D 5 , Triplet Eighth) + , (Just $ Note F 5 , Triplet Eighth) + , (Just $ Note Bb 5, Triplet Eighth) + , (Just $ Note D 6 , Triplet Eighth) + ] + +marioCoin :: Song +marioCoin = [(Just $ Note B 5, Eighth), (Just $ Note E 6, Tie (Dot Quarter) Half)] + +updateInProgress :: Song +updateInProgress = take 6 $ (, Triplet Eighth) . Just <$> circleOfFifths (Note A 3) + +beethoven :: Song +beethoven = run . execWriter $ do + tell $ replicate 3 (Just $ Note E 5, Eighth) + tell $ [(Just $ Note C 5, Half)] + tell $ [(Nothing @Note, Eighth)] + tell $ replicate 3 (Just $ Note D 5, Eighth) + tell $ [(Just $ Note B 5, Half)] + +restoreActionInProgress :: Song +restoreActionInProgress = take 5 $ (, Triplet Eighth) . Just <$> circleOfFourths (Note C 4) + +backupActionInProgress :: [(Maybe Note, TimeSlice)] +backupActionInProgress = reverse restoreActionInProgress diff --git a/agent/src/Lib/Ssh.hs b/agent/src/Lib/Ssh.hs new file mode 100644 index 000000000..9ebbe573b --- /dev/null +++ b/agent/src/Lib/Ssh.hs @@ -0,0 +1,81 @@ +{-# LANGUAGE TupleSections #-} +module Lib.Ssh where + +import Startlude + +import Control.Lens +import Crypto.Hash +import Data.Aeson +import Data.ByteArray hiding ( null + , view + ) +import Data.ByteArray.Encoding +import Data.ByteString.Builder +import Data.ByteString.Lazy ( toStrict ) +import Data.List ( partition ) +import qualified Data.Text as T +import System.Directory + +import Lib.SystemPaths +import Settings + +data SshAlg = RSA | ECDSA | Ed25519 | DSA deriving (Eq, Show) +instance ToJSON SshAlg where + toJSON = String . \case + RSA -> "ssh-rsa" + ECDSA -> "ecdsa-sha2-nistp256" + Ed25519 -> "ssh-ed25519" + DSA -> "ssh-dss" + +getSshKeys :: (MonadReader AppSettings m, MonadIO m) => m [Text] +getSshKeys = do + base <- asks appFilesystemBase + liftIO $ doesFileExist (toS $ sshKeysFilePath `relativeTo` base) >>= \case + False -> pure [] + True -> lines . T.strip <$> readFile (toS $ sshKeysFilePath `relativeTo` base) + +fingerprint :: Text -> Either String (SshAlg, Text, Text) +fingerprint sshKey = do + (alg, b64, host) <- case T.split isSpace sshKey of + [alg, bin, host] -> (, encodeUtf8 bin, host) <$> parseAlg alg + [alg, bin] -> (, encodeUtf8 bin, "") <$> parseAlg alg + _ -> Left $ "Invalid SSH Key: " <> toS sshKey + bin <- convertFromBase @_ @ByteString Base64 b64 + let dig = unpack . convert @_ @ByteString $ hashWith MD5 bin + let hex = fmap (byteString . convertToBase @ByteString Base16 . singleton) dig + let colons = intersperse (charUtf8 ':') hex + pure . (alg, , host) . decodeUtf8 . toStrict . toLazyByteString $ fold colons + where + + parseAlg :: Text -> Either String SshAlg + parseAlg alg = case alg of + "ssh-rsa" -> Right RSA + "ecdsa-sha2-nistp256" -> Right ECDSA + "ssh-ed25519" -> Right Ed25519 + "ssh-dss" -> Right DSA + _ -> Left $ "Invalid SSH Alg: " <> toS alg + +createSshKey :: (MonadReader AppSettings m, MonadIO m) => Text -> m () +createSshKey key = do + base <- asks appFilesystemBase + let writeFirstKeyToFile k = writeFile (toS $ sshKeysFilePath `relativeTo` base) (k <> "\n") + liftIO $ doesFileExist (toS $ sshKeysFilePath `relativeTo` base) >>= \case + False -> writeFirstKeyToFile sanitizedKey + True -> addKeyToFile (toS $ sshKeysFilePath `relativeTo` base) sanitizedKey + where sanitizedKey = T.strip key + +addKeyToFile :: FilePath -> Text -> IO () +addKeyToFile path k = do + oldKeys <- filter (not . T.null) . lines <$> readFile path + writeFile path $ unlines (k : oldKeys) + +-- true if key deleted, false if key did not exist +deleteSshKey :: (MonadReader AppSettings m, MonadIO m) => Text -> m Bool +deleteSshKey fp = do + base <- asks appFilesystemBase + let rewriteFile others = liftIO $ writeFile (toS $ sshKeysFilePath `relativeTo` base) $ unlines others + getSshKeys >>= \case + [] -> pure False + keys -> do + let (existed, others) = partition ((Right fp ==) . fmap (view _2) . fingerprint) keys + if null existed then pure False else rewriteFile others >> pure True diff --git a/agent/src/Lib/Ssl.hs b/agent/src/Lib/Ssl.hs new file mode 100644 index 000000000..37dea7a5d --- /dev/null +++ b/agent/src/Lib/Ssl.hs @@ -0,0 +1,355 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE QuasiQuotes #-} +module Lib.Ssl where + +import Startlude + +import Control.Lens +import Data.String.Interpolate.IsString +import System.Process + +root_CA_CERT_NAME :: Text +root_CA_CERT_NAME = "Embassy Local Root CA" + +root_CA_OPENSSL_CONF :: FilePath -> ByteString +root_CA_OPENSSL_CONF path = [i| +# OpenSSL root CA configuration file. +# Copy to `/root/ca/openssl.cnf`. + +[ ca ] +# `man ca` +default_ca = CA_default + +[ CA_default ] +# Directory and file locations. +dir = #{path} +certs = $dir/certs +crl_dir = $dir/crl +new_certs_dir = $dir/newcerts +database = $dir/index.txt +serial = $dir/serial +RANDFILE = $dir/private/.rand + +# The root key and root certificate. +private_key = $dir/private/ca.key.pem +certificate = $dir/certs/ca.cert.pem + +# For certificate revocation lists. +crlnumber = $dir/crlnumber +crl = $dir/crl/ca.crl.pem +crl_extensions = crl_ext +default_crl_days = 30 + +# SHA-1 is deprecated, so use SHA-2 instead. +default_md = sha256 + +name_opt = ca_default +cert_opt = ca_default +default_days = 375 +preserve = no +policy = policy_loose + +[ policy_loose ] +# Allow the intermediate CA to sign a more diverse range of certificates. +# See the POLICY FORMAT section of the `ca` man page. +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ req ] +# Options for the `req` tool (`man req`). +default_bits = 4096 +distinguished_name = req_distinguished_name +string_mask = utf8only +prompt = no + +# SHA-1 is deprecated, so use SHA-2 instead. +default_md = sha256 + +# Extension to add when the -x509 option is used. +x509_extensions = v3_ca + +[ req_distinguished_name ] +# See . +CN = #{root_CA_CERT_NAME} +O = Start9 Labs +OU = Embassy + +[ v3_ca ] +# Extensions for a typical CA (`man x509v3_config`). +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical, CA:true +keyUsage = critical, digitalSignature, cRLSign, keyCertSign + +[ v3_intermediate_ca ] +# Extensions for a typical intermediate CA (`man x509v3_config`). +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical, CA:true, pathlen:0 +keyUsage = critical, digitalSignature, cRLSign, keyCertSign + +[ usr_cert ] +# Extensions for client certificates (`man x509v3_config`). +basicConstraints = CA:FALSE +nsCertType = client, email +nsComment = "OpenSSL Generated Client Certificate" +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = clientAuth, emailProtection + +[ server_cert ] +# Extensions for server certificates (`man x509v3_config`). +basicConstraints = CA:FALSE +nsCertType = server +nsComment = "OpenSSL Generated Server Certificate" +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer:always +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth + +[ crl_ext ] +# Extension for CRLs (`man x509v3_config`). +authorityKeyIdentifier=keyid:always + +[ ocsp ] +# Extension for OCSP signing certificates (`man ocsp`). +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +keyUsage = critical, digitalSignature +extendedKeyUsage = critical, OCSPSigning +|] + +intermediate_CA_OPENSSL_CONF :: Text -> ByteString +intermediate_CA_OPENSSL_CONF path = [i| +# OpenSSL intermediate CA configuration file. +# Copy to `/root/ca/intermediate/openssl.cnf`. + +[ ca ] +# `man ca` +default_ca = CA_default + +[ CA_default ] +# Directory and file locations. +dir = #{path} +certs = $dir/certs +crl_dir = $dir/crl +new_certs_dir = $dir/newcerts +database = $dir/index.txt +serial = $dir/serial +RANDFILE = $dir/private/.rand + +# The root key and root certificate. +private_key = $dir/private/intermediate.key.pem +certificate = $dir/certs/intermediate.cert.pem + +# For certificate revocation lists. +crlnumber = $dir/crlnumber +crl = $dir/crl/intermediate.crl.pem +crl_extensions = crl_ext +default_crl_days = 30 + +# SHA-1 is deprecated, so use SHA-2 instead. +default_md = sha256 + +name_opt = ca_default +cert_opt = ca_default +default_days = 375 +preserve = no +copy_extensions = copy +policy = policy_loose + + +[ policy_loose ] +# Allow the intermediate CA to sign a more diverse range of certificates. +# See the POLICY FORMAT section of the `ca` man page. +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ req ] +# Options for the `req` tool (`man req`). +default_bits = 4096 +distinguished_name = req_distinguished_name +string_mask = utf8only +prompt = no + +# SHA-1 is deprecated, so use SHA-2 instead. +default_md = sha256 + +# Extension to add when the -x509 option is used. +x509_extensions = v3_ca + +[ req_distinguished_name ] +CN = Embassy Local Intermediate CA +O = Start9 Labs +OU = Embassy + +[ v3_ca ] +# Extensions for a typical CA (`man x509v3_config`). +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical, CA:true +keyUsage = critical, digitalSignature, cRLSign, keyCertSign + +[ v3_intermediate_ca ] +# Extensions for a typical intermediate CA (`man x509v3_config`). +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical, CA:true, pathlen:0 +keyUsage = critical, digitalSignature, cRLSign, keyCertSign + +[ usr_cert ] +# Extensions for client certificates (`man x509v3_config`). +basicConstraints = CA:FALSE +nsCertType = client, email +nsComment = "OpenSSL Generated Client Certificate" +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = clientAuth, emailProtection + +[ server_cert ] +# Extensions for server certificates (`man x509v3_config`). +basicConstraints = CA:FALSE +nsCertType = server +nsComment = "OpenSSL Generated Server Certificate" +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer:always +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth + +[ crl_ext ] +# Extension for CRLs (`man x509v3_config`). +authorityKeyIdentifier=keyid:always + +[ ocsp ] +# Extension for OCSP signing certificates (`man ocsp`). +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +keyUsage = critical, digitalSignature +extendedKeyUsage = critical, OCSPSigning +|] + +domain_CSR_CONF :: Text -> ByteString +domain_CSR_CONF name = [i| +[req] +default_bits = 4096 +default_md = sha256 +distinguished_name = req_distinguished_name +prompt = no + +[req_distinguished_name] +CN = #{name} +O = Start9 Labs +OU = Embassy +|] + +writeRootCaCert :: MonadIO m => FilePath -> FilePath -> FilePath -> m (ExitCode, String, String) +writeRootCaCert confPath keyFilePath certFileDestinationPath = liftIO $ readProcessWithExitCode + "openssl" + [ "req" + , -- use x509 + "-new" + , -- new request + "-x509" + , -- self signed x509 + "-nodes" + , -- no passphrase + "-days" + , -- expires in... + "3650" + , -- valid for 10 years. Max is 20 years + "-key" + , -- source private key + toS keyFilePath + , "-out" + -- target cert path + , toS certFileDestinationPath + , "-config" + -- configured by... + , toS confPath + ] + "" + +data DeriveCertificate = DeriveCertificate + { applicantConfPath :: FilePath + , applicantKeyPath :: FilePath + , applicantCertPath :: FilePath + , signingConfPath :: FilePath + , signingKeyPath :: FilePath + , signingCertPath :: FilePath + , duration :: Integer + } +writeIntermediateCert :: MonadIO m => DeriveCertificate -> m (ExitCode, String, String) +writeIntermediateCert DeriveCertificate {..} = liftIO $ interpret $ do + -- openssl genrsa -out dump/int.key 4096 + segment $ openssl [i|genrsa -out #{applicantKeyPath} 4096|] + -- openssl req -new -config dump/int-csr.conf -key dump/int.key -nodes -out dump/int.csr + segment $ openssl [i|req -new + -config #{applicantConfPath} + -key #{applicantKeyPath} + -nodes + -out #{applicantCertPath <> ".csr"}|] + -- openssl x509 -CA dump/ca.crt -CAkey dump/ca.key -CAcreateserial -days 3650 -req -in dump/int.csr -out dump/int.crt + segment $ openssl [i|ca -batch + -config #{signingConfPath} + -rand_serial + -keyfile #{signingKeyPath} + -cert #{signingCertPath} + -extensions v3_intermediate_ca + -days #{duration} + -notext + -in #{applicantCertPath <> ".csr"} + -out #{applicantCertPath}|] + liftIO $ readFile signingCertPath >>= appendFile applicantCertPath + +writeLeafCert :: MonadIO m => DeriveCertificate -> Text -> Text -> m (ExitCode, String, String) +writeLeafCert DeriveCertificate {..} hostname torAddress = liftIO $ interpret $ do + segment $ openssl [i|genrsa -out #{applicantKeyPath} 4096|] + segment $ openssl [i|req -config #{applicantConfPath} + -key #{applicantKeyPath} + -new + -addext subjectAltName=DNS:#{hostname},DNS:*.#{hostname},DNS:#{torAddress},DNS:*.#{torAddress} + -out #{applicantCertPath <> ".csr"}|] + segment $ openssl [i|ca -batch + -config #{signingConfPath} + -rand_serial + -keyfile #{signingKeyPath} + -cert #{signingCertPath} + -extensions server_cert + -days #{duration} + -notext + -in #{applicantCertPath <> ".csr"} + -out #{applicantCertPath} + |] + liftIO $ readFile signingCertPath >>= appendFile applicantCertPath + +openssl :: Text -> IO (ExitCode, String, String) +openssl = ($ "") . readProcessWithExitCode "openssl" . fmap toS . words +{-# INLINE openssl #-} + +interpret :: ExceptT ExitCode (StateT (String, String) IO) () -> IO (ExitCode, String, String) +interpret = fmap (over _1 (either id (const ExitSuccess)) . regroup) . flip runStateT ("", "") . runExceptT +{-# INLINE interpret #-} + +regroup :: (a, (b, c)) -> (a, b, c) +regroup (a, (b, c)) = (a, b, c) +{-# INLINE regroup #-} + +segment :: IO (ExitCode, String, String) -> ExceptT ExitCode (StateT (String, String) IO) () +segment action = liftIO action >>= \case + (ExitSuccess, o, e) -> modify (bimap (<> o) (<> e)) + (ec , o, e) -> modify (bimap (<> o) (<> e)) *> throwE ec +{-# INLINE segment #-} diff --git a/agent/src/Lib/Synchronizers.hs b/agent/src/Lib/Synchronizers.hs new file mode 100644 index 000000000..ae60630a2 --- /dev/null +++ b/agent/src/Lib/Synchronizers.hs @@ -0,0 +1,437 @@ +{-# OPTIONS_GHC -fno-warn-type-defaults #-} +{-# LANGUAGE ExtendedDefaultRules #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +module Lib.Synchronizers where + +import Startlude hiding ( check ) +import qualified Startlude.ByteStream as ByteStream +import qualified Startlude.ByteStream.Char8 as ByteStream + +import qualified Control.Effect.Reader.Labelled + as Fused +import Control.Carrier.Lift ( runM ) +import Control.Monad.Trans.Reader ( mapReaderT ) +import Control.Monad.Trans.Resource +import Data.Attoparsec.Text +import qualified Data.ByteString as BS +import qualified Data.ByteString.Char8 as B8 +import qualified Data.Conduit as Conduit +import qualified Data.Conduit.Combinators as Conduit +import qualified Data.Conduit.Tar as Conduit +import Data.Conduit.Shell hiding ( arch + , patch + , stream + , hostname + ) +import Data.FileEmbed +import qualified Data.HashMap.Strict as HM +import Data.IORef +import Data.String.Interpolate.IsString +import qualified Data.Yaml as Yaml +import Exinst +import System.FilePath ( splitPath + , joinPath + , () + ) +import System.FilePath.Posix ( takeDirectory ) +import System.Directory +import System.IO.Error +import System.Posix.Files +import qualified Streaming.Prelude as Stream +import qualified Streaming.Conduit as Conduit +import qualified Streaming.Zip as Stream + +import Constants +import Foundation +import Lib.ClientManifest +import Lib.Error +import qualified Lib.External.AppMgr as AppMgr +import Lib.External.Registry +import Lib.Sound +import Lib.Ssl +import Lib.Tor +import Lib.Types.Core +import Lib.Types.NetAddress +import Lib.Types.Emver +import Lib.SystemCtl +import Lib.SystemPaths hiding ( () ) +import Settings +import Util.File +import qualified Lib.Algebra.Domain.AppMgr as AppMgr2 +import Daemon.ZeroConf ( getStart9AgentHostname ) + + +data Synchronizer = Synchronizer + { synchronizerVersion :: Version + , synchronizerOperations :: [SyncOp] + } + +data SyncOp = SyncOp + { syncOpName :: Text + , syncOpShouldRun :: ReaderT AgentCtx IO Bool -- emit true if op is to be run + , syncOpRun :: ReaderT AgentCtx IO () + , syncOpRequiresReboot :: Bool + } + +data Arch = ArmV7 | ArmV8 deriving (Show) +data KernelVersion = KernelVersion + { kernelVersionNumber :: Version + , kernelVersionArch :: Arch + } + deriving Show + +parseKernelVersion :: Parser KernelVersion +parseKernelVersion = do + major' <- decimal + minor' <- char '.' *> decimal + patch' <- char '.' *> decimal + arch <- string "-v7l+" *> pure ArmV7 <|> string "-v8+" *> pure ArmV8 + pure $ KernelVersion (Version (major', minor', patch', 0)) arch + +synchronizer :: Synchronizer +synchronizer = sync_0_2_5 +{-# INLINE synchronizer #-} + +sync_0_2_5 :: Synchronizer +sync_0_2_5 = Synchronizer + "0.2.5" + [ syncCreateAgentTmp + , syncCreateSshDir + , syncRemoveAvahiSystemdDependency + , syncInstallAppMgr + , syncFullUpgrade + , sync32BitKernel + , syncInstallNginx + , syncWriteNginxConf + , syncInstallDuplicity + , syncInstallExfatFuse + , syncInstallExfatUtils + , syncInstallAmbassadorUI + , syncOpenHttpPorts + , syncUpgradeLifeline + , syncPrepSslRootCaDir + , syncPrepSslIntermediateCaDir + , syncPersistLogs + ] + +syncCreateAgentTmp :: SyncOp +syncCreateAgentTmp = SyncOp "Create Agent Tmp Dir" check migrate False + where + check = do + s <- asks appSettings + tmp <- injectFilesystemBaseFromContext s $ getAbsoluteLocationFor agentTmpDirectory + liftIO $ not <$> doesPathExist (toS tmp) + migrate = do + s <- asks appSettings + tmp <- injectFilesystemBaseFromContext s $ getAbsoluteLocationFor agentTmpDirectory + liftIO $ createDirectoryIfMissing True (toS tmp) + +syncCreateSshDir :: SyncOp +syncCreateSshDir = SyncOp "Create SSH directory" check migrate False + where + check = do + base <- asks $ appFilesystemBase . appSettings + liftIO $ not <$> doesPathExist (toS $ sshKeysDirectory `relativeTo` base) + migrate = do + base <- asks $ appFilesystemBase . appSettings + liftIO $ createDirectoryIfMissing False (toS $ sshKeysDirectory `relativeTo` base) + +syncRemoveAvahiSystemdDependency :: SyncOp +syncRemoveAvahiSystemdDependency = SyncOp "Remove Avahi Systemd Dependency" check migrate False + where + wanted = decodeUtf8 $ $(embedFile "config/agent.service") + check = do + base <- asks $ appFilesystemBase . appSettings + content <- liftIO $ readFile (toS $ agentServicePath `relativeTo` base) + pure (content /= wanted) + migrate = do + base <- asks $ appFilesystemBase . appSettings + liftIO $ writeFile (toS $ agentServicePath `relativeTo` base) wanted + void $ liftIO systemCtlDaemonReload + +-- the main purpose of this is the kernel upgrade but it does upgrade all packages on the system, maybe we should +-- reconsider the heavy handed approach here +syncFullUpgrade :: SyncOp +syncFullUpgrade = SyncOp "Full Upgrade" check migrate True + where + check = liftIO . run $ do + v <- decodeUtf8 <<$>> (uname ("-r" :: Text) $| conduit await) + case parse parseKernelVersion <$> v of + Just (Done _ (KernelVersion (Version av) _)) -> if av < (4, 19, 118, 0) then pure True else pure False + _ -> pure False + migrate = liftIO . run $ do + shell "apt update" + shell "apt full-upgrade -y" + +sync32BitKernel :: SyncOp +sync32BitKernel = SyncOp "32 Bit Kernel Switch" check migrate True + where + getBootCfgPath = getAbsoluteLocationFor bootConfigPath + check = do + settings <- asks appSettings + cfg <- injectFilesystemBaseFromContext settings getBootCfgPath + liftIO . run $ fmap isNothing $ (shell [i|grep "arm_64bit=0" #{cfg} || true|] $| conduit await) + migrate = do + base <- asks $ appFilesystemBase . appSettings + let tmpFile = bootConfigTempPath `relativeTo` base + let bootCfg = bootConfigPath `relativeTo` base + contents <- liftIO $ readFile (toS bootCfg) + let contents' = unlines . (<> ["arm_64bit=0"]) . filter (/= "arm_64bit=1") . lines $ contents + liftIO $ writeFile (toS tmpFile) contents' + liftIO $ renameFile (toS tmpFile) (toS bootCfg) + +syncInstallNginx :: SyncOp +syncInstallNginx = SyncOp "Install Nginx" check migrate False + where + check = liftIO . run $ fmap isNothing (shell [i|which nginx || true|] $| conduit await) + migrate = liftIO . run $ do + apt "update" + apt "install" "nginx" "-y" + +syncInstallDuplicity :: SyncOp +syncInstallDuplicity = SyncOp "Install duplicity" check migrate False + where + check = liftIO . run $ fmap isNothing (shell [i|which duplicity || true|] $| conduit await) + migrate = liftIO . run $ do + apt "update" + apt "install" "-y" "duplicity" + +syncInstallExfatFuse :: SyncOp +syncInstallExfatFuse = SyncOp "Install exfat-fuse" check migrate False + where + check = + liftIO + $ (run (shell [i|dpkg -l|] $| shell [i|grep exfat-fuse|] $| conduit await) $> False) + `catch` \(e :: ProcessException) -> case e of + ProcessException _ (ExitFailure 1) -> pure True + _ -> throwIO e + migrate = liftIO . run $ do + apt "update" + apt "install" "-y" "exfat-fuse" + +syncInstallExfatUtils :: SyncOp +syncInstallExfatUtils = SyncOp "Install exfat-utils" check migrate False + where + check = + liftIO + $ (run (shell [i|dpkg -l|] $| shell [i|grep exfat-utils|] $| conduit await) $> False) + `catch` \(e :: ProcessException) -> case e of + ProcessException _ (ExitFailure 1) -> pure True + _ -> throwIO e + migrate = liftIO . run $ do + apt "update" + apt "install" "-y" "exfat-utils" + +syncWriteConf :: Text -> ByteString -> SystemPath -> SyncOp +syncWriteConf name contents' confLocation = SyncOp [i|Write #{name} Conf|] check migrate False + where + contents = decodeUtf8 contents' + check = do + base <- asks $ appFilesystemBase . appSettings + conf <- + liftIO + $ (Just <$> readFile (toS $ confLocation `relativeTo` base)) + `catch` (\(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e) + case conf of + Nothing -> pure True + Just co -> pure $ if co == contents then False else True + migrate = do + base <- asks $ appFilesystemBase . appSettings + void . liftIO $ createDirectoryIfMissing True (takeDirectory (toS $ confLocation `relativeTo` base)) + liftIO $ writeFile (toS $ confLocation `relativeTo` base) contents + +syncPrepSslRootCaDir :: SyncOp +syncPrepSslRootCaDir = SyncOp "Create Embassy Root CA Environment" check migrate False + where + check = do + base <- asks $ appFilesystemBase . appSettings + liftIO . fmap not . doesPathExist . toS $ rootCaDirectory `relativeTo` base + migrate = do + base <- asks $ appFilesystemBase . appSettings + liftIO $ do + createDirectoryIfMissing True . toS $ rootCaDirectory `relativeTo` base + for_ ["/certs", "/crl", "/newcerts", "/private"] $ \p -> do + createDirectoryIfMissing True . toS $ p `relativeTo` (rootCaDirectory `relativeTo` base) + setFileMode (toS $ (rootCaDirectory <> "/private") `relativeTo` base) (7 `shiftL` 6) + writeFile (toS $ (rootCaDirectory <> "/index.txt") `relativeTo` base) "" + writeFile (toS $ (rootCaDirectory <> "/serial") `relativeTo` base) "1000" + BS.writeFile (toS $ rootCaOpenSslConfPath `relativeTo` base) + (root_CA_OPENSSL_CONF . toS $ rootCaDirectory `relativeTo` base) + +syncPrepSslIntermediateCaDir :: SyncOp +syncPrepSslIntermediateCaDir = SyncOp "Create Embassy Intermediate CA Environment" check migrate False + where + check = do + base <- asks $ appFilesystemBase . appSettings + liftIO . fmap not . doesPathExist . toS $ intermediateCaDirectory `relativeTo` base + migrate = do + base <- asks $ appFilesystemBase . appSettings + liftIO $ do + createDirectoryIfMissing True . toS $ intermediateCaDirectory `relativeTo` base + for_ ["/certs", "/crl", "/newcerts", "/private"] $ \p -> do + createDirectoryIfMissing True . toS $ (intermediateCaDirectory <> p) `relativeTo` base + setFileMode (toS $ (intermediateCaDirectory <> "/private") `relativeTo` base) (7 `shiftL` 6) + writeFile (toS $ (intermediateCaDirectory <> "/index.txt") `relativeTo` base) "" + writeFile (toS $ (intermediateCaDirectory <> "/serial") `relativeTo` base) "1000" + BS.writeFile (toS $ intermediateCaOpenSslConfPath `relativeTo` base) + (intermediate_CA_OPENSSL_CONF . toS $ intermediateCaDirectory `relativeTo` base) + +syncWriteNginxConf :: SyncOp +syncWriteNginxConf = syncWriteConf "Nginx" $(embedFile "config/nginx.conf") nginxConfig + +syncInstallAmbassadorUI :: SyncOp +syncInstallAmbassadorUI = SyncOp "Install Ambassador UI" check migrate False + where + check = do + base <- asks (appFilesystemBase . appSettings) + liftIO (doesPathExist (toS $ ambassadorUiPath `relativeTo` base)) >>= \case + True -> do + manifest <- liftIO $ readFile (toS $ ambassadorUiManifestPath `relativeTo` base) + case Yaml.decodeEither' (encodeUtf8 manifest) of + Left _ -> pure False + Right (Some1 _ cm) -> case cm of + (V0 cmv0) -> pure $ clientManifestV0AppVersion cmv0 /= agentVersion + False -> pure True + migrate = mapReaderT runResourceT $ do + base <- asks (appFilesystemBase . appSettings) + liftIO $ removePathForcibly (toS $ ambassadorUiPath `relativeTo` base) + + void + . runInContext + -- untar and save to path + $ streamUntar (ambassadorUiPath `relativeTo` base) + -- unzip + . Stream.gunzip + -- download + $ getAmbassadorUiForSpec (exactly agentVersion) + + runM $ injectFilesystemBase base $ do + -- if the ssl config has already been setup, we want to override the config with new UI details + -- otherwise we leave it alone + whenM (liftIO $ doesFileExist (toS $ nginxSitesAvailable nginxSslConf `relativeTo` base)) $ do + sid <- getStart9AgentHostname + let hostname = sid <> ".local" + installAmbassadorUiNginxHTTPS + (NginxSiteConfOverride + hostname + 443 + (Just $ NginxSsl { nginxSslKeyPath = entityKeyPath sid + , nginxSslCertPath = entityCertPath sid + , nginxSslOnlyServerNames = [hostname] + } + ) + ) + nginxSslConf + installAmbassadorUiNginxHTTP nginxTorConf + + streamUntar :: (MonadResource m, MonadThrow m) => Text -> ByteStream.ByteStream m () -> m () + streamUntar root stream = Conduit.runConduit $ Conduit.fromBStream stream .| Conduit.untar \f -> do + let path = toS . (toS root ) . joinPath . drop 1 . splitPath . B8.unpack . Conduit.filePath $ f + print path + if (Conduit.fileType f == Conduit.FTDirectory) + then liftIO $ createDirectoryIfMissing True path + else Conduit.sinkFile path + +installAmbassadorUiNginxHTTP :: (MonadIO m, HasFilesystemBase sig m) => SystemPath -> m () +installAmbassadorUiNginxHTTP = installAmbassadorUiNginx Nothing + +installAmbassadorUiNginxHTTPS :: (MonadIO m, HasFilesystemBase sig m) => NginxSiteConfOverride -> SystemPath -> m () +installAmbassadorUiNginxHTTPS o = installAmbassadorUiNginx $ Just o + +-- Private. Installs an nginx conf from client-manifest to 'fileName' and restarts nginx. +installAmbassadorUiNginx :: (MonadIO m, HasFilesystemBase sig m) + => Maybe NginxSiteConfOverride + -> SystemPath -- nginx conf file name + -> m () +installAmbassadorUiNginx mSslOverrides fileName = do + base <- Fused.ask @"filesystemBase" + + -- parse app manifest + -- generate nginx conf from app manifest + -- write conf to ambassador target location + appEnv <- flip runReaderT base . handleS9ErrNuclear $ liftA2 + (HM.intersectionWith (,)) + (AppMgr2.runAppMgrCliC $ HM.mapMaybe AppMgr2.infoResTorAddress <$> AppMgr2.list [AppMgr2.flags| |]) + AppMgr.readLanIps -- TODO: get appmgr to expose this or guarantee its structure + agentTor <- getAgentHiddenServiceUrl + let fullEnv = HM.insert (AppId "start9-ambassador") (TorAddress agentTor, LanIp "127.0.0.1") appEnv + + removeFileIfExists $ nginxAvailableConf base + removeFileIfExists $ nginxEnabledConf base + + flip runReaderT fullEnv + $ transpile mSslOverrides (ambassadorUiClientManifiest base) (nginxAvailableConf base) + >>= \case + True -> pure () + False -> throwIO . InternalS9Error $ "Failed to write ambassador ui nginx config " <> show fileName + liftIO $ createSymbolicLink (nginxAvailableConf base) (nginxEnabledConf base) + + -- restart nginx + void . liftIO $ systemCtl RestartService "nginx" + where + ambassadorUiClientManifiest b = toS $ (ambassadorUiPath <> "/client-manifest.yaml") `relativeTo` b + nginxAvailableConf b = toS $ (nginxSitesAvailable fileName) `relativeTo` b + nginxEnabledConf b = toS $ (nginxSitesEnabled fileName) `relativeTo` b + +syncOpenHttpPorts :: SyncOp +syncOpenHttpPorts = SyncOp "Open Hidden Service Port 80" check migrate False + where + check = runResourceT $ do + base <- asks $ appFilesystemBase . appSettings + res <- + ByteStream.readFile (toS $ AppMgr.torrcBase `relativeTo` base) + & ByteStream.lines + & Stream.mapped ByteStream.toStrict + & Stream.map decodeUtf8 + & Stream.filter + ( ( (== ["HiddenServicePort", "443", "127.0.0.1:443"]) + <||> (== ["HiddenServicePort", "80", "127.0.0.1:80"]) + ) + . words + ) + & Stream.toList_ + if length res < 2 then pure True else pure False + migrate = cantFail . flip catchE failUpdate $ do + lift $ syncOpRun $ syncWriteConf "Torrc" $(embedFile "config/torrc") AppMgr.torrcBase + AppMgr.torReload + +syncInstallAppMgr :: SyncOp +syncInstallAppMgr = SyncOp "Install AppMgr" check migrate False + where + check = runExceptT AppMgr.getAppMgrVersion >>= \case + Left _ -> pure True + Right v -> not . (v <||) <$> asks (appMgrVersionSpec . appSettings) + migrate = fmap (either absurd id) . runExceptT . flip catchE failUpdate $ do + avs <- asks $ appMgrVersionSpec . appSettings + av <- AppMgr.installNewAppMgr avs + unless (av <|| avs) $ throwE $ AppMgrVersionE av avs + +syncUpgradeLifeline :: SyncOp +syncUpgradeLifeline = SyncOp "Upgrade Lifeline" check migrate False + where + clearResets :: SystemPath + clearResets = "/usr/local/bin/clear-resets.sh" + check = do + base <- asks $ appFilesystemBase . appSettings + liftIO $ doesFileExist . toS $ clearResets `relativeTo` base + migrate = do + base <- asks $ appFilesystemBase . appSettings + removeFileIfExists . toS $ lifelineBinaryPath `relativeTo` base + mapReaderT runResourceT $ runInContext $ getLifelineBinary (exactly "0.2.0") + removeFileIfExists . toS $ clearResets `relativeTo` base + +syncPersistLogs :: SyncOp +syncPersistLogs = + (syncWriteConf "Journald" $(embedFile "config/journald.conf") journaldConfig) { syncOpRequiresReboot = True } + +failUpdate :: S9Error -> ExceptT Void (ReaderT AgentCtx IO) () +failUpdate e = do + ref <- asks appIsUpdateFailed + putStrLn $ "UPDATE FAILED: " <> errorMessage (toError e) + liftIO $ playSong 216 beethoven + liftIO $ writeIORef ref (Just e) + +cantFail :: Monad m => ExceptT Void m a -> m a +cantFail = fmap (either absurd id) . runExceptT diff --git a/agent/src/Lib/SystemCtl.hs b/agent/src/Lib/SystemCtl.hs new file mode 100644 index 000000000..8b19c1e2d --- /dev/null +++ b/agent/src/Lib/SystemCtl.hs @@ -0,0 +1,23 @@ +module Lib.SystemCtl where + +import Startlude hiding ( words ) +import Protolude.Unsafe ( unsafeHead ) + +import Data.String +import System.Process +import Text.Casing + +data ServiceAction = + StartService + | StopService + | RestartService + deriving (Eq, Show) + +toAction :: ServiceAction -> String +toAction = fmap toLower . unsafeHead . words . wordify . show + +systemCtl :: ServiceAction -> Text -> IO ExitCode +systemCtl action service = rawSystem "systemctl" [toAction action, toS service] + +systemCtlDaemonReload :: IO ExitCode +systemCtlDaemonReload = rawSystem "systemctl" ["daemon-reload"] diff --git a/agent/src/Lib/SystemPaths.hs b/agent/src/Lib/SystemPaths.hs new file mode 100644 index 000000000..d63da47ee --- /dev/null +++ b/agent/src/Lib/SystemPaths.hs @@ -0,0 +1,254 @@ +{-# LANGUAGE ScopedTypeVariables #-} +module Lib.SystemPaths where + +import Startlude hiding ( (<.>) + , Reader + , ask + , runReader + ) + +import Control.Effect.Labelled ( Labelled + , runLabelled + ) +import Control.Effect.Reader.Labelled +import Data.List +import qualified Data.Text as T +import qualified Protolude.Base as P + ( show ) +import System.IO.Error ( isDoesNotExistError ) +import System.Directory + +import Lib.Types.Core +import Settings + +strJoin :: Char -> Text -> Text -> Text +strJoin c a b = case (T.unsnoc a, T.uncons b) of + (Nothing , Nothing ) -> "" + (Nothing , Just _ ) -> b + (Just _ , Nothing ) -> a + (Just (_, c0), Just (c1, s)) -> case (c0 == c, c1 == c) of + (True , True ) -> a <> s + (False, False) -> a <> T.singleton c <> b + _ -> a <> b + +() :: Text -> Text -> Text +() = strJoin '/' + +(<.>) :: Text -> Text -> Text +(<.>) = strJoin '.' + +-- system paths behave the same as FilePaths mostly except that they can be rebased onto alternative roots so that things +-- can be tested in an isolated way. This uses a church encoding. +newtype SystemPath = SystemPath { relativeTo :: Text -> Text } +instance Eq SystemPath where + (==) a b = a `relativeTo` "/" == b `relativeTo` "/" +instance Show SystemPath where + show sp = P.show $ sp `relativeTo` "/" +instance Semigroup SystemPath where + (SystemPath f) <> (SystemPath g) = SystemPath $ g . f +instance Monoid SystemPath where + mempty = SystemPath id +instance IsString SystemPath where + fromString (c : cs) = case c of + '/' -> relBase . toS $ cs + _ -> relBase . toS $ c : cs + fromString [] = mempty + +leaf :: SystemPath -> Text +leaf = last . T.splitOn "/" . show + +relBase :: Text -> SystemPath +relBase = SystemPath . flip () + +type HasFilesystemBase sig m = HasLabelled "filesystemBase" (Reader Text) sig m + +injectFilesystemBase :: Monad m => Text -> Labelled "filesystemBase" (ReaderT Text) m a -> m a +injectFilesystemBase fsbase = flip runReaderT fsbase . runLabelled @"filesystemBase" + +injectFilesystemBaseFromContext :: Monad m => AppSettings -> Labelled "filesystemBase" (ReaderT Text) m a -> m a +injectFilesystemBaseFromContext = injectFilesystemBase . appFilesystemBase + +getAbsoluteLocationFor :: HasFilesystemBase sig m => SystemPath -> m Text +getAbsoluteLocationFor path = do + base <- ask @"filesystemBase" + pure $ path `relativeTo` base + +readSystemPath :: (HasFilesystemBase sig m, MonadIO m) => SystemPath -> m (Maybe Text) +readSystemPath path = do + loadPath <- getAbsoluteLocationFor path + contents <- + liftIO + $ (Just <$> readFile (toS loadPath)) + `catch` (\(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e) + pure contents + +-- like the above, but throws IO error if file not found +readSystemPath' :: (HasFilesystemBase sig m, MonadIO m) => SystemPath -> m Text +readSystemPath' path = do + loadPath <- getAbsoluteLocationFor path + contents <- liftIO . readFile . toS $ loadPath + pure contents + +writeSystemPath :: (HasFilesystemBase sig m, MonadIO m) => SystemPath -> Text -> m () +writeSystemPath path contents = do + loadPath <- getAbsoluteLocationFor path + liftIO $ writeFile (toS loadPath) contents + +deleteSystemPath :: (HasFilesystemBase sig m, MonadIO m) => SystemPath -> m () +deleteSystemPath path = do + loadPath <- getAbsoluteLocationFor path + liftIO $ removePathForcibly (toS loadPath) + +dbPath :: (HasFilesystemBase sig m, HasLabelled "sqlDatabase" (Reader Text) sig m) => m Text +dbPath = do + rt <- ask @"filesystemBase" + dbName <- ask @"sqlDatabase" + pure $ rt "root/agent" toS dbName + +uiPath :: SystemPath +uiPath = "/var/www/html" + +agentDataDirectory :: SystemPath +agentDataDirectory = "/root/agent" + +agentTmpDirectory :: SystemPath +agentTmpDirectory = "/root/agent/tmp" + +bootConfigPath :: SystemPath +bootConfigPath = "/boot/config.txt" + +bootConfigTempPath :: SystemPath +bootConfigTempPath = "/boot/config_tmp.txt" + +executablePath :: SystemPath +executablePath = "/usr/local/bin" + +-- Caches -- + +iconBasePath :: SystemPath +iconBasePath = "/root/agent/icons" + +-- Nginx -- + +nginxConfig :: SystemPath +nginxConfig = "/etc/nginx/nginx.conf" + +journaldConfig :: SystemPath +journaldConfig = "/etc/systemd/journald.conf" + +nginxSitesAvailable :: SystemPath -> SystemPath +nginxSitesAvailable = ("/etc/nginx/sites-available" <>) + +nginxSitesEnabled :: SystemPath -> SystemPath +nginxSitesEnabled = ("/etc/nginx/sites-enabled" <>) + +nginxTorConf :: SystemPath +nginxTorConf = "/start9-ambassador.conf" + +nginxSslConf :: SystemPath +nginxSslConf = "/start9-ambassador-ssl.conf" + +-- SSH -- + +sshKeysDirectory :: SystemPath +sshKeysDirectory = "/home/pi/.ssh" + +sshKeysFilePath :: SystemPath +sshKeysFilePath = sshKeysDirectory <> "authorized_keys" + +-- Zero Conf -- + +avahiPath :: SystemPath +avahiPath = "/etc/avahi" + +avahiServiceFolder :: SystemPath +avahiServiceFolder = avahiPath <> "services" + +avahiServicePath :: Text -> SystemPath +avahiServicePath svc = avahiServiceFolder <> relBase (svc <.> "service") + +-- Ambassador UI -- + +ambassadorUiPath :: SystemPath +ambassadorUiPath = uiPath <> "/start9-ambassador" + +ambassadorUiManifestPath :: SystemPath +ambassadorUiManifestPath = ambassadorUiPath <> "/client-manifest.yaml" + +-- Tor -- + +agentTorHiddenServiceDirectory :: SystemPath +agentTorHiddenServiceDirectory = "/var/lib/tor/agent" + +agentTorHiddenServiceHostnamePath :: SystemPath +agentTorHiddenServiceHostnamePath = agentTorHiddenServiceDirectory <> "/hostname" + +agentTorHiddenServicePrivateKeyPath :: SystemPath +agentTorHiddenServicePrivateKeyPath = agentTorHiddenServiceDirectory <> "/hs_ed25519_secret_key" + +-- Server Config -- + +serverNamePath :: SystemPath +serverNamePath = "/root/agent/name.txt" + +altRegistryUrlPath :: SystemPath +altRegistryUrlPath = "/root/agent/alt_registry_url.txt" + +-- Session Auth Key -- + +sessionSigningKeyPath :: SystemPath +sessionSigningKeyPath = "/root/agent/start9.aes" + +-- AppMgr -- + +appMgrRootPath :: SystemPath +appMgrRootPath = "/root/appmgr" + +appMgrAppPath :: AppId -> SystemPath +appMgrAppPath = ((appMgrRootPath <> "apps") <>) . relBase . unAppId + +lifelineBinaryPath :: SystemPath +lifelineBinaryPath = "/usr/local/bin/lifeline" + +-- Open SSL -- + +rootCaDirectory :: SystemPath +rootCaDirectory = agentDataDirectory <> "/ca" + +rootCaKeyPath :: SystemPath +rootCaKeyPath = rootCaDirectory <> "/private/embassy-root-ca.key.pem" + +rootCaCertPath :: SystemPath +rootCaCertPath = rootCaDirectory <> "/certs/embassy-root-ca.cert.pem" + +rootCaOpenSslConfPath :: SystemPath +rootCaOpenSslConfPath = rootCaDirectory <> "/openssl.conf" + +intermediateCaDirectory :: SystemPath +intermediateCaDirectory = rootCaDirectory <> "/intermediate" + +intermediateCaKeyPath :: SystemPath +intermediateCaKeyPath = intermediateCaDirectory <> "/private/embassy-int-ca.key.pem" + +intermediateCaCertPath :: SystemPath +intermediateCaCertPath = intermediateCaDirectory <> "/certs/embassy-int-ca.crt.pem" + +intermediateCaOpenSslConfPath :: SystemPath +intermediateCaOpenSslConfPath = intermediateCaDirectory <> "/openssl.conf" + +sslDirectory :: SystemPath +sslDirectory = "/etc/nginx/ssl" + +entityKeyPath :: Text -> SystemPath +entityKeyPath hostname = sslDirectory <> relBase ("/" <> hostname <> "-local.key.pem") + +entityCertPath :: Text -> SystemPath +entityCertPath hostname = sslDirectory <> relBase ("/" <> hostname <> "-local.crt.pem") + +entityConfPath :: Text -> SystemPath +entityConfPath hostname = sslDirectory <> relBase ("/" <> hostname <> "-local.conf") + +-- Systemd + +agentServicePath :: SystemPath +agentServicePath = "/etc/systemd/system/agent.service" diff --git a/agent/src/Lib/Tor.hs b/agent/src/Lib/Tor.hs new file mode 100644 index 000000000..d4e54f584 --- /dev/null +++ b/agent/src/Lib/Tor.hs @@ -0,0 +1,13 @@ +module Lib.Tor where + +import Startlude + +import qualified Data.Text as T + +import Lib.SystemPaths + +getAgentHiddenServiceUrl :: (HasFilesystemBase sig m, MonadIO m) => m Text +getAgentHiddenServiceUrl = T.strip <$> readSystemPath' agentTorHiddenServiceHostnamePath + +getAgentHiddenServiceUrlMaybe :: (HasFilesystemBase sig m, MonadIO m) => m (Maybe Text) +getAgentHiddenServiceUrlMaybe = fmap T.strip <$> readSystemPath agentTorHiddenServiceHostnamePath diff --git a/agent/src/Lib/TyFam/ConditionalData.hs b/agent/src/Lib/TyFam/ConditionalData.hs new file mode 100644 index 000000000..6d2f62dd5 --- /dev/null +++ b/agent/src/Lib/TyFam/ConditionalData.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE TemplateHaskell #-} +module Lib.TyFam.ConditionalData where + +import Startlude + +import Data.Singletons.TH + +type Include :: Bool -> Type -> Type +type family Include p a where + Include 'True a = a + Include 'False _ = () +genDefunSymbols [''Include] +type Keep :: Type ~> Type +type Keep = IncludeSym1 'True +type Full :: ((Type ~> Type) -> Type) -> Type +type Full t = t Keep +type Strip :: Type ~> Type +type Strip = IncludeSym1 'False +type Stripped :: ((Type ~> Type) -> Type) -> Type +type Stripped t = t Strip diff --git a/agent/src/Lib/Types/Core.hs b/agent/src/Lib/Types/Core.hs new file mode 100644 index 000000000..4c5ba0063 --- /dev/null +++ b/agent/src/Lib/Types/Core.hs @@ -0,0 +1,114 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE UndecidableInstances #-} +module Lib.Types.Core where + +import Startlude +import qualified GHC.Read ( Read(..) ) +import qualified GHC.Show ( Show(..) ) + +import Data.Aeson ( withText + , FromJSON(parseJSON) + , FromJSONKey(fromJSONKey) + , Value(String) + , ToJSON(toJSON) + , ToJSONKey(toJSONKey) + ) +import Data.Functor.Contravariant ( Contravariant(contramap) ) +import Data.Singletons.TH +import Database.Persist ( PersistField(..) + , PersistValue(PersistText) + , SqlType(SqlString) + ) +import Database.Persist.Sql ( PersistFieldSql(..) ) +import Yesod.Core ( PathPiece(..) ) +import Control.Monad.Fail ( MonadFail(fail) ) +import Data.Text ( toUpper ) +import Web.HttpApiData + +newtype AppId = AppId { unAppId :: Text } deriving (Eq, Ord) +deriving newtype instance ToHttpApiData AppId +deriving newtype instance FromHttpApiData AppId + +instance IsString AppId where + fromString = AppId . fromString +instance Show AppId where + show = toS . unAppId +instance Read AppId where + readsPrec _ s = [(AppId $ toS s, "")] +instance Hashable AppId where + hashWithSalt n = hashWithSalt n . unAppId +instance ToJSON AppId where + toJSON = toJSON . unAppId +instance FromJSON AppId where + parseJSON = fmap AppId . parseJSON +instance PathPiece AppId where + toPathPiece = unAppId + fromPathPiece = fmap AppId . fromPathPiece +instance PersistField AppId where + toPersistValue = PersistText . show + fromPersistValue (PersistText t) = Right . AppId $ toS t + fromPersistValue other = Left $ "Invalid AppId: " <> show other +instance PersistFieldSql AppId where + sqlType _ = SqlString +instance FromJSONKey AppId where + fromJSONKey = fmap AppId fromJSONKey +instance ToJSONKey AppId where + toJSONKey = contramap unAppId toJSONKey + + +data AppContainerStatus = + Running + | Stopped + | Paused + | Restarting + | Removing + | Dead deriving (Eq, Show) +instance ToJSON AppContainerStatus where + toJSON Paused = String "STOPPED" -- we never want to show paused to the Front End + toJSON other = String . toUpper . show $ other +instance FromJSON AppContainerStatus where + parseJSON = withText "health status" $ \case + "RUNNING" -> pure Running + "STOPPED" -> pure Stopped + "PAUSED" -> pure Paused + "RESTARTING" -> pure Restarting + "REMOVING" -> pure Removing + "DEAD" -> pure Dead + _ -> fail "unknown status" + +data AppAction = Start | Stop deriving (Eq, Show) + +data BackupJobType = CreateBackup | RestoreBackup deriving (Eq, Show) + +$(singletons [d| + data AppTmpStatus + = Installing + | CreatingBackup + | RestoringBackup + | NeedsConfig + | BrokenDependencies + | Crashed + | StoppingT + | RestartingT + deriving (Eq, Show) |]) + +instance ToJSON AppTmpStatus where + toJSON = String . \case + Installing -> "INSTALLING" + CreatingBackup -> "CREATING_BACKUP" + RestoringBackup -> "RESTORING_BACKUP" + NeedsConfig -> "NEEDS_CONFIG" + BrokenDependencies -> "BROKEN_DEPENDENCIES" + Crashed -> "CRASHED" + RestartingT -> "RESTARTING" + StoppingT -> "STOPPING" + +data AppStatus + = AppStatusTmp AppTmpStatus + | AppStatusAppMgr AppContainerStatus + deriving (Eq, Show) +instance ToJSON AppStatus where + toJSON (AppStatusTmp s) = toJSON s + toJSON (AppStatusAppMgr s) = toJSON s diff --git a/agent/src/Lib/Types/Emver.hs b/agent/src/Lib/Types/Emver.hs new file mode 100644 index 000000000..0cceeed9e --- /dev/null +++ b/agent/src/Lib/Types/Emver.hs @@ -0,0 +1,258 @@ +{- | +Module : Lib.Types.Emver +Description : Semver with 4th digit extension for Embassy +License : Start9 Non-Commercial +Maintainer : keagan@start9labs.com +Stability : experimental +Portability : portable + +This module was designed to address the problem of releasing updates to Embassy Packages where the upstream project was +either unaware of or apathetic towards supporting their application on the Embassy platform. In most cases, the original +package will support . This leaves us with the problem where we would like +to preserve the original package's version, since one of the goals of the Embassy platform is transparency. However, on +occasion, we have screwed up and published a version of a package that needed to have its metadata updated. In this +scenario we were left with the conundrum of either unilaterally claiming a version number of a package we did not author +or let the issue persist until the next update. Neither of these promote good user experiences, for different reasons. +This module extends the semver standard linked above with a 4th digit, which is given PATCH semantics. +-} + +module Lib.Types.Emver + ( major + , minor + , patch + , revision + , satisfies + , (<||) + , (||>) + -- we do not export 'None' because it is useful for its internal algebraic properties only + , VersionRange(Anchor, Any, None) + , Version(..) + , AnyRange(..) + , AllRange(..) + , conj + , disj + , exactly + , parseVersion + , parseRange + ) +where + +import Prelude +import qualified Data.Attoparsec.Text as Atto +import Data.Function +import Data.Functor ( (<&>) + , ($>) + ) +import Control.Applicative ( liftA2 + , Alternative((<|>)) + ) +import Data.String ( IsString(..) ) +import qualified Data.Text as T + +-- | AppVersion is the core representation of the SemverQuad type. +newtype Version = Version { unVersion :: (Word, Word, Word, Word) } deriving (Eq, Ord) +instance Show Version where + show (Version (x, y, z, q)) = + let postfix = if q == 0 then "" else '.' : show q in show x <> "." <> show y <> "." <> show z <> postfix +instance IsString Version where + fromString s = either error id $ Atto.parseOnly parseVersion (T.pack s) + +-- | A change in the value found at 'major' implies a breaking change in the API that this version number describes +major :: Version -> Word +major (Version (x, _, _, _)) = x + +-- | A change in the value found at 'minor' implies a backwards compatible addition to the API that this version number +-- describes +minor :: Version -> Word +minor (Version (_, y, _, _)) = y + +-- | A change in the value found at 'patch' implies that the implementation of the API has changed without changing the +-- invariants promised by the API. In many cases this will be incremented when repairing broken functionality +patch :: Version -> Word +patch (Version (_, _, z, _)) = z + +-- | This is the fundamentally new value in comparison to the original semver 2.0 specification. It is given the same +-- semantics as 'patch' above, which begs the question, when should you update this value instead of that one. Generally +-- speaking, if you are both the package author and maintainer, you should not ever increment this number, as it is +-- redundant with 'patch'. However, if you maintain a package on some distribution channel, and you are /not/ the +-- original author, then it is encouraged for you to increment 'quad' instead of 'patch'. +revision :: Version -> Word +revision (Version (_, _, _, q)) = q + + +-- | 'Operator' is the type that specifies how to compare against the target version. Right represents the ordering, +-- Left negates it +type Operator = Either Ordering Ordering + +-- | 'VersionRange' is the algebra of sets of versions. They can be constructed by having an 'Anchor' term which +-- compares against the target version, or can be described with 'Conj' which is a conjunction, or 'Disj', which is a +-- disjunction. The 'Any' and 'All' terms are primarily there to round out the algebra, but 'Any' is also exposed due to +-- its usage in semantic versioning in general. The 'None' term is not useful to the end user as there would be no +-- reasonable usage of it to describe version sets. It is included for its utility as a unit on 'Disj' and possibly as +-- a zero on 'Conj' +-- +-- Laws (reflected in implementations of smart constructors): +-- Commutativity of conjunction: Conj a b === Conj b a +-- Commutativity of disjunction: Disj a b === Disj b a +-- Associativity of conjunction: Conj (Conj a b) c === Conj a (Conj b c) +-- Associativity of disjunction: Disj (Disj a b) c === Disj a (Disj b c) +-- Identity of conjunction: Any `Conj` a === a +-- Identity of disjunction: None `Disj` a === a +-- Zero of conjunction: None `Conj` a === None +-- Zero of disjunction: Any `Disj` a === Any +-- Distributivity of conjunction over disjunction: Conj a (Disj b c) === Disj (Conj a b) (Conj a c) +-- Distributivity of disjunction over conjunction: Disj a (Conj b c) === Conj (Disj a b) (Disj a c) +data VersionRange + = Anchor Operator Version + | Conj VersionRange VersionRange + | Disj VersionRange VersionRange + | Any + | None + deriving (Eq) + +-- | Smart constructor for conjunctions. Eagerly evaluates zeros and identities +conj :: VersionRange -> VersionRange -> VersionRange +conj Any b = b +conj a Any = a +conj None _ = None +conj _ None = None +conj a b = Conj a b + +-- | Smart constructor for disjunctions. Eagerly evaluates zeros and identities +disj :: VersionRange -> VersionRange -> VersionRange +disj Any _ = Any +disj _ Any = Any +disj None b = b +disj a None = a +disj a b = Disj a b + +exactly :: Version -> VersionRange +exactly = Anchor (Right EQ) + +instance Show VersionRange where + show (Anchor ( Left EQ) v ) = '!' : '=' : show v + show (Anchor ( Right EQ) v ) = '=' : show v + show (Anchor ( Left LT) v ) = '>' : '=' : show v + show (Anchor ( Right LT) v ) = '<' : show v + show (Anchor ( Left GT) v ) = '<' : '=' : show v + show (Anchor ( Right GT) v ) = '>' : show v + show (Conj a@(Disj _ _) b@(Disj _ _)) = paren (show a) <> (' ' : paren (show b)) + show (Conj a@(Disj _ _) b ) = paren (show a) <> (' ' : show b) + show (Conj a b@(Disj _ _)) = show a <> (' ' : paren (show b)) + show (Conj a b ) = show a <> (' ' : show b) + show (Disj a b ) = show a <> " || " <> show b + show Any = "*" + show None = "!" +instance Read VersionRange where + readsPrec _ s = case Atto.parseOnly parseRange (T.pack s) of + Left _ -> [] + Right a -> [(a, "")] + +paren :: String -> String +paren = mappend "(" . flip mappend ")" + +newtype AnyRange = AnyRange { unAnyRange :: VersionRange } +instance Semigroup AnyRange where + (<>) = AnyRange <<$>> disj `on` unAnyRange +instance Monoid AnyRange where + mempty = AnyRange None + +newtype AllRange = AllRange { unAllRange :: VersionRange } +instance Semigroup AllRange where + (<>) = AllRange <<$>> conj `on` unAllRange +instance Monoid AllRange where + mempty = AllRange Any + +-- | Predicate for deciding whether the 'Version' is in the 'VersionRange' +satisfies :: Version -> VersionRange -> Bool +satisfies v (Anchor op v') = either (\c x y -> compare x y /= c) (\c x y -> compare x y == c) op v v' +satisfies v (Conj a b ) = v `satisfies` a && v `satisfies` b +satisfies v (Disj a b ) = v `satisfies` a || v `satisfies` b +satisfies _ Any = True +satisfies _ None = False + +(<||) :: Version -> VersionRange -> Bool +(<||) = satisfies +{-# INLINE (<||) #-} + +(||>) :: VersionRange -> Version -> Bool +(||>) = flip satisfies +{-# INLINE (||>) #-} + +(<<$>>) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b) +(<<$>>) = fmap . fmap +{-# INLINE (<<$>>) #-} + +parseOperator :: Atto.Parser Operator +parseOperator = + (Atto.char '=' $> Right EQ) + <|> (Atto.string "!=" $> Left EQ) + <|> (Atto.string ">=" $> Left LT) + <|> (Atto.string "<=" $> Left GT) + <|> (Atto.char '>' $> Right GT) + <|> (Atto.char '<' $> Right LT) + +parseVersion :: Atto.Parser Version +parseVersion = do + major' <- Atto.decimal <* Atto.char '.' + minor' <- Atto.decimal <* Atto.char '.' + patch' <- Atto.decimal + quad' <- Atto.option 0 $ Atto.char '.' *> Atto.decimal + pure $ Version (major', minor', patch', quad') + +-- >>> Atto.parseOnly parseRange "=2.3.4 1.2.3.4 - 2.3.4.5 (>3.0.0 || <3.4.5)" +-- Right =2.3.4 >=1.2.3.4 <=2.3.4.5 ((>3.0.0 || <3.4.5)) +-- >>> Atto.parseOnly parseRange "0.2.6" +-- Right =0.2.6 +parseRange :: Atto.Parser VersionRange +parseRange = s <|> (Atto.char '*' *> pure Any) <|> (Anchor (Right EQ) <$> parseVersion) + where + sub = Atto.char '(' *> Atto.skipSpace *> parseRange <* Atto.skipSpace <* Atto.char ')' + s = + unAnyRange + . foldMap AnyRange + <$> ((p <|> sub) `Atto.sepBy1` (Atto.skipSpace *> Atto.string "||" <* Atto.skipSpace)) + p = unAllRange . foldMap AllRange <$> ((a <|> sub) `Atto.sepBy1` Atto.space) + a = liftA2 Anchor parseOperator parseVersion <|> caret <|> tilde <|> wildcard <|> hyphen + +-- >>> liftA2 satisfies (Atto.parseOnly parseVersion "0.20.1.1") (Atto.parseOnly parseRange "^0.20.1") +-- Right True +caret :: Atto.Parser VersionRange +caret = (Atto.char '^' *> parseVersion) <&> \case + v@(Version (0, 0, 0, _)) -> Anchor (Right EQ) v + v@(Version (0, 0, z, _)) -> rangeIE v (Version (0, 0, z + 1, 0)) + v@(Version (0, y, _, _)) -> rangeIE v (Version (0, y + 1, 0, 0)) + v@(Version (x, _, _, _)) -> rangeIE v (Version (x + 1, 0, 0, 0)) + +-- >>> Atto.parseOnly tilde "~1.2.3.4" +-- Right >=1.2.3.4 <1.2.4 +tilde :: Atto.Parser VersionRange +tilde = (Atto.char '~' *> (Atto.decimal `Atto.sepBy1` Atto.char '.')) >>= \case + [x, y, z, q] -> pure $ rangeIE (Version (x, y, z, q)) (Version (x, y, z + 1, 0)) + [x, y, z] -> pure $ rangeIE (Version (x, y, z, 0)) (Version (x, y + 1, 0, 0)) + [x, y] -> pure $ rangeIE (Version (x, y, 0, 0)) (Version (x, y + 1, 0, 0)) + [x] -> pure $ rangeIE (Version (x, 0, 0, 0)) (Version (x + 1, 0, 0, 0)) + o -> fail $ "Invalid number of version numbers: " <> show (length o) + +range :: Bool -> Bool -> Version -> Version -> VersionRange +range inc0 inc1 v0 v1 = + let lo = if inc0 then Left LT else Right GT + hi = if inc1 then Left GT else Right LT + in Conj (Anchor lo v0) (Anchor hi v1) + +rangeIE :: Version -> Version -> VersionRange +rangeIE = range True False + +-- >>> Atto.parseOnly wildcard "1.2.3.x" +-- Right >=1.2.3 <1.2.4 +wildcard :: Atto.Parser VersionRange +wildcard = (Atto.many1 (Atto.decimal <* Atto.char '.') <* Atto.char 'x') >>= \case + [x, y, z] -> pure $ rangeIE (Version (x, y, z, 0)) (Version (x, y, z + 1, 0)) + [x, y] -> pure $ rangeIE (Version (x, y, 0, 0)) (Version (x, y + 1, 0, 0)) + [x] -> pure $ rangeIE (Version (x, 0, 0, 0)) (Version (x + 1, 0, 0, 0)) + o -> fail $ "Invalid number of version numbers: " <> show (length o) + +-- >>> Atto.parseOnly hyphen "0.1.2.3 - 1.2.3.4" +-- Right >=0.1.2.3 <=1.2.3.4 +hyphen :: Atto.Parser VersionRange +hyphen = liftA2 (range True True) parseVersion (Atto.skipSpace *> Atto.char '-' *> Atto.skipSpace *> parseVersion) diff --git a/agent/src/Lib/Types/Emver/Orphans.hs b/agent/src/Lib/Types/Emver/Orphans.hs new file mode 100644 index 000000000..1af62533d --- /dev/null +++ b/agent/src/Lib/Types/Emver/Orphans.hs @@ -0,0 +1,40 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Lib.Types.Emver.Orphans where + +import Startlude + +import Data.Aeson + +import Lib.Types.Emver +import Database.Persist +import Database.Persist.Sql +import qualified Data.Attoparsec.Text as Atto +import Control.Monad.Fail +import qualified Data.Text as T +import Yesod.Core.Dispatch + +instance ToJSON Version where + toJSON = String . show +instance FromJSON Version where + parseJSON = withText + "Quad Semver" + \t -> case Atto.parseOnly parseVersion t of + Left e -> fail e + Right a -> pure a +instance ToJSON VersionRange where + toJSON = String . show +instance FromJSON VersionRange where + parseJSON = withText "Quad Semver Range" $ \t -> case Atto.parseOnly parseRange t of + Left e -> fail e + Right a -> pure a + +instance PersistField Version where + toPersistValue = toPersistValue @Text . show + fromPersistValue = first T.pack . Atto.parseOnly parseVersion <=< fromPersistValue + +instance PersistFieldSql Version where + sqlType _ = SqlString + +instance PathPiece VersionRange where + toPathPiece = show + fromPathPiece = hush . Atto.parseOnly parseRange diff --git a/agent/src/Lib/Types/NetAddress.hs b/agent/src/Lib/Types/NetAddress.hs new file mode 100644 index 000000000..82d4c5138 --- /dev/null +++ b/agent/src/Lib/Types/NetAddress.hs @@ -0,0 +1,13 @@ +module Lib.Types.NetAddress where + +import Startlude +import Protolude.Base ( show ) + +newtype TorAddress = TorAddress { unTorAddress :: Text } deriving (Eq) +instance Show TorAddress where + show = toS . unTorAddress + +newtype LanIp = LanIp { unLanIp :: Text } deriving (Eq) +instance Show LanIp where + show = toS . unLanIp + diff --git a/agent/src/Lib/Types/ServerApp.hs b/agent/src/Lib/Types/ServerApp.hs new file mode 100644 index 000000000..61dccc34c --- /dev/null +++ b/agent/src/Lib/Types/ServerApp.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE UndecidableInstances #-} +module Lib.Types.ServerApp where + +import Startlude hiding ( break ) + +import Data.Aeson + +import Lib.Types.Core +import Lib.Types.Emver +import Lib.Types.Emver.Orphans ( ) + +data StoreApp = StoreApp + { storeAppId :: AppId + , storeAppTitle :: Text + , storeAppDescriptionShort :: Text + , storeAppDescriptionLong :: Text + , storeAppIconUrl :: Text + , storeAppVersions :: NonEmpty StoreAppVersionInfo + } + deriving (Eq, Show) + +data StoreAppVersionInfo = StoreAppVersionInfo + { storeAppVersionInfoVersion :: Version + , storeAppVersionInfoReleaseNotes :: Text + } + deriving (Eq, Show) +instance Ord StoreAppVersionInfo where + compare = compare `on` storeAppVersionInfoVersion +instance FromJSON StoreAppVersionInfo where + parseJSON = withObject "Store App Version Info" $ \o -> do + storeAppVersionInfoVersion <- o .: "version" + storeAppVersionInfoReleaseNotes <- o .: "release-notes" + pure StoreAppVersionInfo { .. } +instance ToJSON StoreAppVersionInfo where + toJSON StoreAppVersionInfo {..} = + object ["version" .= storeAppVersionInfoVersion, "releaseNotes" .= storeAppVersionInfoReleaseNotes] diff --git a/agent/src/Lib/Types/Url.hs b/agent/src/Lib/Types/Url.hs new file mode 100644 index 000000000..df164f67b --- /dev/null +++ b/agent/src/Lib/Types/Url.hs @@ -0,0 +1,50 @@ +module Lib.Types.Url where + +import Startlude + +import Control.Monad.Fail +import qualified Data.Attoparsec.Text as A +import qualified GHC.Show ( Show(..) ) + +-- this is a very weak definition of url, it needs to be fleshed out in accordance with https://www.ietf.org/rfc/rfc1738.txt +data Url = Url + { urlScheme :: Text + , urlHost :: Text + , urlPort :: Word16 + } + deriving Eq +instance Show Url where + show (Url scheme host port) = toS $ scheme <> "://" <> host <> ":" <> show port + +parseUrl :: Text -> Either String Url +parseUrl t = A.parseOnly urlParser (toS t) + +urlParser :: A.Parser Url +urlParser = do + (scheme, defPort) <- A.option ("https", 443) $ schemeParser >>= \case + "http" -> pure ("http", 80) + "https" -> pure ("https", 443) + other -> fail $ "Invalid Scheme: " <> toS other + eHost <- fmap Left (untilParser ":") <|> fmap Right (atLeastParser 2) + case eHost of + Left host -> Url scheme host <$> portParser + Right host -> pure $ Url scheme host defPort + +untilParser :: Text -> A.Parser Text +untilParser t = toS <$> A.manyTill A.anyChar (A.string t) + +atLeastParser :: Int -> A.Parser Text +atLeastParser n = do + minLength <- toS <$> A.count n A.anyChar + rest <- A.takeText + pure $ minLength <> rest + +portParser :: A.Parser Word16 +portParser = do + port <- A.decimal + A.atEnd >>= \case + True -> pure port + False -> fail "invalid port" + +schemeParser :: A.Parser Text +schemeParser = toS <$> A.manyTill A.anyChar (A.string "://") diff --git a/agent/src/Lib/WebServer.hs b/agent/src/Lib/WebServer.hs new file mode 100644 index 000000000..56e1314d9 --- /dev/null +++ b/agent/src/Lib/WebServer.hs @@ -0,0 +1,185 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE ViewPatterns #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Lib.WebServer where + +import Startlude hiding ( exp ) + +import Control.Monad.Logger +import Data.Default +import Data.IORef +import Language.Haskell.TH.Syntax ( qLocation ) +import Network.Wai +import Network.Wai.Handler.Warp ( Settings + , defaultSettings + , defaultShouldDisplayException + , runSettings + , setHost + , setOnException + , setPort + ) +import Network.Wai.Middleware.Cors ( CorsResourcePolicy(..) + , cors + , simpleCorsResourcePolicy + ) +import Network.Wai.Middleware.RequestLogger + ( Destination(Logger) + , IPAddrSource(..) + , OutputFormat(..) + , destination + , mkRequestLogger + , outputFormat + ) +import Yesod.Core +import Yesod.Core.Types hiding ( Logger ) + +import Auth +import Foundation +import Handler.Apps +import Handler.Authenticate +import Handler.Backups +import Handler.Hosts +import Handler.Icons +import Handler.Login +import Handler.Notifications +import Handler.PasswordUpdate +import Handler.PowerOff +import Handler.Register +import Handler.SelfUpdate +import Handler.SshKeys +import Handler.Status +import Handler.Wifi +import Handler.V0 +import Settings + +-- This line actually creates our YesodDispatch instance. It is the second half +-- of the call to mkYesodData which occurs in Foundation.hs. Please see the +-- comments there for more details. +mkYesodDispatch "AgentCtx" resourcesAgentCtx + +instance YesodSubDispatch Auth AgentCtx where + yesodSubDispatch = $(mkYesodSubDispatch resourcesAuth) + +-- | Convert our foundation to a WAI Application by calling @toWaiAppPlain@ and +-- applying some additional middlewares. +makeApplication :: AgentCtx -> IO Application +makeApplication foundation = do + logWare <- makeLogWare foundation + -- Create the WAI application and apply middlewares + appPlain <- toWaiAppPlain foundation + let origin = case appCorsOverrideStar $ appSettings foundation of + Nothing -> Nothing + Just override -> Just ([encodeUtf8 override], True) + pure . logWare . cors (const . Just $ policy origin) . defaultMiddlewaresNoLogging $ appPlain + where + policy o = simpleCorsResourcePolicy + { corsOrigins = o + , corsMethods = ["GET", "POST", "HEAD", "PUT", "DELETE", "TRACE", "CONNECT", "OPTIONS", "PATCH"] + , corsRequestHeaders = [ "app-version" + , "Accept" + , "Accept-Charset" + , "Accept-Encoding" + , "Accept-Language" + , "Accept-Ranges" + , "Age" + , "Allow" + , "Authorization" + , "Cache-Control" + , "Connection" + , "Content-Encoding" + , "Content-Language" + , "Content-Length" + , "Content-Location" + , "Content-MD5" + , "Content-Range" + , "Content-Type" + , "Date" + , "ETag" + , "Expect" + , "Expires" + , "From" + , "Host" + , "If-Match" + , "If-Modified-Since" + , "If-None-Match" + , "If-Range" + , "If-Unmodified-Since" + , "Last-Modified" + , "Location" + , "Max-Forwards" + , "Pragma" + , "Proxy-Authenticate" + , "Proxy-Authorization" + , "Range" + , "Referer" + , "Retry-After" + , "Server" + , "TE" + , "Trailer" + , "Transfer-Encoding" + , "Upgrade" + , "User-Agent" + , "Vary" + , "Via" + , "WWW-Authenticate" + , "Warning" + , "Content-Disposition" + , "MIME-Version" + , "Cookie" + , "Set-Cookie" + , "Origin" + , "Prefer" + , "Preference-Applied" + ] + , corsIgnoreFailures = True + } + +startWeb :: AgentCtx -> IO () +startWeb foundation = do + app <- makeApplication foundation + + putStrLn @Text $ "Launching Web Server on port " <> show (appPort $ appSettings foundation) + action <- async $ runSettings (warpSettings foundation) app + + setWebProcessThreadId (asyncThreadId action) foundation + wait action + +shutdownAll :: [ThreadId] -> IO () +shutdownAll threadIds = do + for_ threadIds killThread + exitSuccess + +shutdownWeb :: AgentCtx -> IO () +shutdownWeb AgentCtx {..} = do + mThreadId <- readIORef appWebServerThreadId + for_ mThreadId $ \tid -> do + killThread tid + writeIORef appWebServerThreadId Nothing + +makeLogWare :: AgentCtx -> IO Middleware +makeLogWare foundation = mkRequestLogger def + { outputFormat = if appDetailedRequestLogging $ appSettings foundation + then Detailed True + else Apache (if appIpFromHeader $ appSettings foundation then FromFallback else FromSocket) + , destination = Logger $ loggerSet $ appLogger foundation + } + +-- | Warp settings for the given foundation value. +warpSettings :: AgentCtx -> Settings +warpSettings foundation = + setPort (fromIntegral . appPort $ appSettings foundation) + $ setHost (appHost $ appSettings foundation) + $ setOnException + (\_req e -> when (defaultShouldDisplayException e) $ messageLoggerSource + foundation + (appLogger foundation) + $(qLocation >>= liftLoc) + "yesod" + LevelError + (toLogStr $ "Exception from Warp: " ++ show e) + ) + defaultSettings diff --git a/agent/src/Model.hs b/agent/src/Model.hs new file mode 100644 index 000000000..308c813d1 --- /dev/null +++ b/agent/src/Model.hs @@ -0,0 +1,62 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE EmptyDataDecls #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoDeriveAnyClass #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} +module Model where + +import Startlude + +import Crypto.Hash +import Data.UUID +import Database.Persist.TH + +import Lib.Types.Core +import Lib.Types.Emver +import Lib.Types.Emver.Orphans ( ) +import Orphans.Digest ( ) +import Orphans.UUID ( ) + +share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| +Account + createdAt UTCTime + updatedAt UTCTime + name Text + password Text + UniqueAccount name + +ExecutedMigration + createdAt UTCTime + updatedAt UTCTime + srcVersion Version + tgtVersion Version + deriving Eq + deriving Show + +Notification json + Id UUID + createdAt UTCTime + archivedAt UTCTime Maybe + appId AppId + appVersion Version + code Text + title Text + message Text + deriving Eq + deriving Show + +BackupRecord sql=backup + Id UUID + createdAt UTCTime + appId AppId + appVersion Version + succeeded Bool + +IconDigest + Id AppId + tag (Digest MD5) +|] diff --git a/agent/src/Orphans/Digest.hs b/agent/src/Orphans/Digest.hs new file mode 100644 index 000000000..819a55948 --- /dev/null +++ b/agent/src/Orphans/Digest.hs @@ -0,0 +1,25 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +module Orphans.Digest where + +import Startlude + +import Crypto.Hash +import Data.ByteArray +import Data.ByteArray.Encoding +import Data.String.Interpolate.IsString +import Database.Persist.Sql +import Web.HttpApiData + +instance HashAlgorithm a => PersistField (Digest a) where + toPersistValue = PersistByteString . convert + fromPersistValue (PersistByteString bs) = + note [i|Invalid Digest: #{decodeUtf8 $ convertToBase Base16 bs}|] . digestFromByteString $ bs + fromPersistValue other = Left $ "Invalid Digest: " <> show other + +instance HashAlgorithm a => PersistFieldSql (Digest a) where + sqlType _ = SqlBlob + +instance HashAlgorithm a => ToHttpApiData (Digest a) where + toUrlPiece = decodeUtf8 . convertToBase Base16 diff --git a/agent/src/Orphans/UUID.hs b/agent/src/Orphans/UUID.hs new file mode 100644 index 000000000..f28a0b452 --- /dev/null +++ b/agent/src/Orphans/UUID.hs @@ -0,0 +1,18 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Orphans.UUID where + +import Startlude + +import Data.UUID +import Database.Persist.Sql +import Yesod.Core + +instance PathPiece UUID where + toPathPiece = show + fromPathPiece = readMaybe +instance PersistField UUID where + toPersistValue = PersistText . show + fromPersistValue (PersistText t) = note "Invalid UUID" $ readMaybe t + fromPersistValue other = Left $ "Invalid UUID: " <> show other +instance PersistFieldSql UUID where + sqlType _ = SqlString diff --git a/agent/src/Settings.hs b/agent/src/Settings.hs new file mode 100644 index 000000000..27635482f --- /dev/null +++ b/agent/src/Settings.hs @@ -0,0 +1,85 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +-- | Settings are centralized, as much as possible, into this file. This +-- includes database connection settings, static file locations, etc. +-- In addition, you can configure a number of different aspects of Yesod +-- by overriding methods in the Yesod typeclass. That instance is +-- declared in the Foundation.hs file. +module Settings where + +import Startlude + +import qualified Control.Effect.Labelled as Fused +import qualified Control.Exception as Exception +import Data.Aeson +import Data.FileEmbed ( embedFile ) +import Data.Yaml ( decodeEither' ) +import Database.Persist.Sqlite ( SqliteConf(..) ) +import Network.Wai.Handler.Warp ( HostPreference ) +import Yesod.Default.Config2 ( applyEnvValue + , configSettingsYml + ) +import Lib.Types.Emver +import Lib.Types.Emver.Orphans ( ) + +-- | Runtime settings to configure this application. These settings can be +-- loaded from various sources: defaults, environment variables, config files, +-- theoretically even a database. +data AppSettings = AppSettings + { appDatabaseConf :: SqliteConf + -- ^ Configuration settings for accessing the database. + , appHost :: HostPreference + -- ^ Host/interface the server should bind to. + , appPort :: Word16 + -- ^ Port to listen on + , appIpFromHeader :: Bool + -- ^ Get the IP address from the header when logging. Useful when sitting + -- behind a reverse proxy. + , appDetailedRequestLogging :: Bool + -- ^ Use detailed request logging system + , appShouldLogAll :: Bool + -- ^ Should all log messages be displayed? + , appMgrVersionSpec :: VersionRange + , appFilesystemBase :: Text + , appCorsOverrideStar :: Maybe Text + } + deriving Show + +instance FromJSON AppSettings where + parseJSON = withObject "AppSettings" $ \o -> do + appDatabaseConf <- o .: "database" >>= withObject + "database conf" + (\db -> do + dbName <- db .: "database" + poolSize <- db .: "poolsize" + pure $ SqliteConf dbName poolSize + ) + + appHost <- fromString <$> o .: "host" + appPort <- o .: "port" + appIpFromHeader <- o .: "ip-from-header" + + appDetailedRequestLogging <- o .:? "detailed-logging" .!= False + appShouldLogAll <- o .:? "should-log-all" .!= False + + appMgrVersionSpec <- o .: "app-mgr-version-spec" + appFilesystemBase <- o .: "filesystem-base" + appCorsOverrideStar <- o .:? "cors-override-star" + return AppSettings { .. } + +-- | Raw bytes at compile time of @config/settings.yml@ +configSettingsYmlBS :: ByteString +configSettingsYmlBS = $(embedFile configSettingsYml) + +-- | @config/settings.yml@, parsed to a @Value@. +configSettingsYmlValue :: Value +configSettingsYmlValue = either Exception.throw id $ decodeEither' configSettingsYmlBS + +-- | A version of @AppSettings@ parsed at compile time from @config/settings.yml@. +compileTimeAppSettings :: AppSettings +compileTimeAppSettings = case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of + Error e -> panic $ toS e + Success settings -> settings + +injectSettings :: Monad m => AppSettings -> Fused.Labelled "appSettings" (ReaderT AppSettings) m a -> m a +injectSettings s = flip runReaderT s . Fused.runLabelled @"appSettings" diff --git a/agent/src/Startlude.hs b/agent/src/Startlude.hs new file mode 100644 index 000000000..7277be2ff --- /dev/null +++ b/agent/src/Startlude.hs @@ -0,0 +1,128 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE UndecidableInstances #-} +module Startlude + ( module X + , module Startlude + ) +where + +import Control.Arrow as X + ( (&&&) ) +import Control.Comonad as X +import Control.Monad.Trans.Maybe as X +import Control.Error.Util as X + hiding ( (??) ) +import Data.Coerce as X +import Data.String as X + ( String + , fromString + ) +import Data.Time.Clock as X +import Protolude as X + hiding ( bool + , hush + , isLeft + , isRight + , note + , tryIO + , readMaybe + , (:+:) + , throwError + , toTitle + , toStrict + , toUpper + , Handler(..) + , yield + , type (==) + ) +import qualified Protolude as P + ( readMaybe ) + +-- not reexported +import Control.Monad.Logger +import Control.Monad.Trans.Resource +import qualified Control.Carrier.Lift as FE +import qualified Control.Carrier.Reader as FE +import qualified Control.Carrier.Error.Church as FE +import qualified Control.Effect.Labelled as FE +import Data.Singletons.Prelude.Eq ( PEq ) +import Yesod.Core ( MonadHandler(..) ) +import Control.Monad.Trans.Control +import Control.Monad.Base + +id :: a -> a +id = identity + +ioLogFailure :: Exception e => String -> e -> IO () +ioLogFailure t e = putStrLn @Text (toS t <> show e) >> pure () + +readMaybe :: Read a => Text -> Maybe a +readMaybe = P.readMaybe . toS + +-- orphans for stitching fused effects into the larger ecosystem +instance MonadResource (sub m) => MonadResource (FE.Labelled label sub m) where + liftResourceT = FE.Labelled . liftResourceT +instance MonadResource m => MonadResource (FE.LiftC m) where + liftResourceT = FE.LiftC . liftResourceT +instance MonadResource m => MonadResource (FE.ReaderC r m) where + liftResourceT = lift . liftResourceT +instance MonadResource m => MonadResource (FE.ErrorC e m) where + liftResourceT = lift . liftResourceT + + +instance MonadThrow (sub m) => MonadThrow (FE.Labelled label sub m) where + throwM = FE.Labelled . throwM +instance MonadThrow m => MonadThrow (FE.LiftC m) where + throwM = FE.LiftC . throwM + +instance MonadLogger m => MonadLogger (FE.LiftC m) where +instance MonadLogger (sub m) => MonadLogger (FE.Labelled label sub m) where + monadLoggerLog a b c d = FE.Labelled $ monadLoggerLog a b c d + +instance MonadHandler m => MonadHandler (FE.LiftC m) where + type HandlerSite (FE.LiftC m) = HandlerSite m + type SubHandlerSite (FE.LiftC m) = SubHandlerSite m + liftHandler = FE.LiftC . liftHandler + liftSubHandler = FE.LiftC . liftSubHandler + +instance MonadHandler (sub m) => MonadHandler (FE.Labelled label sub m) where + type HandlerSite (FE.Labelled label sub m) = HandlerSite (sub m) + type SubHandlerSite (FE.Labelled label sub m) = SubHandlerSite (sub m) + liftHandler = FE.Labelled . liftHandler + liftSubHandler = FE.Labelled . liftSubHandler + +instance MonadTransControl t => MonadTransControl (FE.Labelled k t) where + type StT (FE.Labelled k t) a = StT t a + liftWith f = FE.Labelled $ liftWith $ \run -> f (run . FE.runLabelled) + restoreT = FE.Labelled . restoreT +instance MonadBase IO (t m) => MonadBase IO (FE.Labelled k t m) where + liftBase = FE.Labelled . liftBase +instance MonadBaseControl IO (t m) => MonadBaseControl IO (FE.Labelled k t m) where + type StM (FE.Labelled k t m) a = StM (t m) a + liftBaseWith f = FE.Labelled $ liftBaseWith $ \run -> f (run . FE.runLabelled) + restoreM = FE.Labelled . restoreM +instance MonadBase IO m => MonadBase IO (FE.LiftC m) where + liftBase = FE.LiftC . liftBase +instance MonadTransControl FE.LiftC where + type StT (FE.LiftC) a = a + liftWith f = FE.LiftC $ f $ FE.runM + restoreT = FE.LiftC +instance MonadBaseControl IO m => MonadBaseControl IO (FE.LiftC m) where + type StM (FE.LiftC m) a = StM m a + liftBaseWith = defaultLiftBaseWith + restoreM = defaultRestoreM +instance MonadBase IO m => MonadBase IO (FE.ErrorC e m) where + liftBase = liftBaseDefault +instance MonadTransControl (FE.ErrorC e) where + type StT (FE.ErrorC e) a = Either e a + liftWith f = FE.ErrorC $ \_ leaf -> f (FE.runError (pure . Left) (pure . Right)) >>= leaf + restoreT m = FE.ErrorC $ \fail leaf -> m >>= \case + Left e -> fail e + Right a -> leaf a +instance MonadBaseControl IO m => MonadBaseControl IO (FE.ErrorC e m) where + type StM (FE.ErrorC e m) a = StM m (Either e a) + liftBaseWith = defaultLiftBaseWith + restoreM = defaultRestoreM + + +instance PEq Type -- DRAGONS? I may rue the day I decided to do this diff --git a/agent/src/Startlude/ByteStream.hs b/agent/src/Startlude/ByteStream.hs new file mode 100644 index 000000000..323893af4 --- /dev/null +++ b/agent/src/Startlude/ByteStream.hs @@ -0,0 +1,13 @@ +-- {-# OPTIONS_GHC -fno-warn-unused-imports #-} +module Startlude.ByteStream + ( module Startlude.ByteStream + , module BS + ) +where + +import Data.ByteString.Streaming as BS + hiding ( ByteString ) +import Data.ByteString.Streaming as X + ( ByteString ) + +type ByteStream m = X.ByteString m diff --git a/agent/src/Startlude/ByteStream/Char8.hs b/agent/src/Startlude/ByteStream/Char8.hs new file mode 100644 index 000000000..70a81cb6a --- /dev/null +++ b/agent/src/Startlude/ByteStream/Char8.hs @@ -0,0 +1,7 @@ +module Startlude.ByteStream.Char8 + ( module X + ) +where + +import Data.ByteString.Streaming.Char8 + as X diff --git a/agent/src/Util/Conduit.hs b/agent/src/Util/Conduit.hs new file mode 100644 index 000000000..d3150abc7 --- /dev/null +++ b/agent/src/Util/Conduit.hs @@ -0,0 +1,23 @@ +module Util.Conduit where + +import Startlude + +import Conduit +import Data.Text as T +import Data.Attoparsec.Text + +parseC :: MonadIO m => Parser b -> ConduitT Text b m () +parseC parser = fix $ \cont -> parseWith g parser "" >>= \case + Done rest result -> do + yield result + unless (T.null rest) $ leftover rest >> cont + Fail _ _ msg -> panic $ toS msg + Partial _ -> panic "INCOMPLETE PARSE" + where + g :: MonadIO m => ConduitT Text o m Text + g = await >>= \case + Nothing -> pure mempty + Just x -> print x >> pure x + +lineParser :: Parser Text +lineParser = takeTill isEndOfLine <* endOfLine diff --git a/agent/src/Util/File.hs b/agent/src/Util/File.hs new file mode 100644 index 000000000..751c9dc01 --- /dev/null +++ b/agent/src/Util/File.hs @@ -0,0 +1,12 @@ +module Util.File where + +import Startlude + +import System.Directory +import System.IO.Error + +removeFileIfExists :: MonadIO m => FilePath -> m () +removeFileIfExists fileName = liftIO $ removeFile fileName `catch` handleExists + where + handleExists e | isDoesNotExistError e = return () + | otherwise = throwIO e diff --git a/agent/src/Util/Function.hs b/agent/src/Util/Function.hs new file mode 100644 index 000000000..a04628b10 --- /dev/null +++ b/agent/src/Util/Function.hs @@ -0,0 +1,16 @@ +module Util.Function where + +import Startlude + +infixr 9 .* +(.*) :: (b -> c) -> (a0 -> a1 -> b) -> a0 -> a1 -> c +(.*) = (.) . (.) +{-# INLINE (.*) #-} + +infixr 9 .** +(.**) :: (b -> c) -> (a0 -> a1 -> a2 -> b) -> a0 -> a1 -> a2 -> c +(.**) = (.) . (.*) +{-# INLINE (.**) #-} + +uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d +uncurry3 f (a, b, c) = f a b c diff --git a/agent/src/Util/Text.hs b/agent/src/Util/Text.hs new file mode 100644 index 000000000..7e5c23001 --- /dev/null +++ b/agent/src/Util/Text.hs @@ -0,0 +1,24 @@ +module Util.Text where + +import Data.Text ( strip ) +import Startlude +import Text.Regex ( matchRegexAll + , mkRegex + , subRegex + ) + + +-- | Behaves like Ruby gsub implementation +gsub :: Text -> Text -> Text -> Text +gsub regex replaceWith str = toS $ subRegex (mkRegex $ toS regex) (toS str) (toS replaceWith) + +containsMatch :: Text -> Text -> Bool +containsMatch regex str = not . null $ getMatches regex str + +getMatches :: Text -> Text -> [Text] +getMatches regex str + | str == "" = [] + | otherwise = case matchRegexAll (mkRegex $ toS regex) (toS str) of + Nothing -> [] + Just (_, "" , after, _) -> getMatches regex (toS . strip . toS $ after) + Just (_, match, after, _) -> toS match : getMatches regex (toS after) diff --git a/agent/stack.yaml b/agent/stack.yaml new file mode 100644 index 000000000..a175fbdfc --- /dev/null +++ b/agent/stack.yaml @@ -0,0 +1,25 @@ +resolver: nightly-2020-09-29 + +packages: +- . + +extra-deps: + - aeson-1.4.7.1 + - aeson-flatten-0.1.0.2 + - exinst-0.8 + - fused-effects-1.1.0.0 + - fused-effects-th-0.1.0.2 + - git-embed-0.1.0 + - json-stream-0.4.2.4 + - protolude-0.3.0 + - streaming-conduit-0.1.2.2 + - streaming-utils-0.2.0.0 + # to avoid the ridiculous bug where stat64 is not found (only affects development) + - git: https://github.com/ProofOfKeags/persistent.git + commit: 3b52b13d9ce79cdef14bb1c37cc527657a529462 + subdirs: + - persistent-sqlite + +ghc-options: + "$locals": -fwrite-ide-info + "$everything": -haddock diff --git a/agent/test/ChecklistSpec.hs b/agent/test/ChecklistSpec.hs new file mode 100644 index 000000000..e47956e13 --- /dev/null +++ b/agent/test/ChecklistSpec.hs @@ -0,0 +1,20 @@ +module ChecklistSpec where + +import Startlude + +import Data.List ( (!!) ) +import Data.Text +import System.Directory +import Test.Hspec + +import Constants +import Lib.Synchronizers + +spec :: Spec +spec = describe "Current Version" $ do + it "Requires System Synchronizer" $ do + agentVersion `shouldSatisfy` (synchronizerVersion synchronizer ==) + it "Requires Migration Target" $ do + names <- liftIO $ listDirectory "migrations" + let targets = names <&> (fromString . toS . (!! 1) . (splitOn "::") . toS) + agentVersion `shouldSatisfy` flip elem targets diff --git a/agent/test/Lib/External/AppManifestSpec.hs b/agent/test/Lib/External/AppManifestSpec.hs new file mode 100644 index 000000000..ae6109a45 --- /dev/null +++ b/agent/test/Lib/External/AppManifestSpec.hs @@ -0,0 +1,77 @@ +{-# LANGUAGE QuasiQuotes #-} +module Lib.External.AppManifestSpec where + +import Startlude + +import Test.Hspec + +import Data.String.Interpolate.IsString +import Data.Yaml + +import Lib.External.AppManifest + +cups023Manifest :: ByteString +cups023Manifest = [i| +--- +compat: v0 +id: cups +version: 0.2.3 +title: Cups +description: + short: Peer-to-Peer Encrypted Messaging + long: A peer-to-peer encrypted messaging platform that operates over tor. +release-notes: fix autofill for password field +ports: + - internal: 59001 + tor: 59001 + - internal: 80 + tor: 80 +image: + type: tar +mount: /root +assets: + - src: httpd.conf + dst: "." + overwrite: true + - src: www + dst: "." + overwrite: true +hidden-service-version: v3 +|] + +cups023ManifestModNoUI :: ByteString +cups023ManifestModNoUI = [i| +--- +compat: v0 +id: cups +version: 0.2.3 +title: Cups +description: + short: Peer-to-Peer Encrypted Messaging + long: A peer-to-peer encrypted messaging platform that operates over tor. +release-notes: fix autofill for password field +ports: + - internal: 59001 + tor: 59001 +image: + type: tar +mount: /root +assets: + - src: httpd.conf + dst: "." + overwrite: true + - src: www + dst: "." + overwrite: true +hidden-service-version: v3 +|] + +spec :: Spec +spec = do + describe "parsing app manifest ports" $ do + it "should yield true for cups 0.2.3" $ do + res <- decodeThrow @IO @(AppManifest 0) cups023Manifest + uiAvailable res `shouldBe` True + it "should yield false for cups 0.2.3 Mod" $ do + res <- decodeThrow @IO @(AppManifest 0) cups023ManifestModNoUI + uiAvailable res `shouldBe` False diff --git a/agent/test/Lib/SoundSpec.hs b/agent/test/Lib/SoundSpec.hs new file mode 100644 index 000000000..129005a4a --- /dev/null +++ b/agent/test/Lib/SoundSpec.hs @@ -0,0 +1,16 @@ +module Lib.SoundSpec where + +import Startlude + +import Test.Hspec + +import Lib.Sound + +spec :: Spec +spec = describe "Sound Interface" $ do + it "Async sound actions should be FIFO" $ do + action <- async $ playSongTimed 400 marioDeath + action' <- async $ playSongTimed 400 marioDeath + marks0 <- wait action + marks1 <- wait action' + (marks0, marks1) `shouldSatisfy` \((s0, f0), (s1, f1)) -> s1 > s0 && s1 > f0 || s0 > s1 && s0 > f1 diff --git a/agent/test/Lib/Types/EmverProp.hs b/agent/test/Lib/Types/EmverProp.hs new file mode 100644 index 000000000..f02c9312b --- /dev/null +++ b/agent/test/Lib/Types/EmverProp.hs @@ -0,0 +1,149 @@ +{-# LANGUAGE TemplateHaskell #-} +module Lib.Types.EmverProp where + +import Startlude hiding ( Any ) + +import Hedgehog as Test +import Lib.Types.Emver +import Hedgehog.Range +import Hedgehog.Gen as Gen +import qualified Data.Attoparsec.Text as Atto + +versionGen :: MonadGen m => m Version +versionGen = do + a <- word (linear 0 30) + b <- word (linear 0 30) + c <- word (linear 0 30) + d <- word (linear 0 30) + pure $ Version (a, b, c, d) + +rangeGen :: MonadGen m => m VersionRange +rangeGen = choice [pure None, pure Any, anchorGen, disjGen, conjGen] + +anchorGen :: MonadGen m => m VersionRange +anchorGen = do + c <- element [LT, EQ, GT] + f <- element [Left, Right] + Anchor (f c) <$> versionGen + +conjGen :: MonadGen m => m VersionRange +conjGen = liftA2 conj rangeGen rangeGen + +disjGen :: MonadGen m => m VersionRange +disjGen = liftA2 disj rangeGen rangeGen + +prop_conjAssoc :: Property +prop_conjAssoc = property $ do + a <- forAll rangeGen + b <- forAll rangeGen + c <- forAll rangeGen + obs <- forAll versionGen + (obs <|| conj a (conj b c)) === (obs <|| conj (conj a b) c) + +prop_conjCommut :: Property +prop_conjCommut = property $ do + a <- forAll rangeGen + b <- forAll rangeGen + obs <- forAll versionGen + (obs <|| conj a b) === (obs <|| conj b a) + +prop_disjAssoc :: Property +prop_disjAssoc = property $ do + a <- forAll rangeGen + b <- forAll rangeGen + c <- forAll rangeGen + obs <- forAll versionGen + (obs <|| disj a (disj b c)) === (obs <|| disj (disj a b) c) + +prop_disjCommut :: Property +prop_disjCommut = property $ do + a <- forAll rangeGen + b <- forAll rangeGen + obs <- forAll versionGen + (obs <|| disj a b) === (obs <|| disj b a) + +prop_anyIdentConj :: Property +prop_anyIdentConj = property $ do + a <- forAll rangeGen + obs <- forAll versionGen + obs <|| conj Any a === obs <|| a + +prop_noneIdentDisj :: Property +prop_noneIdentDisj = property $ do + a <- forAll rangeGen + obs <- forAll versionGen + obs <|| disj None a === obs <|| a + +prop_noneAnnihilatesConj :: Property +prop_noneAnnihilatesConj = property $ do + a <- forAll rangeGen + obs <- forAll versionGen + obs <|| conj None a === obs <|| None + +prop_anyAnnihilatesDisj :: Property +prop_anyAnnihilatesDisj = property $ do + a <- forAll rangeGen + obs <- forAll versionGen + obs <|| disj Any a === obs <|| Any + +prop_conjDistributesOverDisj :: Property +prop_conjDistributesOverDisj = property $ do + a <- forAll rangeGen + b <- forAll rangeGen + c <- forAll rangeGen + obs <- forAll versionGen + obs <|| conj a (disj b c) === obs <|| disj (conj a b) (conj a c) + +prop_disjDistributesOverConj :: Property +prop_disjDistributesOverConj = property $ do + a <- forAll rangeGen + b <- forAll rangeGen + c <- forAll rangeGen + obs <- forAll versionGen + obs <|| disj a (conj b c) === obs <|| conj (disj a b) (disj a c) + +prop_anyAcceptsAny :: Property +prop_anyAcceptsAny = property $ do + obs <- forAll versionGen + assert $ obs <|| Any + +prop_noneAcceptsNone :: Property +prop_noneAcceptsNone = property $ do + obs <- forAll versionGen + assert . not $ obs <|| None + +prop_conjBoth :: Property +prop_conjBoth = property $ do + a <- forAll rangeGen + b <- forAll rangeGen + obs <- forAll versionGen + (obs <|| conj a b) === (obs <|| a && obs <|| b) + +prop_disjEither :: Property +prop_disjEither = property $ do + a <- forAll rangeGen + b <- forAll rangeGen + obs <- forAll versionGen + (obs <|| disj a b) === (obs <|| a || obs <|| b) + +prop_rangeParseRoundTrip :: Property +prop_rangeParseRoundTrip = withShrinks 0 . property $ do + a <- forAll rangeGen + obs <- forAll versionGen + when (a == None) Test.discard + -- we do not use 'tripping' here since 'tripping' requires equality of representation + -- we only want to check equality up to OBSERVATION + (satisfies obs <$> Atto.parseOnly parseRange (show a)) === Right (satisfies obs a) + +prop_anchorLeftIsNegatedRight :: Property +prop_anchorLeftIsNegatedRight = property $ do + a <- forAll anchorGen + neg <- case a of + Anchor (Right o) v -> pure $ Anchor (Left o) v + Anchor (Left o) v -> pure $ Anchor (Right o) v + _ -> Test.discard + obs <- forAll versionGen + obs <|| a /== obs <|| neg + +tests :: IO Bool +tests = checkParallel $ $$discover diff --git a/agent/test/Live/Metrics.hs b/agent/test/Live/Metrics.hs new file mode 100644 index 000000000..14e22d552 --- /dev/null +++ b/agent/test/Live/Metrics.hs @@ -0,0 +1,22 @@ +module Live.Metrics where + +import Lib.External.Metrics.Df +import Lib.External.Metrics.Iotop +import Lib.External.Metrics.ProcDev +import Lib.External.Metrics.Top +import Startlude + +parseIotopOutput :: IO IotopMetrics +parseIotopOutput = parseIotop <$> readFile "./test/Live/iotop.sample" + +parseTopOutput :: IO TopMetrics +parseTopOutput = parseTop <$> readFile "./test/Live/top.sample" + +parseDfOutput :: IO DfMetrics +parseDfOutput = parseDf <$> readFile "./test/Live/df.sample" + +parseProcDevOutput :: IO (UTCTime, ProcDevMomentStats) +parseProcDevOutput = do + res <- readFile "./test/Live/procDev.sample" + now <- getCurrentTime + pure $ parseProcDev now res diff --git a/agent/test/Live/Serialize.hs b/agent/test/Live/Serialize.hs new file mode 100644 index 000000000..bf74279c9 --- /dev/null +++ b/agent/test/Live/Serialize.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE QuasiQuotes #-} +module Live.Serialize where + +import Startlude hiding ( runReader ) + +import Control.Carrier.Lift +import Data.String.Interpolate.IsString + +import Application +import Lib.Algebra.State.RegistryUrl +import Lib.External.Registry +import Lib.SystemPaths + +someYaml :: ByteString +someYaml = [i| +bitcoind: + title: "Bitcoin Core" + description: + short: "A Bitcoin Full Node" + long: "The bitcoin full node implementation by Bitcoin Core." + version-info: + - version: 0.18.1 + release-notes: "Some stuff" + icon-type: png +|] + +appRegistryTest :: IO (Either String AppManifestRes) +appRegistryTest = do + settings <- getAppSettings + runM . injectFilesystemBaseFromContext settings . runRegistryUrlIOC $ parseBsManifest someYaml diff --git a/agent/test/Live/df.sample b/agent/test/Live/df.sample new file mode 100644 index 000000000..09e838c6a --- /dev/null +++ b/agent/test/Live/df.sample @@ -0,0 +1,2 @@ +Filesystem 1K-blocks Used Available Use% Mounted on +/dev/root 30391116 16800060 12320856 58% / \ No newline at end of file diff --git a/agent/test/Live/iotop.sample b/agent/test/Live/iotop.sample new file mode 100644 index 000000000..1186d0214 --- /dev/null +++ b/agent/test/Live/iotop.sample @@ -0,0 +1,119 @@ +Total DISK READ : 0.00 B/s | Total DISK WRITE : 0.00 B/s +Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s + TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND + 1 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % init + 2 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kthreadd] + 4 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/0:0H] + 6 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [mm_percpu_wq] + 7 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [ksoftirqd/0] + 8 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [rcu_sched] + 9 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [rcu_bh] + 10 rt/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [migration/0] + 11 rt/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [watchdog/0] + 12 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [cpuhp/0] + 13 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kdevtmpfs] + 14 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [netns] + 15 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [rcu_tasks_kthre] + 16 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kauditd] + 17 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [khungtaskd] + 18 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [oom_reaper] + 19 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [writeback] + 20 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kcompactd0] + 21 be/5 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [ksmd] + 22 be/7 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [khugepaged] + 23 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [crypto] + 24 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kintegrityd] + 25 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kblockd] + 26 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [ata_sff] + 27 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [md] + 28 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [edac-poller] + 29 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [devfreq_wq] + 30 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [watchdogd] + 34 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kswapd0] + 35 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/u3:0] + 36 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [ecryptfs-kthrea] + 78 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kthrotld] + 79 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [acpi_thermal_pm] + 80 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [scsi_eh_0] + 81 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [scsi_tmf_0] + 82 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [scsi_eh_1] + 83 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [scsi_tmf_1] + 89 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [ipv6_addrconf] + 98 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kstrp] + 115 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [charger_manager] + 155 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [scsi_eh_2] + 156 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [scsi_tmf_2] + 157 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/0:1H] + 268 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [raid5wq] + 321 be/3 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [jbd2/vda1-8] + 322 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [ext4-rsv-conver] + 391 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [iscsi_eh] + 392 be/3 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd-journald + 406 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lvmetad -f + 408 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [ib-comp-wq] + 410 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [ib-comp-unb-wq] + 411 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [ib_mcast] + 415 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [ib_nl_sa_wq] + 418 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [rdma_cm] + 530 be/4 systemd- 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd-timesyncd + 577 be/4 systemd- 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd-timesyncd [sd-resolve] + 613 be/4 systemd- 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd-networkd + 633 be/4 systemd- 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd-resolved + 711 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd-udevd + 797 be/4 syslog 0.00 B/s 0.00 B/s 0.00 % 0.00 % rsyslogd -n + 811 be/4 syslog 0.00 B/s 0.00 B/s 0.00 % 0.00 % rsyslogd -n [in:imuxsock] + 812 be/4 syslog 0.00 B/s 0.00 B/s 0.00 % 0.00 % rsyslogd -n [in:imklog] + 813 be/4 syslog 0.00 B/s 0.00 B/s 0.00 % 0.00 % rsyslogd -n [rs:main Q:Reg] + 799 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % cron -f + 802 be/4 daemon 0.00 B/s 0.00 B/s 0.00 % 0.00 % atd -f + 803 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % python3 /usr/bin/networkd-dispatcher --run-startup-triggers + 889 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % python3 /usr/bin/networkd-dispatcher --run-startup-triggers [gmain] + 805 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd-logind + 807 be/4 messageb 0.00 B/s 0.00 B/s 0.00 % 0.00 % dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only + 817 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ + 9445 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ + 9446 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ +19695 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ +19699 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ +23977 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ +23982 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ +23983 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ +23984 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ +23986 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ +23987 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % lxcfs /var/lib/lxcfs/ + 819 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % accounts-daemon + 833 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % accounts-daemon [gmain] + 835 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % accounts-daemon [gdbus] + 825 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % agetty -o -p -- \u --keep-baud 115200,38400,9600 ttyS0 vt220 + 828 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % agetty -o -p -- \u --noclear tty1 linux + 829 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal + 891 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal [gmain] + 838 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % polkitd --no-debug + 840 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % polkitd --no-debug [gmain] + 844 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % polkitd --no-debug [gdbus] + 839 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % sshd -D +22492 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd --user +22493 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % (sd-pam) +22585 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % tmux +22586 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % -bash +22597 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % ./start9-registry +22598 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % ./start9-registry [ghc_ticker] +22599 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % ./start9-registry [start9-regist:w] +22600 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % ./start9-registry [start9-regist:w] +22601 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % ./start9-registry [start9-regist:w] +22602 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % ./start9-registry [start9-regist:w] +22686 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % ./start9-registry [start9-regist:w] +22953 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % ./start9-registry [start9-regist:w] +23052 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/0:2] +23761 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/u2:1] +23861 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/u2:2] +23946 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/0:0] +24091 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % sshd: root@pts/0 +24198 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % -bash +24264 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/u2:0] +24265 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/u2:3] +24266 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/u2:4] +24336 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [kworker/0:1] +24407 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % python3 /usr/sbin/iotop -bn1 +29224 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [xfsalloc] +29227 be/0 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % [xfs_mru_cache] diff --git a/agent/test/Live/lscpu.sample b/agent/test/Live/lscpu.sample new file mode 100644 index 000000000..dd9ae19f6 --- /dev/null +++ b/agent/test/Live/lscpu.sample @@ -0,0 +1,15 @@ +Architecture: armv7l +Byte Order: Little Endian +CPU(s): 4 +On-line CPU(s) list: 0-3 +Thread(s) per core: 1 +Core(s) per socket: 4 +Socket(s): 1 +Vendor ID: ARM +Model: 3 +Model name: Cortex-A72 +Stepping: r0p3 +CPU max MHz: 1500.0000 +CPU min MHz: 600.0000 +BogoMIPS: 270.00 +Flags: half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32 \ No newline at end of file diff --git a/agent/test/Live/procDev.sample b/agent/test/Live/procDev.sample new file mode 100755 index 000000000..eff9158c8 --- /dev/null +++ b/agent/test/Live/procDev.sample @@ -0,0 +1,6 @@ +Inter-| Receive | Transmit + face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed + eth0: 1490932684 1431621 0 0 0 0 0 0 1725610837 1054325 0 0 0 0 0 0 + eth1: 1000000000 1000000 0 0 0 0 0 0 1000000000 1000000 0 0 0 0 0 0 + eth2: fuck askksf oijefoijsf everythign is dicked maodsfijoijther shit bals. + lo: 51480 488 0 0 0 0 0 0 51480 488 0 0 0 0 0 0 diff --git a/agent/test/Live/test-configure-bitcoind b/agent/test/Live/test-configure-bitcoind new file mode 100644 index 000000000..558dd8e9c --- /dev/null +++ b/agent/test/Live/test-configure-bitcoind @@ -0,0 +1,2 @@ +#!/bin/bash +curl -k --header "Content-Type: application/json" --request PATCH --data '{"rpcuser":"bitcoin","rpcpassword":"shitcoin"}' https://localhost:5959/v0/apps/installed/bitcoind/config \ No newline at end of file diff --git a/agent/test/Live/test-install-bitcoind b/agent/test/Live/test-install-bitcoind new file mode 100644 index 000000000..97b3b2cc8 --- /dev/null +++ b/agent/test/Live/test-install-bitcoind @@ -0,0 +1,2 @@ +#!/bin/bash +curl -k --header "Content-Type: application/json" --request POST --data '{"id":"bitcoind","version":"0.18.1"}' https://localhost:5959/v0/apps/install \ No newline at end of file diff --git a/agent/test/Live/test-register b/agent/test/Live/test-register new file mode 100755 index 000000000..1c5251668 --- /dev/null +++ b/agent/test/Live/test-register @@ -0,0 +1,3 @@ +#!/bin/bash + +curl -k --header "Content-Type: application/json" --request POST --data '{"productKey":"abcdefgh","pubKey":"03df51984d6b8b8b1cc693e239491f77a36c9e9dfe4a486e9972a18e03610a0d22"}' https://localhost:5959/v0/register \ No newline at end of file diff --git a/agent/test/Live/test-self-update b/agent/test/Live/test-self-update new file mode 100755 index 000000000..715424aec --- /dev/null +++ b/agent/test/Live/test-self-update @@ -0,0 +1,3 @@ +#!/bin/bash +curl -k --header "Content-Type: application/json" --request POST --data '{"versionSpecification":">=0.0.0"}' https://localhost:5959/v0/update + diff --git a/agent/test/Live/top.sample b/agent/test/Live/top.sample new file mode 100644 index 000000000..150f1dcba --- /dev/null +++ b/agent/test/Live/top.sample @@ -0,0 +1,130 @@ +top - 20:41:46 up 15:49, 1 user, load average: 3.28, 3.29, 3.01 +Tasks: 123 total, 1 running, 122 sleeping, 0 stopped, 0 zombie +%Cpu(s): 3.0 us, 4.5 sy, 0.0 ni, 50.7 id, 41.8 wa, 0.0 hi, 0.0 si, 0.0 st +MiB Mem : 3906.0 total, 568.4 free, 799.6 used, 2538.1 buff/cache +MiB Swap: 100.0 total, 31.5 free, 68.5 used. 2960.4 avail Mem + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 983 root 20 0 777140 586900 2264 S 6.2 14.7 363:41.88 bitcoind + 1 root 20 0 33700 5376 4344 S 0.0 0.1 0:03.90 systemd + 2 root 20 0 0 0 0 S 0.0 0.0 0:00.07 kthreadd + 3 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_gp + 4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_par_gp + 8 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mm_percpu_wq + 9 root 20 0 0 0 0 S 0.0 0.0 0:14.62 ksoftirqd/0 + 10 root 20 0 0 0 0 I 0.0 0.0 0:40.13 rcu_sched + 11 root 20 0 0 0 0 I 0.0 0.0 0:00.00 rcu_bh + 12 root rt 0 0 0 0 S 0.0 0.0 0:00.02 migration/0 + 13 root 20 0 0 0 0 S 0.0 0.0 0:00.00 cpuhp/0 + 14 root 20 0 0 0 0 S 0.0 0.0 0:00.00 cpuhp/1 + 15 root rt 0 0 0 0 S 0.0 0.0 0:00.01 migration/1 + 16 root 20 0 0 0 0 S 0.0 0.0 0:01.66 ksoftirqd/1 + 19 root 20 0 0 0 0 S 0.0 0.0 0:00.00 cpuhp/2 + 20 root rt 0 0 0 0 S 0.0 0.0 0:00.01 migration/2 + 21 root 20 0 0 0 0 S 0.0 0.0 0:03.19 ksoftirqd/2 + 24 root 20 0 0 0 0 S 0.0 0.0 0:00.00 cpuhp/3 + 25 root rt 0 0 0 0 S 0.0 0.0 0:00.01 migration/3 + 26 root 20 0 0 0 0 S 0.0 0.0 0:01.87 ksoftirqd/3 + 29 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kdevtmpfs + 30 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 netns + 34 root 20 0 0 0 0 S 0.0 0.0 0:00.03 khungtaskd + 35 root 20 0 0 0 0 S 0.0 0.0 0:00.00 oom_reaper + 36 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 writeback + 37 root 20 0 0 0 0 S 0.0 0.0 0:01.78 kcompactd0 + 38 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 crypto + 39 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kblockd + 41 root rt 0 0 0 0 S 0.0 0.0 0:00.00 watchdogd + 42 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rpciod + 43 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/u9:0-hci0 + 44 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 xprtiod + 47 root 20 0 0 0 0 S 0.0 0.0 0:50.55 kswapd0 + 48 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 nfsiod + 59 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kthrotld + 60 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 iscsi_eh + 62 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 dwc_otg + 64 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 DWC Notificatio + 65 root 1 -19 0 0 0 S 0.0 0.0 0:00.00 vchiq-slot/0 + 66 root 1 -19 0 0 0 S 0.0 0.0 0:00.00 vchiq-recy/0 + 67 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 vchiq-sync/0 + 68 root 20 0 0 0 0 S 0.0 0.0 0:00.00 vchiq-keep/0 + 69 root 10 -10 0 0 0 S 0.0 0.0 0:00.00 SMIO + 71 root -51 0 0 0 0 S 0.0 0.0 0:00.00 irq/38-brcmstb_ + 72 root -51 0 0 0 0 S 0.0 0.0 0:00.65 irq/39-mmc1 + 74 root -51 0 0 0 0 S 0.0 0.0 0:00.00 irq/39-mmc0 + 75 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mmc_complete + 79 root 20 0 0 0 0 D 0.0 0.0 0:22.44 jbd2/mmcblk0p2- + 80 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 ext4-rsv-conver + 84 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 ipv6_addrconf + 123 root 20 0 21216 6296 5424 S 0.0 0.2 0:02.12 systemd-journal + 148 root 20 0 18016 2648 1872 S 0.0 0.1 0:00.66 systemd-udevd + 182 root 10 -10 0 0 0 S 0.0 0.0 0:00.00 SMIO + 199 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mmal-vchiq + 203 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mmal-vchiq + 208 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mmal-vchiq + 217 root -2 0 0 0 0 S 0.0 0.0 0:00.00 v3d_bin + 218 root -2 0 0 0 0 S 0.0 0.0 0:00.00 v3d_render + 220 root -2 0 0 0 0 S 0.0 0.0 0:00.00 v3d_tfu + 221 root -2 0 0 0 0 S 0.0 0.0 0:00.00 v3d_csd + 222 root -2 0 0 0 0 S 0.0 0.0 0:00.00 v3d_cache_clean + 238 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 cfg80211 + 241 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 brcmf_wq/mmc1:0 + 242 root 20 0 0 0 0 S 0.0 0.0 0:00.38 brcmf_wdog/mmc1 + 285 systemd+ 20 0 22380 3164 2676 S 0.0 0.1 0:00.46 systemd-timesyn + 328 nobody 20 0 4320 1516 1344 S 0.0 0.0 0:00.36 thd + 333 root 20 0 7944 1840 1656 S 0.0 0.0 0:00.13 cron + 335 root 39 19 3692 1768 1568 S 0.0 0.0 0:00.04 alsactl + 342 message+ 20 0 6556 2792 2324 S 0.0 0.1 0:00.25 dbus-daemon + 346 root 20 0 13092 4768 4152 S 0.0 0.1 0:00.30 systemd-logind + 349 root 20 0 27656 1068 928 S 0.0 0.0 0:06.77 rngd + 361 root 20 0 10708 2596 2188 S 0.0 0.1 0:00.28 wpa_supplicant + 362 root 20 0 25512 2764 2156 S 0.0 0.1 0:00.37 rsyslogd + 464 root 20 0 11088 3176 2716 S 0.0 0.1 0:00.50 wpa_supplicant + 487 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/u9:1-hci0 + 488 root 20 0 2140 128 0 S 0.0 0.0 0:00.00 hciattach + 493 root 20 0 9536 1728 1448 S 0.0 0.0 0:00.03 bluetoothd + 548 root 20 0 3028 1600 1252 S 0.0 0.0 0:00.39 dhcpcd + 550 root 20 0 1007336 35764 11120 S 0.0 0.9 3:24.38 dockerd + 555 root 20 0 211952 27340 15432 S 0.0 0.7 40:34.92 agent + 592 root 20 0 4308 948 860 S 0.0 0.0 0:00.01 agetty + 618 root 20 0 10692 3804 3364 S 0.0 0.1 0:00.01 sshd + 619 debian-+ 20 0 34724 22244 5604 S 0.0 0.6 44:17.87 tor + 635 avahi 20 0 5904 2488 2144 S 0.0 0.1 0:03.10 avahi-daemon + 636 avahi 20 0 5772 244 0 S 0.0 0.0 0:00.00 avahi-daemon + 638 root 20 0 968328 9212 4604 S 0.0 0.2 2:38.54 docker-containe + 920 root 20 0 861496 976 556 S 0.0 0.0 0:00.06 docker-proxy + 934 root 20 0 861496 1032 620 S 0.0 0.0 0:00.05 docker-proxy + 948 root 20 0 852276 1032 620 S 0.0 0.0 0:00.04 docker-proxy + 961 root 20 0 852276 860 420 S 0.0 0.0 0:00.04 docker-proxy + 968 root 20 0 889864 2112 1148 S 0.0 0.1 0:07.54 docker-containe + 1332 Debian-+ 20 0 14096 2336 1888 S 0.0 0.1 0:00.04 exim4 + 2987 root 0 -20 0 0 0 I 0.0 0.0 0:51.45 kworker/2:1H-kblockd + 3261 root 0 -20 0 0 0 I 0.0 0.0 0:40.57 kworker/1:0H-kblockd + 3580 root 0 -20 0 0 0 D 0.0 0.0 0:21.37 kworker/3:0H+kblockd + 4084 root 20 0 0 0 0 I 0.0 0.0 0:03.75 kworker/u8:0-events_unbound + 4155 root 0 -20 0 0 0 I 0.0 0.0 0:08.39 kworker/0:2H-mmc_complete + 4176 root 20 0 0 0 0 I 0.0 0.0 0:00.04 kworker/1:0-mm_percpu_wq + 4185 root 20 0 0 0 0 I 0.0 0.0 0:00.14 kworker/3:0-mm_percpu_wq + 4187 root 20 0 0 0 0 D 0.0 0.0 0:00.48 kworker/2:3+events_freezable + 4191 root 20 0 0 0 0 I 0.0 0.0 0:00.13 kworker/1:2-mm_percpu_wq + 4218 root 20 0 12204 6288 5364 S 0.0 0.2 0:00.04 sshd + 4224 pi 20 0 14564 6632 5788 S 0.0 0.2 0:00.14 systemd + 4227 pi 20 0 35240 2964 1636 S 0.0 0.1 0:00.00 (sd-pam) + 4241 pi 20 0 12204 4084 3140 S 0.0 0.1 0:00.44 sshd + 4244 pi 20 0 8492 3624 2704 S 0.0 0.1 0:00.24 bash + 4255 root 20 0 0 0 0 I 0.0 0.0 0:00.31 kworker/u8:1-events_unbound + 4256 root 20 0 0 0 0 I 0.0 0.0 0:00.06 kworker/2:2-events_power_efficient + 4270 root 20 0 0 0 0 I 0.0 0.0 0:00.11 kworker/0:1-events_power_efficient + 4307 root 20 0 0 0 0 I 0.0 0.0 0:00.00 kworker/3:2 + 4326 root 0 -20 0 0 0 I 0.0 0.0 0:00.01 kworker/3:1H-kblockd + 4327 root 0 -20 0 0 0 I 0.0 0.0 0:00.01 kworker/2:2H-kblockd + 4337 root 0 -20 0 0 0 I 0.0 0.0 0:00.01 kworker/1:1H-kblockd + 4338 root 20 0 0 0 0 I 0.0 0.0 0:00.03 kworker/0:2-events + 4343 root 0 -20 0 0 0 I 0.0 0.0 0:00.01 kworker/0:0H-kblockd + 4382 root 20 0 0 0 0 I 0.0 0.0 0:00.02 kworker/2:0-mm_percpu_wq + 4389 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/2:0H-kblockd + 4390 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/3:2H + 4396 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/1:2H-kblockd + 4397 root 20 0 0 0 0 I 0.0 0.0 0:00.02 kworker/0:0-mm_percpu_wq + 4398 root 20 0 0 0 0 I 0.0 0.0 0:00.00 kworker/2:1-mm_percpu_wq + 4399 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:1H-kblockd + 4400 pi 20 0 10184 2828 2448 R 0.0 0.1 0:00.01 top diff --git a/agent/test/Main.hs b/agent/test/Main.hs new file mode 100644 index 000000000..cd7dadb3d --- /dev/null +++ b/agent/test/Main.hs @@ -0,0 +1,13 @@ +module Main where + +import Startlude + +import Test.Hspec.Runner +import Test.Hspec.Formatters +import qualified Spec +import qualified Lib.Types.EmverProp as EmverProp + +main :: IO () +main = do + EmverProp.tests + hspecWith defaultConfig { configFormatter = Just progress } Spec.spec diff --git a/agent/test/Spec.hs b/agent/test/Spec.hs new file mode 100644 index 000000000..5416ef6a8 --- /dev/null +++ b/agent/test/Spec.hs @@ -0,0 +1 @@ +{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-} diff --git a/agent/weeder.dhall b/agent/weeder.dhall new file mode 100644 index 000000000..c93f586eb --- /dev/null +++ b/agent/weeder.dhall @@ -0,0 +1 @@ +{ roots = [ "^main.main$", "^Paths_.*" ], type-class-roots = True } diff --git a/appmgr/.github/workflows/rust.yml b/appmgr/.github/workflows/rust.yml new file mode 100644 index 000000000..9ebd328dd --- /dev/null +++ b/appmgr/.github/workflows/rust.yml @@ -0,0 +1,30 @@ +name: Rust + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Cache .cargo + uses: actions/cache@v1 + with: + path: ~/.cargo + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + ${{ runner.os }}-cargo- + - name: Cache target release directory + uses: actions/cache@v1 + with: + path: target/release + key: ${{ runner.os }}-target-release-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-target-release-${{ hashFiles('**/Cargo.lock') }} + ${{ runner.os }}-target-release- + - name: Check + run: cargo check + - name: Test + run: cargo test --release diff --git a/appmgr/.gitignore b/appmgr/.gitignore new file mode 100644 index 000000000..53eaa2196 --- /dev/null +++ b/appmgr/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/appmgr/Cargo.lock b/appmgr/Cargo.lock new file mode 100644 index 000000000..b4e4a074c --- /dev/null +++ b/appmgr/Cargo.lock @@ -0,0 +1,2599 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "aho-corasick" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b476ce7103678b0c6d3d395dbbae31d48ff910bd28be979ba5d48c6351131d0d" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "appmgr" +version = "0.2.5" +dependencies = [ + "argonautica", + "async-trait", + "base32", + "clap", + "ed25519-dalek", + "emver", + "failure", + "file-lock", + "futures 0.3.7", + "git-version", + "itertools 0.9.0", + "lazy_static", + "linear-map", + "log", + "openssl", + "pest", + "pest_derive", + "prettytable-rs", + "rand 0.7.3", + "regex", + "reqwest", + "rpassword", + "serde", + "serde_cbor", + "serde_json", + "serde_yaml", + "simple-logging", + "tokio", + "tokio-tar", +] + +[[package]] +name = "argonautica" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b765e206f4ab068271148430e0799aa84d81b8a13680c680f29f6c1d67da5f37" +dependencies = [ + "base64 0.10.1", + "bindgen", + "bitflags", + "cc", + "cfg-if 0.1.10", + "failure", + "futures 0.1.30", + "futures-cpupool", + "libc", + "log", + "nom 4.2.3", + "num_cpus", + "rand 0.6.5", + "scopeguard 1.1.0", + "tempdir", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "async-trait" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b246867b8b3b6ae56035f1eb1ed557c1d8eae97f0d53696138a50fa0e3a3b8c0" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2baad346b2d4e94a24347adeee9c7a93f412ee94b9cc26e5b59dea23848e9f28" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "bindgen" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d3d411fd93fd296e613bdac1d16755a6a922a4738e1c8f6a5e13542c905f3ca" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if 0.1.10", + "clang-sys", + "clap", + "env_logger", + "hashbrown 0.1.8", + "lazy_static", + "log", + "peeking_take_while", + "proc-macro2 0.4.30", + "quote 0.6.13", + "regex", + "which", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bitvec" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2b_simd" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.3", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "bstr" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473fc6b38233f9af7baa94fb5852dca389e3d95b8e21c8e3719301462c5d9faf" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "cc" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cexpr" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce5b5fb86b0c57c20c834c1b412fd09c77c8a59b9473f86272709e78874cd1d" +dependencies = [ + "nom 4.2.3", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef0c1bcf2e99c649104bd7a7012d8f8802684400e03db0ec0af48583c6fa0e4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "csv" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279" +dependencies = [ + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "curve25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8492de420e9e60bc9a1d66e2dbb91825390b738a388606600663fc529b4b307" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.3", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + +[[package]] +name = "dtoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" + +[[package]] +name = "ed25519" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c66a534cbb46ab4ea03477eae19d5c22c01da8258030280b7bd9d8433fb6ef" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2", + "zeroize", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "emver" +version = "0.1.0" +source = "git+https://github.com/Start9Labs/emver-rs.git#9007920a8e361669fb83b29dd8506b32eeb20180" +dependencies = [ + "either", + "fp-core", + "nom 6.0.0", + "serde", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62bc5e388624f1a13da83b479275dbec9663a876e414df80decf7d2cdab6670" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "env_logger" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", + "synstructure", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "file-lock" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16486239b3741480cef090b6f9924faf5dd5481022c6f266a51fab1a92971a2" +dependencies = [ + "gcc", + "libc", + "mktemp", + "nix", +] + +[[package]] +name = "filetime" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed85775dcc68644b5c950ac06a2b23768d3bc9390464151aaf27136998dcf9e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "fp-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338a5feb6c7248603dfa3da758da4e99abb65e792a157fe1d657e7c2f5fbcd0b" +dependencies = [ + "itertools 0.8.2", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "funty" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba62103ce691c2fd80fbae2213dfdda9ce60804973ac6b6e97de818ea7f52c8" + +[[package]] +name = "futures" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7e4c2612746b0df8fed4ce0c69156021b704c9aefa360311c04e6e9e002eed" + +[[package]] +name = "futures" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95314d38584ffbfda215621d723e0a3906f032e03ae5551e650058dac83d4797" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0448174b01148032eed37ac4aed28963aaaa8cfa93569a08e5b479bbc6c2c151" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18eaa56102984bed2c88ea39026cff3ce3b4c7f508ca970cedf2450ea10d4e46" + +[[package]] +name = "futures-cpupool" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +dependencies = [ + "futures 0.1.30", + "num_cpus", +] + +[[package]] +name = "futures-executor" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f8e0c9258abaea85e78ebdda17ef9666d390e987f006be6080dfe354b708cb" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1798854a4727ff944a7b12aa999f58ce7aa81db80d2dfaaf2ba06f065ddd2b" + +[[package]] +name = "futures-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36fccf3fc58563b4a14d265027c627c3b665d7fed489427e88e7cc929559efe" +dependencies = [ + "proc-macro-hack", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e3ca3f17d6e8804ae5d3df7a7d35b2b3a6fe89dac84b31872720fc3060a0b11" + +[[package]] +name = "futures-task" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d502af37186c4fef99453df03e374683f8a1eec9dcc1e66b3b82dc8278ce3c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abcb44342f62e6f3e8ac427b8aa815f724fd705dfad060b18ac7866c15bb8e34" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project 1.0.1", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check 0.9.2", +] + +[[package]] +name = "getrandom" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + +[[package]] +name = "git-version" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94918e83f1e01dedc2e361d00ce9487b14c58c7f40bab148026fa39d42cb41e2" +dependencies = [ + "git-version-macro", + "proc-macro-hack", +] + +[[package]] +name = "git-version-macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34a97a52fdee1870a34fa6e4b77570cba531b27d1838874fef4429a791a3d657" +dependencies = [ + "proc-macro-hack", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", +] + +[[package]] +name = "glob" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", + "tracing-futures", +] + +[[package]] +name = "half" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177" + +[[package]] +name = "hashbrown" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bae29b6653b3412c2e71e9d486db9f9df5d701941d86683005efb9f2d28e3da" +dependencies = [ + "byteorder", + "scopeguard 0.3.3", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "hyper" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3afcfae8af5ad0576a31e768415edb627824129e8e5a29b8bfccb2f234e835" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project 0.4.27", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-tls", +] + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +dependencies = [ + "autocfg 1.0.1", + "hashbrown 0.9.1", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" + +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lexical-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 0.1.10", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" + +[[package]] +name = "libloading" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" +dependencies = [ + "cc", + "winapi 0.3.9", +] + +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" +dependencies = [ + "serde", + "serde_test", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +dependencies = [ + "adler", + "autocfg 1.0.1", +] + +[[package]] +name = "mio" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.1", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log", + "mio", + "miow 0.3.5", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e" +dependencies = [ + "socket2", + "winapi 0.3.9", +] + +[[package]] +name = "mktemp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77001ceb9eed65439f3dc2a2543f9ba1417d912686bf224a7738d0966e6dcd69" +dependencies = [ + "uuid", +] + +[[package]] +name = "native-tls" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "net2" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ebc3ec692ed7c9a255596c67808dee269f64655d8baf7b4f0638e51ba1d6853" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nix" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "becb657d662f1cd2ef38c7ad480ec6b8cf9e96b27adb543e594f9cf0f2e6065c" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] +name = "nom" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4489ccc7d668957ddf64af7cd027c081728903afa6479d35da7e99bf5728f75f" +dependencies = [ + "bitvec", + "lexical-core", + "memchr", + "version_check 0.9.2", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" + +[[package]] +name = "once_cell" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" +dependencies = [ + "bitflags", + "cfg-if 0.1.10", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "openssl-sys" +version = "0.9.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" +dependencies = [ + "autocfg 1.0.1", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1", +] + +[[package]] +name = "pin-project" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" +dependencies = [ + "pin-project-internal 0.4.27", +] + +[[package]] +name = "pin-project" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee41d838744f60d959d7074e3afb6b35c7456d0f61cad38a24e35e6553f73841" +dependencies = [ + "pin-project-internal 1.0.1", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a4ffa594b66bff340084d4081df649a7dc049ac8d7fc458d8e628bfbbb2f86" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "ppv-lite86" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" + +[[package]] +name = "prettytable-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e" +dependencies = [ + "atty", + "csv", + "encode_unicode", + "lazy_static", + "term", + "unicode-width", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid 0.2.1", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2 1.0.24", +] + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + +[[package]] +name = "regex" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-automata" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" +dependencies = [ + "byteorder", +] + +[[package]] +name = "regex-syntax" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9eaa17ac5d7b838b7503d118fa16ad88f440498bf9ffe5424e621f93190d61e" +dependencies = [ + "base64 0.12.3", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rpassword" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d755237fc0f99d98641540e66abac8bc46a0652f19148ac9e21de2da06b326c9" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "rust-argon2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" +dependencies = [ + "base64 0.12.3", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "scopeguard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_cbor" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_test" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a49e2f787c0fddfd5e758cbbd3cbf81c3a8d12ef091283c52d9e9b4d417d3c" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" +dependencies = [ + "dtoa", + "itoa", + "serde", + "url", +] + +[[package]] +name = "serde_yaml" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7baae0a99f1a324984bcdc5f0718384c1f69775f1c7eec8b859b71b443e3fd7" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha2" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2933378ddfeda7ea26f48c555bdad8bb446bf8a3d17832dc83e380d444cfb8c1" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 0.1.10", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "signal-hook-registry" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f060a7d147e33490ec10da418795238fd7545bba241504d6b31a409f2e6210" + +[[package]] +name = "simple-logging" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542" +dependencies = [ + "lazy_static", + "log", + "thread-id", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "socket2" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "subtle" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343f3f510c2915908f155e94f17220b19ccfacf2a64a2a5d8004f2c3e311e7fd" + +[[package]] +name = "syn" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "unicode-xid 0.2.1", +] + +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", + "unicode-xid 0.2.1", +] + +[[package]] +name = "tap" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "rand 0.7.3", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "term" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" +dependencies = [ + "byteorder", + "dirs", + "winapi 0.3.9", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +dependencies = [ + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tinyvec" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117" + +[[package]] +name = "tokio" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d34ca54d84bf2b5b4d7d31e901a8464f7b60ac145a284fba25ceb801f2ddccd" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-named-pipes", + "mio-uds", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "slab", + "tokio-macros", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-macros" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", +] + +[[package]] +name = "tokio-tar" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a9e415c93375be93253134543229563114a2be8d46440d6d8f25b2ec62a7fb" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall", + "tokio", + "xattr", +] + +[[package]] +name = "tokio-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" + +[[package]] +name = "tracing" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0987850db3733619253fe60e17cb59b82d37c7e6c0236bb81e4d6b87c879f27" +dependencies = [ + "cfg-if 0.1.10", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-futures" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +dependencies = [ + "pin-project 0.4.27", + "tracing", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check 0.9.2", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "url" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +dependencies = [ + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c590b5bd79ed10aad8fb75f078a59d8db445af6c743e55c4a53227fc01c13f" +dependencies = [ + "rand 0.3.23", + "rustc-serialize", +] + +[[package]] +name = "vcpkg" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" +dependencies = [ + "cfg-if 0.1.10", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7866cab0aa01de1edf8b5d7936938a7e397ee50ce24119aef3e1eaa3b6171da" +dependencies = [ + "cfg-if 0.1.10", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" +dependencies = [ + "quote 1.0.7", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" + +[[package]] +name = "web-sys" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b57acb10231b9493c8472b20cb57317d0679a49e0bdbee44b3b803a6473af164" +dependencies = [ + "failure", + "libc", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + +[[package]] +name = "yaml-rust" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zeroize" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f33972566adbd2d3588b0491eb94b98b43695c4ef897903470ede4f3f5a28a" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f369ddb18862aba61aa49bf31e74d29f0f162dec753063200e1dc084345d16" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn", + "synstructure", +] diff --git a/appmgr/Cargo.toml b/appmgr/Cargo.toml new file mode 100644 index 000000000..61444f663 --- /dev/null +++ b/appmgr/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "appmgr" +version = "0.2.5" +authors = ["Aiden McClelland "] +edition = "2018" + +[lib] +name = "appmgrlib" +path = "src/lib.rs" + +[[bin]] +name = "appmgr" +path = "src/main.rs" + +[features] +default = [] +portable = [] +production = [] + +[dependencies] +emver = { git = "https://github.com/Start9Labs/emver-rs.git", version = "0.1.0", features = ["serde"] } +argonautica = "0.2.0" +async-trait = "0.1.41" +base32 = "0.4.0" +clap = "2.33" +ed25519-dalek = "1.0.1" +failure = "0.1.8" +file-lock = "1.1" +futures = "0.3.7" +git-version = "0.3.4" +itertools = "0.9.0" +lazy_static = "1.4" +linear-map = { version = "1.2", features = ["serde_impl"] } +log = "0.4.11" +openssl = "0.10.30" +pest = "2.1" +pest_derive = "2.1" +prettytable-rs = "0.8.0" +rand = "0.7.3" +regex = "1.4.2" +reqwest = { version = "0.10.8", features = ["stream", "json"] } +rpassword = "5.0.0" +serde = { version = "1.0.117", features = ["derive", "rc"] } +serde_yaml = "0.8.14" +serde_cbor = "0.11.1" +serde_json = "1.0.59" +simple-logging = "2.0" +tokio = { version = "0.2.22", features = ["full"] } +tokio-tar = "0.2.0" diff --git a/appmgr/README.md b/appmgr/README.md new file mode 100644 index 000000000..b61d2845c --- /dev/null +++ b/appmgr/README.md @@ -0,0 +1,10 @@ +# appmgr + +## Exit Codes +1. General Error +2. File System IO Error +3. Docker Error +4. Config Spec violation +5. Config Rules violation +6. Requested value does not exist +7. Invalid Backup Password \ No newline at end of file diff --git a/appmgr/build-dev.sh b/appmgr/build-dev.sh new file mode 100755 index 000000000..e3ed2a7dc --- /dev/null +++ b/appmgr/build-dev.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e +shopt -s expand_aliases + +alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest' + +rust-arm-builder cargo build --release \ No newline at end of file diff --git a/appmgr/build-prod.sh b/appmgr/build-prod.sh new file mode 100755 index 000000000..673968324 --- /dev/null +++ b/appmgr/build-prod.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e +shopt -s expand_aliases + +alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest' + +rust-arm-builder cargo build --release --features=production +rust-arm-builder arm-linux-gnueabi-strip target/armv7-unknown-linux-gnueabihf/release/appmgr \ No newline at end of file diff --git a/appmgr/src/apps.rs b/appmgr/src/apps.rs new file mode 100644 index 000000000..96a445084 --- /dev/null +++ b/appmgr/src/apps.rs @@ -0,0 +1,440 @@ +use failure::ResultExt as _; +use futures::future::{BoxFuture, FutureExt, OptionFuture}; +use linear_map::{set::LinearSet, LinearMap}; +use rand::SeedableRng; + +use crate::dependencies::AppDependencies; +use crate::manifest::{Manifest, ManifestLatest}; +use crate::util::Apply; +use crate::util::{from_yaml_async_reader, PersistencePath, YamlUpdateHandle}; +use crate::Error; +use crate::ResultExt as _; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DockerStatus { + Running, + Stopped, // created || exited + Paused, + Restarting, + Removing, + Dead, +} + +fn not(b: &bool) -> bool { + !b +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppInfo { + pub title: String, + pub version: emver::Version, + pub tor_address: Option, + pub configured: bool, + #[serde(default)] + #[serde(skip_serializing_if = "not")] + pub recoverable: bool, + #[serde(default)] + #[serde(skip_serializing_if = "not")] + pub needs_restart: bool, +} + +#[derive(Clone, Debug, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppStatus { + pub status: DockerStatus, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppConfig { + pub spec: crate::config::ConfigSpec, + pub rules: Vec, + pub config: Option, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppInfoFull { + #[serde(flatten)] + pub info: AppInfo, + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub manifest: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub config: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dependencies: Option, +} + +pub async fn list_info() -> Result, Error> { + let apps_path = PersistencePath::from_ref("apps.yaml"); + let mut f = match apps_path.maybe_read(false).await.transpose()? { + Some(a) => a, + None => return Ok(LinearMap::new()), + }; + from_yaml_async_reader(&mut *f).await +} + +pub async fn list_info_mut() -> Result>, Error> { + let apps_path = PersistencePath::from_ref("apps.yaml"); + YamlUpdateHandle::new_or_default(apps_path).await +} + +pub async fn add(id: &str, info: AppInfo) -> Result<(), failure::Error> { + let mut apps = list_info_mut().await?; + apps.insert(id.to_string(), info); + apps.commit().await?; + Ok(()) +} + +pub async fn set_configured(id: &str, configured: bool) -> Result<(), Error> { + let mut apps = list_info_mut().await?; + let mut app = apps + .get_mut(id) + .ok_or_else(|| failure::format_err!("App Not Installed: {}", id)) + .with_code(crate::error::NOT_FOUND)?; + app.configured = configured; + apps.commit().await?; + Ok(()) +} + +pub async fn set_needs_restart(id: &str, needs_restart: bool) -> Result<(), Error> { + let mut apps = list_info_mut().await?; + let mut app = apps + .get_mut(id) + .ok_or_else(|| failure::format_err!("App Not Installed: {}", id)) + .with_code(crate::error::NOT_FOUND)?; + app.needs_restart = needs_restart; + apps.commit().await?; + Ok(()) +} + +pub async fn set_recoverable(id: &str, recoverable: bool) -> Result<(), Error> { + let mut apps = list_info_mut().await?; + let mut app = apps + .get_mut(id) + .ok_or_else(|| failure::format_err!("App Not Installed: {}", id)) + .with_code(crate::error::NOT_FOUND)?; + app.recoverable = recoverable; + apps.commit().await?; + Ok(()) +} + +pub async fn remove(id: &str) -> Result<(), failure::Error> { + let mut apps = list_info_mut().await?; + apps.remove(id); + apps.commit().await?; + Ok(()) +} + +pub async fn status(id: &str) -> Result { + let output = std::process::Command::new("docker") + .args(&["inspect", id, "--format", "{{.State.Status}}"]) + .stdout(std::process::Stdio::piped()) + .stderr(match log::max_level() { + log::LevelFilter::Error => std::process::Stdio::null(), + _ => std::process::Stdio::inherit(), + }) + .spawn()? + .wait_with_output()?; + crate::ensure_code!( + output.status.success(), + crate::error::DOCKER_ERROR, + "{}: Docker Error: {}", + id, + std::str::from_utf8(&output.stderr).no_code()? + ); + let status = std::str::from_utf8(&output.stdout).no_code()?; + Ok(AppStatus { + status: match status.trim() { + "running" => DockerStatus::Running, + "restarting" => DockerStatus::Restarting, + "removing" => DockerStatus::Removing, + "dead" => DockerStatus::Dead, + "created" | "exited" => DockerStatus::Stopped, + "paused" => DockerStatus::Paused, + _ => Err(format_err!("unknown status: {}", status))?, + }, + }) +} + +pub async fn manifest(id: &str) -> Result { + let manifest: Manifest = from_yaml_async_reader( + &mut *PersistencePath::from_ref("apps") + .join(id) + .join("manifest.yaml") + .read(false) + .await?, + ) + .await?; + Ok(manifest.into_latest()) +} + +pub async fn config(id: &str) -> Result { + let spec = PersistencePath::from_ref("apps") + .join(id) + .join("config_spec.yaml"); + let spec: crate::config::ConfigSpec = + crate::util::from_yaml_async_reader(&mut *spec.read(false).await?) + .await + .no_code()?; + let rules = PersistencePath::from_ref("apps") + .join(id) + .join("config_rules.yaml"); + let rules: Vec = + crate::util::from_yaml_async_reader(&mut *rules.read(false).await?) + .await + .no_code()?; + let config = PersistencePath::from_ref("apps") + .join(id) + .join("config.yaml"); + let config: Option = match config + .maybe_read(false) + .await + .transpose()? + .map(|mut f| async move { from_yaml_async_reader(&mut *f).await }) + .apply(OptionFuture::from) + .await + { + Some(Ok(cfg)) => Some(cfg), + #[cfg(not(feature = "production"))] + Some(Err(e)) => return Err(e), + _ => { + let volume_config = std::path::Path::new(crate::VOLUMES) + .join(id) + .join("start9") + .join("config.yaml"); + if volume_config.exists() { + let cfg_path = config.path(); + tokio::fs::copy(&volume_config, &cfg_path) + .await + .with_context(|e| { + format!( + "{}: {} -> {}", + e, + volume_config.display(), + cfg_path.display() + ) + }) + .with_code(crate::error::FILESYSTEM_ERROR)?; + let mut f = tokio::fs::File::open(&volume_config) + .await + .with_context(|e| format!("{}: {}", e, volume_config.display())) + .with_code(crate::error::FILESYSTEM_ERROR)?; + match from_yaml_async_reader(&mut f).await { + Ok(a) => Some(a), + #[cfg(not(feature = "production"))] + Err(e) => return Err(e), + #[cfg(feature = "production")] + _ => None, + } + } else { + None + } + } + }; + Ok(AppConfig { + spec, + rules, + config, + }) +} + +pub async fn config_or_default(id: &str) -> Result { + let config = config(id).await?; + Ok(if let Some(config) = config.config { + config + } else { + config + .spec + .gen(&mut rand::rngs::StdRng::from_entropy(), &None) + .with_code(crate::error::CFG_SPEC_VIOLATION)? + }) +} + +pub async fn info(id: &str) -> Result { + list_info() + .await + .map_err(Error::from)? + .get(id) + .ok_or_else(|| Error::new(failure::format_err!("{} is not installed", id), Some(6))) + .map(Clone::clone) +} + +pub async fn info_full( + id: &str, + with_status: bool, + with_manifest: bool, + with_config: bool, + with_dependencies: bool, +) -> Result { + Ok(AppInfoFull { + info: info(id).await?, + status: if with_status { + Some(status(id).await?) + } else { + None + }, + manifest: if with_manifest { + Some(manifest(id).await?) + } else { + None + }, + config: if with_config { + Some(config(id).await?) + } else { + None + }, + dependencies: if with_dependencies { + Some(dependencies(id, true).await?) + } else { + None + }, + }) +} + +pub async fn dependencies(id_version: &str, local_only: bool) -> Result { + let mut id_version_iter = id_version.split("@"); + let id = id_version_iter.next().unwrap(); + let version_range = id_version_iter + .next() + .map(|a| a.parse::()) + .transpose() + .with_context(|e| format!("Failed to Parse Version Requirement: {}", e)) + .no_code()? + .unwrap_or_else(emver::VersionRange::any); + let (manifest, config_info) = match list_info().await?.get(id) { + Some(info) if info.version.satisfies(&version_range) => { + futures::try_join!(manifest(id), config(id))? + } + _ if !local_only => futures::try_join!( + crate::registry::manifest(id, &version_range), + crate::registry::config(id, &version_range) + )?, + _ => { + return Err(failure::format_err!("App Not Installed: {}", id)) + .with_code(crate::error::NOT_FOUND) + } + }; + let config = if let Some(cfg) = config_info.config { + cfg + } else { + config_info + .spec + .gen(&mut rand::rngs::StdRng::from_entropy(), &None) + .unwrap_or_default() + }; + crate::dependencies::check_dependencies(manifest, &config, &config_info.spec).await +} + +pub async fn dependents(id: &str, transitive: bool) -> Result, Error> { + pub fn dependents_rec<'a>( + id: &'a str, + transitive: bool, + res: &'a mut LinearSet, + ) -> BoxFuture<'a, Result<(), Error>> { + async move { + for (app_id, _) in list_info().await? { + let manifest = manifest(&app_id).await?; + match manifest.dependencies.0.get(id) { + Some(info) if !res.contains(&app_id) => { + let config_info = config(&app_id).await?; + let config = if let Some(cfg) = config_info.config { + cfg + } else { + config_info + .spec + .gen(&mut rand::rngs::StdRng::from_entropy(), &None) + .unwrap_or_default() + }; + if info.optional.is_none() || config_info.spec.requires(&id, &config) { + res.insert(app_id.clone()); + if transitive { + dependents_rec(&app_id, true, res).await?; + } + } + } + _ => (), + } + } + Ok(()) + } + .boxed() + } + let mut res = LinearSet::new(); + dependents_rec(id, transitive, &mut res).await?; + Ok(res) +} + +pub async fn list( + with_status: bool, + with_manifest: bool, + with_config: bool, + with_dependencies: bool, +) -> Result, Error> { + let info = list_info().await?; + futures::future::join_all(info.into_iter().map(move |(id, info)| async move { + let (status, manifest, config, dependencies) = futures::try_join!( + OptionFuture::from(if with_status { Some(status(&id)) } else { None }) + .map(Option::transpose), + OptionFuture::from(if with_manifest { + Some(manifest(&id)) + } else { + None + }) + .map(Option::transpose), + OptionFuture::from(if with_config { Some(config(&id)) } else { None }) + .map(Option::transpose), + OptionFuture::from(if with_dependencies { + Some(dependencies(&id, true)) + } else { + None + }) + .map(Option::transpose) + )?; + Ok(( + id, + AppInfoFull { + info, + status, + manifest, + config, + dependencies, + }, + )) + })) + .await + .into_iter() + .collect() +} + +pub async fn print_instructions(id: &str) -> Result<(), Error> { + if let Some(file) = PersistencePath::from_ref("apps") + .join(id) + .join("instructions.md") + .maybe_read(false) + .await + { + use tokio::io::AsyncWriteExt; + + let mut stdout = tokio::io::stdout(); + tokio::io::copy(&mut *file?, &mut stdout) + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + stdout + .flush() + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + stdout + .shutdown() + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + Ok(()) + } else { + Err(failure::format_err!("No Instructions: {}", id)).with_code(crate::error::NOT_FOUND) + } +} diff --git a/appmgr/src/backup.rs b/appmgr/src/backup.rs new file mode 100644 index 000000000..52a30c6a9 --- /dev/null +++ b/appmgr/src/backup.rs @@ -0,0 +1,238 @@ +use std::path::Path; + +use argonautica::{Hasher, Verifier}; +use futures::try_join; +use futures::TryStreamExt; + +use crate::apps; +use crate::util::from_yaml_async_reader; +use crate::util::Invoke; +use crate::Error; +use crate::ResultExt; + +pub async fn create_backup>( + path: P, + app_id: &str, + password: &str, +) -> Result<(), Error> { + let path = tokio::fs::canonicalize(path).await?; + crate::ensure_code!( + path.is_dir(), + crate::error::FILESYSTEM_ERROR, + "Backup Path Must Be Directory" + ); + let pw_path = path.join("password"); + let data_path = path.join("data"); + let tor_path = path.join("tor"); + let volume_path = Path::new(crate::VOLUMES).join(app_id); + let hidden_service_path = + Path::new(crate::tor::HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", app_id)); + + if pw_path.exists() { + use tokio::io::AsyncReadExt; + + let mut f = tokio::fs::File::open(&pw_path).await?; + let mut hash = String::new(); + f.read_to_string(&mut hash).await?; + crate::ensure_code!( + Verifier::new() + .with_password(password) + .with_hash(hash) + .verify() + .with_code(crate::error::INVALID_BACKUP_PASSWORD)?, + crate::error::INVALID_BACKUP_PASSWORD, + "Invalid Backup Decryption Password" + ); + } + { + // save password + use tokio::io::AsyncWriteExt; + + let mut hasher = Hasher::default(); + hasher.opt_out_of_secret_key(true); + let hash = hasher.with_password(password).hash().no_code()?; + let mut f = tokio::fs::File::create(pw_path).await?; + f.write_all(hash.as_bytes()).await?; + f.flush().await?; + } + + let status = crate::apps::status(app_id).await?; + let exclude = if volume_path.is_dir() { + let ignore_path = volume_path.join(".backupignore"); + if ignore_path.is_file() { + use tokio::io::AsyncBufReadExt; + tokio::io::BufReader::new(tokio::fs::File::open(ignore_path).await?) + .lines() + .try_filter(|l| futures::future::ready(!l.is_empty())) + .try_collect() + .await? + } else { + Vec::new() + } + } else { + return Err(format_err!("Volume For {} Does Not Exist", app_id)) + .with_code(crate::error::NOT_FOUND); + }; + let running = status.status == crate::apps::DockerStatus::Running; + if running { + crate::control::pause_app(&app_id).await?; + } + let mut data_cmd = tokio::process::Command::new("duplicity"); + for exclude in exclude { + if exclude.starts_with('!') { + data_cmd.arg(format!( + "--include={}", + volume_path.join(exclude.trim_start_matches('!')).display() + )); + } else { + data_cmd.arg(format!("--exclude={}", volume_path.join(exclude).display())); + } + } + let data_res = data_cmd + .env("PASSPHRASE", password) + .arg(volume_path) + .arg(format!("file://{}", data_path.display())) + .invoke("Duplicity") + .await; + let tor_res = tokio::process::Command::new("duplicity") + .env("PASSPHRASE", password) + .arg(hidden_service_path) + .arg(format!("file://{}", tor_path.display())) + .invoke("Duplicity") + .await; + if running { + if crate::apps::info(&app_id).await?.needs_restart { + crate::control::restart_app(&app_id).await?; + } else { + crate::control::resume_app(&app_id).await?; + } + } + data_res?; + tor_res?; + + Ok(()) +} + +pub async fn restore_backup>( + path: P, + app_id: &str, + password: &str, +) -> Result<(), Error> { + let path = tokio::fs::canonicalize(path).await?; + crate::ensure_code!( + path.is_dir(), + crate::error::FILESYSTEM_ERROR, + "Backup Path Must Be Directory" + ); + let pw_path = path.join("password"); + let data_path = path.join("data"); + let tor_path = path.join("tor"); + let volume_path = Path::new(crate::VOLUMES).join(app_id); + let hidden_service_path = + Path::new(crate::tor::HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", app_id)); + + if pw_path.exists() { + use tokio::io::AsyncReadExt; + + let mut f = tokio::fs::File::open(&pw_path).await?; + let mut hash = String::new(); + f.read_to_string(&mut hash).await?; + crate::ensure_code!( + Verifier::new() + .with_password(password) + .with_hash(hash) + .verify() + .with_code(crate::error::INVALID_BACKUP_PASSWORD)?, + crate::error::INVALID_BACKUP_PASSWORD, + "Invalid Backup Decryption Password" + ); + } + + let status = crate::apps::status(app_id).await?; + let running = status.status == crate::apps::DockerStatus::Running; + if running { + crate::control::stop_app(app_id, true, false).await?; + } + + let mut data_cmd = tokio::process::Command::new("duplicity"); + data_cmd + .env("PASSPHRASE", password) + .arg("--force") + .arg(format!("file://{}", data_path.display())) + .arg(&volume_path); + + let mut tor_cmd = tokio::process::Command::new("duplicity"); + tor_cmd + .env("PASSPHRASE", password) + .arg("--force") + .arg(format!("file://{}", tor_path.display())) + .arg(&hidden_service_path); + + let (data_output, tor_output) = try_join!(data_cmd.status(), tor_cmd.status())?; + crate::ensure_code!( + data_output.success(), + crate::error::GENERAL_ERROR, + "Duplicity Error" + ); + crate::ensure_code!( + tor_output.success(), + crate::error::GENERAL_ERROR, + "Duplicity Error" + ); + + // Fix the tor address in apps.yaml + let mut yhdl = apps::list_info_mut().await?; + if let Some(app_info) = yhdl.get_mut(app_id) { + app_info.tor_address = Some(crate::tor::read_tor_address(app_id, None).await?); + } + yhdl.commit().await?; + + // Attempt to configure the service with the config coming from restoration + let cfg_path = Path::new(crate::VOLUMES) + .join(app_id) + .join("start9") + .join("config.yaml"); + if cfg_path.exists() { + let cfg = from_yaml_async_reader(tokio::fs::File::open(cfg_path).await?).await?; + if let Err(e) = crate::config::configure(app_id, cfg, None, false).await { + log::warn!("Could not restore backup configuration: {}", e); + } + } + + crate::tor::restart().await?; + + Ok(()) +} + +pub async fn backup_to_partition( + logicalname: &str, + app_id: &str, + password: &str, +) -> Result<(), Error> { + let backup_mount_path = Path::new(crate::BACKUP_MOUNT_POINT); + let guard = crate::disks::MountGuard::new(logicalname, &backup_mount_path).await?; + let backup_dir_path = backup_mount_path.join(crate::BACKUP_DIR).join(app_id); + tokio::fs::create_dir_all(&backup_dir_path).await?; + + let res = create_backup(backup_dir_path, app_id, password).await; + + guard.unmount().await?; + + res +} + +pub async fn restore_from_partition( + logicalname: &str, + app_id: &str, + password: &str, +) -> Result<(), Error> { + let backup_mount_path = Path::new(crate::BACKUP_MOUNT_POINT); + let guard = crate::disks::MountGuard::new(logicalname, &backup_mount_path).await?; + let backup_dir_path = backup_mount_path.join(crate::BACKUP_DIR).join(app_id); + + let res = restore_backup(backup_dir_path, app_id, password).await; + + guard.unmount().await?; + + res +} diff --git a/appmgr/src/config/mod.rs b/appmgr/src/config/mod.rs new file mode 100644 index 000000000..67df94f9e --- /dev/null +++ b/appmgr/src/config/mod.rs @@ -0,0 +1,324 @@ +use std::borrow::Cow; +use std::path::Path; +use std::time::Duration; + +use failure::ResultExt as _; +use futures::future::{BoxFuture, FutureExt}; +use itertools::Itertools; +use linear_map::{set::LinearSet, LinearMap}; +use rand::SeedableRng; +use regex::Regex; + +use crate::dependencies::{DependencyError, TaggedDependencyError}; +use crate::util::PersistencePath; +use crate::util::{from_yaml_async_reader, to_yaml_async_writer}; +use crate::ResultExt as _; + +pub mod rules; +pub mod spec; +pub mod util; +pub mod value; + +pub use rules::{ConfigRuleEntry, ConfigRuleEntryWithSuggestions}; +pub use spec::{ConfigSpec, Defaultable}; +use util::NumRange; +pub use value::Config; + +#[derive(Debug, Fail)] +pub enum ConfigurationError { + #[fail(display = "Timeout Error")] + TimeoutError, + #[fail(display = "No Match: {}", _0)] + NoMatch(NoMatchWithPath), + #[fail(display = "Invalid Variant: {}", _0)] + InvalidVariant(String), + #[fail(display = "System Error: {}", _0)] + SystemError(crate::Error), +} +impl From for ConfigurationError { + fn from(_: TimeoutError) -> Self { + ConfigurationError::TimeoutError + } +} +impl From for ConfigurationError { + fn from(e: NoMatchWithPath) -> Self { + ConfigurationError::NoMatch(e) + } +} + +#[derive(Clone, Copy, Debug, Fail)] +#[fail(display = "Timeout Error")] +pub struct TimeoutError; + +#[derive(Clone, Debug, Fail)] +pub struct NoMatchWithPath { + pub path: Vec, + pub error: MatchError, +} +impl NoMatchWithPath { + pub fn new(error: MatchError) -> Self { + NoMatchWithPath { + path: Vec::new(), + error, + } + } + pub fn prepend(mut self, seg: String) -> Self { + self.path.push(seg); + self + } +} +impl std::fmt::Display for NoMatchWithPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.path.iter().rev().join("."), self.error) + } +} + +#[derive(Clone, Debug, Fail)] +pub enum MatchError { + #[fail(display = "String {:?} Does Not Match Pattern {}", _0, _1)] + Pattern(String, Regex), + #[fail(display = "String {:?} Is Not In Enum {:?}", _0, _1)] + Enum(String, LinearSet), + #[fail(display = "Field Is Not Nullable")] + NotNullable, + #[fail(display = "Length Mismatch: expected {}, actual: {}", _0, _1)] + LengthMismatch(NumRange, usize), + #[fail(display = "Invalid Type: expected {}, actual: {}", _0, _1)] + InvalidType(&'static str, &'static str), + #[fail(display = "Number Out Of Range: expected {}, actual: {}", _0, _1)] + OutOfRange(NumRange, f64), + #[fail(display = "Number Is Not Integral: {}", _0)] + NonIntegral(f64), + #[fail(display = "Variant {:?} Is Not In Union {:?}", _0, _1)] + Union(String, LinearSet), + #[fail(display = "Variant Is Missing Tag {:?}", _0)] + MissingTag(String), + #[fail( + display = "Property {:?} Of Variant {:?} Conflicts With Union Tag", + _0, _1 + )] + PropertyMatchesUnionTag(String, String), + #[fail(display = "Name of Property {:?} Conflicts With Map Tag Name", _0)] + PropertyNameMatchesMapTag(String), + #[fail(display = "Pointer Is Invalid: {}", _0)] + InvalidPointer(spec::ValueSpecPointer), + #[fail(display = "Object Key Is Invalid: {}", _0)] + InvalidKey(String), + #[fail(display = "Value In List Is Not Unique")] + ListUniquenessViolation, +} + +#[derive(Clone, Debug, Default, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct ConfigurationRes { + pub changed: LinearMap, + pub needs_restart: LinearSet, + pub stopped: LinearMap, +} + +// returns apps with changed configurations +pub async fn configure( + name: &str, + config: Option, + timeout: Option, + dry_run: bool, +) -> Result { + async fn handle_broken_dependent( + name: &str, + dependent: String, + dry_run: bool, + res: &mut ConfigurationRes, + error: DependencyError, + ) -> Result<(), crate::Error> { + crate::control::stop_dependents( + &dependent, + dry_run, + DependencyError::NotRunning, + &mut res.stopped, + ) + .await?; + if crate::apps::status(&dependent).await?.status != crate::apps::DockerStatus::Stopped { + crate::control::stop_app(&dependent, false, dry_run).await?; + res.stopped.insert( + // TODO: maybe don't do this if its not running + dependent, + TaggedDependencyError { + dependency: name.to_owned(), + error, + }, + ); + } + Ok(()) + } + fn configure_rec<'a>( + name: &'a str, + config: Option, + timeout: Option, + dry_run: bool, + res: &'a mut ConfigurationRes, + ) -> BoxFuture<'a, Result> { + async move { + let info = crate::apps::list_info() + .await? + .remove(name) + .ok_or_else(|| failure::format_err!("{} is not installed", name)) + .with_code(crate::error::NOT_FOUND)?; + let mut rng = rand::rngs::StdRng::from_entropy(); + let spec_path = PersistencePath::from_ref("apps") + .join(name) + .join("config_spec.yaml"); + let rules_path = PersistencePath::from_ref("apps") + .join(name) + .join("config_rules.yaml"); + let config_path = PersistencePath::from_ref("apps") + .join(name) + .join("config.yaml"); + let spec: ConfigSpec = + from_yaml_async_reader(&mut *spec_path.read(false).await?).await?; + let rules: Vec = + from_yaml_async_reader(&mut *rules_path.read(false).await?).await?; + let old_config: Option = + if let Some(mut f) = config_path.maybe_read(false).await.transpose()? { + Some(from_yaml_async_reader(&mut *f).await?) + } else { + None + }; + let mut config = if let Some(cfg) = config { + cfg + } else { + if let Some(old) = &old_config { + old.clone() + } else { + spec.gen(&mut rng, &timeout) + .with_code(crate::error::CFG_SPEC_VIOLATION)? + } + }; + spec.matches(&config) + .with_code(crate::error::CFG_SPEC_VIOLATION)?; + spec.update(&mut config) + .await + .with_code(crate::error::CFG_SPEC_VIOLATION)?; + let mut cfgs = LinearMap::new(); + cfgs.insert(name, Cow::Borrowed(&config)); + for rule in rules { + rule.check(&config, &cfgs) + .with_code(crate::error::CFG_RULES_VIOLATION)?; + } + match old_config { + Some(old) if &old == &config && info.configured && !info.recoverable => { + return Ok(config) + } + _ => (), + }; + res.changed.insert(name.to_owned(), config.clone()); + for dependent in crate::apps::dependents(name, false).await? { + match configure_rec(&dependent, None, timeout, dry_run, res).await { + Ok(dependent_config) => { + let man = crate::apps::manifest(&dependent).await?; + if let Some(dep_info) = man.dependencies.0.get(name) { + match dep_info + .satisfied( + name, + Some(config.clone()), + &dependent, + &dependent_config, + ) + .await? + { + Ok(_) => (), + Err(e) => { + handle_broken_dependent(name, dependent, dry_run, res, e) + .await?; + } + } + } + } + Err(e) => { + if e.code == Some(crate::error::CFG_RULES_VIOLATION) + || e.code == Some(crate::error::CFG_SPEC_VIOLATION) + { + if !dry_run { + crate::apps::set_configured(&dependent, false).await?; + } + handle_broken_dependent( + name, + dependent, + dry_run, + res, + DependencyError::PointerUpdateError(format!("{}", e)), + ) + .await?; + } else { + handle_broken_dependent( + name, + dependent, + dry_run, + res, + DependencyError::Other(format!("{}", e)), + ) + .await?; + } + } + } + } + if !dry_run { + let mut file = config_path.write(None).await?; + to_yaml_async_writer(file.as_mut(), &config).await?; + file.commit().await?; + let volume_config = Path::new(crate::VOLUMES) + .join(name) + .join("start9") + .join("config.yaml"); + tokio::fs::copy(config_path.path(), &volume_config) + .await + .with_context(|e| { + format!( + "{}: {} -> {}", + e, + config_path.path().display(), + volume_config.display() + ) + }) + .with_code(crate::error::FILESYSTEM_ERROR)?; + crate::apps::set_configured(name, true).await?; + crate::apps::set_recoverable(name, false).await?; + } + if crate::apps::status(name).await?.status != crate::apps::DockerStatus::Stopped { + if !dry_run { + crate::apps::set_needs_restart(name, true).await?; + } + res.needs_restart.insert(name.to_string()); + } + Ok(config) + } + .boxed() + } + let mut res = ConfigurationRes::default(); + configure_rec(name, config, timeout, dry_run, &mut res).await?; + Ok(res) +} + +pub async fn remove(name: &str) -> Result<(), crate::Error> { + let config_path = PersistencePath::from_ref("apps") + .join(name) + .join("config.yaml") + .path(); + if config_path.exists() { + tokio::fs::remove_file(&config_path) + .await + .with_context(|e| format!("{}: {}", e, config_path.display())) + .with_code(crate::error::FILESYSTEM_ERROR)?; + } + let volume_config = Path::new(crate::VOLUMES) + .join(name) + .join("start9") + .join("config.yaml"); + if volume_config.exists() { + tokio::fs::remove_file(&volume_config) + .await + .with_context(|e| format!("{}: {}", e, volume_config.display())) + .with_code(crate::error::FILESYSTEM_ERROR)?; + } + crate::apps::set_configured(name, false).await?; + Ok(()) +} diff --git a/appmgr/src/config/rule_parser.pest b/appmgr/src/config/rule_parser.pest new file mode 100644 index 000000000..53be53252 --- /dev/null +++ b/appmgr/src/config/rule_parser.pest @@ -0,0 +1,76 @@ +num = @{ int ~ ("." ~ ASCII_DIGIT*)? ~ (^"e" ~ int)? } + int = @{ ("+" | "-")? ~ ASCII_DIGIT+ } + +raw_string = @{ (!("\\" | "\"") ~ ANY)+ } +predefined = @{ "n" | "r" | "t" | "\\" | "0" | "\"" | "'" } +escape = @{ "\\" ~ predefined } +str = @{ "\"" ~ (raw_string | escape)* ~ "\"" } + +ident_char = @{ ASCII_ALPHANUMERIC | "-" } +sub_ident = _{ sub_ident_regular | sub_ident_index | sub_ident_any | sub_ident_all | sub_ident_fn } + sub_ident_regular = { sub_ident_regular_base | sub_ident_regular_expr } + sub_ident_regular_base = @{ ASCII_ALPHA ~ ident_char* } + sub_ident_regular_expr = ${ "[" ~ str_expr ~ "]" } + sub_ident_index = { sub_ident_index_base | sub_ident_index_expr } + sub_ident_index_base = @{ ASCII_DIGIT+ } + sub_ident_index_expr = ${ "[" ~ num_expr ~ "]" } + sub_ident_any = @{ "*" } + sub_ident_all = @{ "&" } + sub_ident_fn = ${ "[" ~ list_access_function ~ "]"} + list_access_function = _{ list_access_function_first | list_access_function_last | list_access_function_any | list_access_function_all } + list_access_function_first = !{ "first" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" } + list_access_function_last = !{ "last" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" } + list_access_function_any = !{ "any" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" } + list_access_function_all = !{ "all" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" } + +app_id = ${ "[" ~ sub_ident_regular ~ "]" } +ident = _{ (app_id ~ ".")? ~ sub_ident_regular ~ ("." ~ sub_ident)* } +bool_var = ${ ident ~ "?" } +num_var = ${ "#" ~ ident } +str_var = ${ "'" ~ ident } +any_var = ${ ident } + +bool_op = _{ and | or | xor } + and = { "AND" } + or = { "OR" } + xor = { "XOR" } + +num_cmp_op = _{ lt | lte | eq | neq | gt | gte } +str_cmp_op = _{ lt | lte | eq | neq | gt | gte } + lt = { "<" } + lte = { "<=" } + eq = { "=" } + neq = { "!=" } + gt = { ">" } + gte = { ">=" } + +num_op = _{ add | sub | mul | div | pow } +str_op = _{ add } + add = { "+" } + sub = { "-" } + mul = { "*" } + div = { "/" } + pow = { "^" } + +num_expr = !{ num_term ~ (num_op ~ num_term)* } +num_term = _{ num | num_var | "(" ~ num_expr ~ ")" } + +str_expr = !{ str_term ~ (str_op ~ str_term)* } +str_term = _{ str | str_var | "(" ~ str_expr ~ ")" } + +num_cmp_expr = { num_expr ~ num_cmp_op ~ num_expr } +str_cmp_expr = { str_expr ~ str_cmp_op ~ str_expr } + +bool_expr = !{ bool_term ~ (bool_op ~ bool_term)* } +inv_bool_expr = { "!(" ~ bool_expr ~ ")" } +bool_term = _{ bool_var | "(" ~ bool_expr ~ ")" | inv_bool_expr | num_cmp_expr | str_cmp_expr } + +val_expr = _{ any_var | str_expr | num_expr | bool_expr } + +rule = _{ SOI ~ bool_expr ~ EOI } +reference = _{ SOI ~ any_var ~ EOI } +value = _{ SOI ~ val_expr ~ EOI } +del_action = _{ SOI ~ "FROM" ~ any_var ~ "AS" ~ sub_ident_regular ~ "WHERE" ~ bool_expr ~ EOI } +obj_key = _{ SOI ~ sub_ident_regular ~ EOI } + +WHITESPACE = _{ " " | "\t" } \ No newline at end of file diff --git a/appmgr/src/config/rules.rs b/appmgr/src/config/rules.rs new file mode 100644 index 000000000..f64cff1ef --- /dev/null +++ b/appmgr/src/config/rules.rs @@ -0,0 +1,1252 @@ +use std::borrow::Cow; +use std::sync::Arc; + +use linear_map::LinearMap; +use pest::iterators::Pairs; +use pest::Parser; +use rand::SeedableRng; + +use super::util::STATIC_NULL; +use super::value::{Config, Value}; + +#[derive(Parser)] +#[grammar = "config/rule_parser.pest"] +struct RuleParser; + +lazy_static::lazy_static! { + static ref NUM_PREC_CLIMBER: pest::prec_climber::PrecClimber = { + use pest::prec_climber::*; + use Rule::*; + use Assoc::*; + + PrecClimber::new(vec![ + Operator::new(add, Left) | Operator::new(sub, Left), + Operator::new(mul, Left) | Operator::new(div, Left), + Operator::new(pow, Right) + ]) + }; + + static ref STR_PREC_CLIMBER: pest::prec_climber::PrecClimber = { + use pest::prec_climber::*; + use Rule::*; + use Assoc::*; + + PrecClimber::new(vec![ + Operator::new(add, Left) + ]) + }; + + static ref BOOL_PREC_CLIMBER: pest::prec_climber::PrecClimber = { + use pest::prec_climber::*; + use Rule::*; + use Assoc::*; + + PrecClimber::new(vec![ + Operator::new(or, Left), + Operator::new(xor, Left), + Operator::new(and, Left) + ]) + }; +} + +pub type Accessor = Box< + dyn for<'a> Fn(&'a Value, &LinearMap<&str, Cow>) -> VarRes<&'a Value> + Send + Sync, +>; +pub type AccessorMut = Box< + dyn for<'a> Fn(&'a mut Value, &LinearMap<&str, Cow>) -> Option<&'a mut Value> + + Send + + Sync, +>; +pub type CompiledExpr = Box>) -> T + Send + Sync>; +pub type CompiledReference = Box< + dyn for<'a> Fn(&'a mut Config, &LinearMap<&str, Cow>) -> Option<&'a mut Value> + + Send + + Sync, +>; +pub type Mutator = Box>) + Send + Sync>; +pub type CompiledRule = Box>) -> bool + Send + Sync>; +pub type CompiledRuleRes = Result; + +#[derive(Clone)] +pub struct ConfigRule { + pub src: String, + pub compiled: Arc, +} +impl std::fmt::Debug for ConfigRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConfigRule") + .field("src", &self.src) + .field("compiled", &"Fn(&Config, &Config) -> bool") + .finish() + } +} +impl<'de> serde::de::Deserialize<'de> for ConfigRule { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let src = String::deserialize(deserializer)?; + let compiled = compile(&src).map_err(serde::de::Error::custom)?; + Ok(ConfigRule { + src, + compiled: Arc::new(compiled), + }) + } +} +impl serde::ser::Serialize for ConfigRule { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(&self.src) + } +} +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct ConfigRuleEntry { + pub rule: ConfigRule, + pub description: String, +} +impl ConfigRuleEntry { + pub fn check( + &self, + cfg: &Config, + cfgs: &LinearMap<&str, Cow>, + ) -> Result<(), failure::Error> { + if !(self.rule.compiled)(cfg, cfgs) { + failure::bail!("{}", self.description); + } + Ok(()) + } +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum SetVariant { + To(String), + ToValue(Value), + ToEntropy(super::spec::Entropy), +} + +#[derive(Clone)] +pub enum SuggestionVariant { + Set { + var: String, + to: SetVariant, + compiled: Arc, + }, + Delete { + src: String, + compiled: Arc, + }, + Push { + to: String, + value: Value, + compiled: Arc, + }, +} +impl SuggestionVariant { + pub fn apply<'a>( + &self, + id: &'a str, + cfg: &mut Config, + cfgs: &mut LinearMap<&'a str, Cow>, + ) { + match self { + SuggestionVariant::Set { ref compiled, .. } => compiled(cfg, cfgs), + SuggestionVariant::Delete { ref compiled, .. } => compiled(cfg, cfgs), + SuggestionVariant::Push { ref compiled, .. } => compiled(cfg, cfgs), + } + cfgs.insert(id, Cow::Owned(cfg.clone())); + } +} +impl std::fmt::Debug for SuggestionVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SuggestionVariant::Set { + ref var, ref to, .. + } => f + .debug_struct("SuggestionVariant::Set") + .field("var", var) + .field("to", to) + .field("compiled", &"Fn(&mut Config, Config)") + .finish(), + SuggestionVariant::Delete { ref src, .. } => f + .debug_struct("SuggestionVariant::Delete") + .field("src", src) + .field("compiled", &"Fn(&mut Config, Config)") + .finish(), + SuggestionVariant::Push { + ref to, ref value, .. + } => f + .debug_struct("SuggestionVariant::Delete") + .field("to", to) + .field("value", value) + .field("compiled", &"Fn(&mut Config, Config)") + .finish(), + } + } +} +impl<'de> serde::de::Deserialize<'de> for SuggestionVariant { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + enum _SuggestionVariant { + SET { + var: String, + #[serde(flatten)] + to: SetVariant, + }, + DELETE(String), + PUSH { + to: String, + value: Value, + }, + } + let raw = _SuggestionVariant::deserialize(deserializer)?; + Ok(match raw { + _SuggestionVariant::SET { var, to } => SuggestionVariant::Set { + compiled: Arc::new( + compile_set_action(&var, &to).map_err(serde::de::Error::custom)?, + ), + to: to, + var: var, + }, + _SuggestionVariant::DELETE(src) => SuggestionVariant::Delete { + compiled: Arc::new( + compile_del_action( + RuleParser::parse(Rule::del_action, &src) + .map_err(serde::de::Error::custom)?, + ) + .map_err(serde::de::Error::custom)?, + ), + src, + }, + _SuggestionVariant::PUSH { to, value } => SuggestionVariant::Push { + compiled: Arc::new( + compile_push_action( + RuleParser::parse(Rule::reference, &to) + .map_err(serde::de::Error::custom)?, + value.clone(), + ) + .map_err(serde::de::Error::custom)?, + ), + to, + value, + }, + }) + } +} +impl serde::ser::Serialize for SuggestionVariant { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + #[derive(serde::Serialize)] + enum _SuggestionVariant<'a> { + SET { + var: &'a str, + #[serde(flatten)] + to: &'a SetVariant, + }, + DELETE(&'a str), + PUSH { + to: &'a str, + value: &'a Value, + }, + } + match self { + SuggestionVariant::Set { + ref var, ref to, .. + } => serde::ser::Serialize::serialize(&_SuggestionVariant::SET { var, to }, serializer), + SuggestionVariant::Delete { ref src, .. } => { + serde::ser::Serialize::serialize(&_SuggestionVariant::DELETE(src), serializer) + } + SuggestionVariant::Push { + ref to, ref value, .. + } => serde::ser::Serialize::serialize( + &_SuggestionVariant::PUSH { to, value }, + serializer, + ), + } + } +} +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Suggestion { + #[serde(rename = "if")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub condition: Option, + #[serde(flatten)] + pub variant: SuggestionVariant, +} +impl Suggestion { + pub fn apply<'a>( + &self, + id: &'a str, + cfg: &mut Config, + cfgs: &mut LinearMap<&'a str, Cow>, + ) { + match &self.condition { + Some(condition) if !(condition.compiled)(cfg, cfgs) => (), + _ => self.variant.apply(id, cfg, cfgs), + } + } +} +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct ConfigRuleEntryWithSuggestions { + #[serde(flatten)] + pub entry: ConfigRuleEntry, + pub suggestions: Vec, +} +impl ConfigRuleEntryWithSuggestions { + pub fn apply<'a>( + &self, + id: &'a str, + cfg: &mut Config, + cfgs: &mut LinearMap<&'a str, Cow>, + ) -> Result<(), failure::Error> { + if self.entry.check(cfg, cfgs).is_err() { + for suggestion in &self.suggestions { + suggestion.apply(id, cfg, cfgs); + } + self.entry.check(cfg, cfgs) + } else { + Ok(()) + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum VarRes { + Exactly(T), + Any(Vec>), + All(Vec>), +} +impl VarRes { + fn map U>(self, mut f: F) -> VarRes { + fn map_rec U>(s: VarRes, f: &mut F) -> VarRes { + match s { + VarRes::Exactly(a) => VarRes::Exactly(f(a)), + VarRes::Any(a) => VarRes::Any(a.into_iter().map(|a| map_rec(a, f)).collect()), + VarRes::All(a) => VarRes::All(a.into_iter().map(|a| map_rec(a, f)).collect()), + } + } + map_rec(self, &mut f) + } + fn and_then VarRes>(self, mut f: F) -> VarRes { + fn and_then_rec VarRes>(s: VarRes, f: &mut F) -> VarRes { + match s { + VarRes::Exactly(a) => f(a), + VarRes::Any(a) => VarRes::Any(a.into_iter().map(|a| and_then_rec(a, f)).collect()), + VarRes::All(a) => VarRes::All(a.into_iter().map(|a| and_then_rec(a, f)).collect()), + } + } + and_then_rec(self, &mut f) + } +} +impl VarRes { + fn resolve(self) -> bool { + match self { + VarRes::Exactly(a) => a, + VarRes::Any(a) => a.into_iter().any(|a| a.resolve()), + VarRes::All(a) => a.into_iter().all(|a| a.resolve()), + } + } +} + +fn compile_var_rec(mut ident: Pairs) -> Option { + let idx = ident.next(); + if let Some(idx) = idx { + let deref: Accessor = match idx.as_rule() { + Rule::sub_ident_any => Box::new(|v, _| match v { + Value::List(l) => VarRes::Any(l.iter().map(VarRes::Exactly).collect()), + Value::Object(o) => { + VarRes::Any(o.0.iter().map(|(_, a)| VarRes::Exactly(a)).collect()) + } + _ => VarRes::Exactly(&STATIC_NULL), + }), + Rule::sub_ident_all => Box::new(|v, _| match v { + Value::List(l) => VarRes::All(l.iter().map(VarRes::Exactly).collect()), + Value::Object(o) => { + VarRes::All(o.0.iter().map(|(_, a)| VarRes::Exactly(a)).collect()) + } + _ => VarRes::Exactly(&STATIC_NULL), + }), + Rule::sub_ident_fn => { + let idx = idx.into_inner().next().unwrap(); + match idx.as_rule() { + Rule::list_access_function_first => { + let mut pred_iter = idx.into_inner(); + let item_var = pred_iter.next().unwrap().as_str().to_owned(); + let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); + Box::new(move |v, cfgs| match v { + Value::List(l) => VarRes::Exactly( + l.iter() + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .next() + .unwrap_or(&STATIC_NULL), + ), + Value::Object(o) => VarRes::Exactly( + o.0.iter() + .map(|(_, item)| item) + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .next() + .unwrap_or(&STATIC_NULL), + ), + _ => VarRes::Exactly(&STATIC_NULL), + }) + } + Rule::list_access_function_last => { + let mut pred_iter = idx.into_inner(); + let item_var = pred_iter.next().unwrap().as_str().to_owned(); + let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); + Box::new(move |v, cfgs| match v { + Value::List(l) => VarRes::Exactly( + l.iter() + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .next_back() + .unwrap_or(&STATIC_NULL), + ), + Value::Object(o) => VarRes::Exactly( + o.0.iter() + .map(|(_, item)| item) + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .next_back() + .unwrap_or(&STATIC_NULL), + ), + _ => VarRes::Exactly(&STATIC_NULL), + }) + } + Rule::list_access_function_any => { + let mut pred_iter = idx.into_inner(); + let item_var = pred_iter.next().unwrap().as_str().to_owned(); + let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); + Box::new(move |v, cfgs| match v { + Value::List(l) => VarRes::Any( + l.iter() + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .map(VarRes::Exactly) + .collect(), + ), + Value::Object(o) => VarRes::Any( + o.0.iter() + .map(|(_, item)| item) + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .map(VarRes::Exactly) + .collect(), + ), + _ => VarRes::Exactly(&STATIC_NULL), + }) + } + Rule::list_access_function_all => { + let mut pred_iter = idx.into_inner(); + let item_var = pred_iter.next().unwrap().as_str().to_owned(); + let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); + Box::new(move |v, cfgs| match v { + Value::List(l) => VarRes::All( + l.iter() + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .map(VarRes::Exactly) + .collect(), + ), + Value::Object(o) => VarRes::All( + o.0.iter() + .map(|(_, item)| item) + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .map(VarRes::Exactly) + .collect(), + ), + _ => VarRes::Exactly(&STATIC_NULL), + }) + } + _ => unreachable!(), + } + } + Rule::sub_ident_regular => { + let idx = idx.into_inner().next().unwrap(); + match idx.as_rule() { + Rule::sub_ident_regular_base => { + let idx = idx.as_str().to_owned(); + Box::new(move |v, _| match v { + Value::Object(o) => { + VarRes::Exactly(o.0.get(&idx).unwrap_or(&STATIC_NULL)) + } + _ => VarRes::Exactly(&STATIC_NULL), + }) + } + Rule::sub_ident_regular_expr => { + let idx = compile_str_expr(idx.into_inner().next().unwrap().into_inner()); + Box::new(move |v, dep_cfg| match v { + Value::Object(o) => idx(&Config::default(), dep_cfg).map(|idx| { + idx.and_then(|idx| o.0.get(&idx)).unwrap_or(&STATIC_NULL) + }), + _ => VarRes::Exactly(&STATIC_NULL), + }) + } + _ => unreachable!(), + } + } + Rule::sub_ident_index => { + let idx = idx.into_inner().next().unwrap(); + match idx.as_rule() { + Rule::sub_ident_index_base => { + let idx: usize = idx.as_str().parse().unwrap(); + Box::new(move |v, _| match v { + Value::List(l) => VarRes::Exactly(l.get(idx).unwrap_or(&STATIC_NULL)), + _ => VarRes::Exactly(&STATIC_NULL), + }) + } + Rule::sub_ident_index_expr => { + let idx = compile_num_expr(idx.into_inner().next().unwrap().into_inner()); + Box::new(move |v, dep_cfg| match v { + Value::List(l) => idx(&Config::default(), dep_cfg) + .map(|idx| l.get(idx as usize).unwrap_or(&STATIC_NULL)), + _ => VarRes::Exactly(&STATIC_NULL), + }) + } + _ => unreachable!(), + } + } + _ => unreachable!(), + }; + Some(if let Some(rest) = compile_var_rec(ident) { + Box::new(move |v, cfgs| deref(v, cfgs).and_then(|v| rest(v, cfgs))) + } else { + deref + }) + } else { + None + } +} + +fn compile_var(mut var: Pairs) -> CompiledExpr> { + let mut first_seg = var.next().unwrap(); + let app_id = if first_seg.as_rule() == Rule::app_id { + let app_id = first_seg.into_inner().next().unwrap().as_str().to_owned(); + first_seg = var.next().unwrap(); + Some(app_id) + } else { + None + }; + let first_seg_string = first_seg.as_str().to_owned(); + let accessor = compile_var_rec(var); + Box::new(move |cfg, cfgs| { + let mut cfg: &Config = cfg; + if let Some(ref app_id) = app_id { + cfg = if let Some(cfg) = cfgs.get(&app_id.as_str()) { + cfg + } else { + return VarRes::Exactly(Value::Null); + }; + } + let val = cfg.0.get(&first_seg_string).unwrap_or(&STATIC_NULL); + if let Some(accessor) = &accessor { + accessor(val, cfgs).map(|v| v.clone()) + } else { + VarRes::Exactly(val.clone()) + } + }) +} + +fn compile_var_mut_rec(mut ident: Pairs) -> Result, failure::Error> { + let idx = ident.next(); + Ok(if let Some(idx) = idx { + let deref: AccessorMut = match idx.as_rule() { + Rule::sub_ident_fn => { + let idx = idx.into_inner().next().unwrap(); + match idx.as_rule() { + Rule::list_access_function_first => { + let mut pred_iter = idx.into_inner(); + let item_var = pred_iter.next().unwrap().as_str().to_owned(); + let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); + Box::new(move |v, cfgs| match v { + Value::List(l) => l + .iter_mut() + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .next(), + Value::Object(o) => { + o.0.iter_mut() + .map(|(_, item)| item) + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .next() + } + _ => None, + }) + } + Rule::list_access_function_last => { + let mut pred_iter = idx.into_inner(); + let item_var = pred_iter.next().unwrap().as_str().to_owned(); + let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); + Box::new(move |v, cfgs| match v { + Value::List(l) => l + .iter_mut() + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .next_back(), + Value::Object(o) => { + o.0.iter_mut() + .map(|(_, item)| item) + .filter(|item| { + let mut cfg = Config::default(); + cfg.0.insert(item_var.clone(), (*item).clone()); + predicate(&cfg, cfgs) + }) + .next_back() + } + _ => None, + }) + } + Rule::list_access_function_any | Rule::list_access_function_all => { + failure::bail!("Any and All are immutable") + } + _ => unreachable!(), + } + } + Rule::sub_ident_regular => { + let idx = idx.into_inner().next().unwrap(); + match idx.as_rule() { + Rule::sub_ident_regular_base => { + let idx = idx.as_str().to_owned(); + Box::new(move |v, _| match v { + Value::Object(ref mut o) => { + if o.0.contains_key(&idx) { + o.0.get_mut(&idx) + } else { + o.0.insert(idx.clone(), Value::Null); + o.0.get_mut(&idx) + } + } + _ => None, + }) + } + Rule::sub_ident_regular_expr => { + let idx = compile_str_expr(idx.into_inner().next().unwrap().into_inner()); + Box::new( + move |v, dep_cfg| match (v, idx(&Config::default(), dep_cfg)) { + (Value::Object(ref mut o), VarRes::Exactly(Some(ref idx))) => { + if o.0.contains_key(idx) { + o.0.get_mut(idx) + } else { + o.0.insert(idx.clone(), Value::Null); + o.0.get_mut(idx) + } + } + _ => None, + }, + ) + } + _ => unreachable!(), + } + } + Rule::sub_ident_index => { + let idx = idx.into_inner().next().unwrap(); + match idx.as_rule() { + Rule::sub_ident_index_base => { + let idx: usize = idx.as_str().parse().unwrap(); + Box::new(move |v, _| match v { + Value::List(l) => { + if l.len() > idx { + l.get_mut(idx) + } else if idx == l.len() { + l.push(Value::Null); + l.get_mut(idx) + } else { + None + } + } + _ => None, + }) + } + Rule::sub_ident_index_expr => { + let idx = compile_num_expr(idx.into_inner().next().unwrap().into_inner()); + Box::new( + move |v, dep_cfg| match (v, idx(&Config::default(), dep_cfg)) { + (Value::List(l), VarRes::Exactly(idx)) => { + let idx = idx as usize; + if l.len() > idx { + l.get_mut(idx) + } else if idx == l.len() { + l.push(Value::Null); + l.get_mut(idx) + } else { + None + } + } + _ => None, + }, + ) + } + _ => unreachable!(), + } + } + _ => failure::bail!("invalid token: {:?}", idx.as_rule()), + }; + Some(if let Some(rest) = compile_var_mut_rec(ident)? { + Box::new(move |v, cfgs| deref(v, cfgs).and_then(|v| rest(v, cfgs))) + } else { + deref + }) + } else { + None + }) +} + +fn compile_var_mut(mut var: Pairs) -> Result { + let first_seg = var.next().unwrap(); + if first_seg.as_rule() == Rule::app_id { + failure::bail!("Can only assign to relative path"); + } + let first_seg_string = first_seg.as_str().to_owned(); + let accessor_mut = compile_var_mut_rec(var)?; + Ok(Box::new(move |cfg, cfgs| { + let var = if cfg.0.contains_key(&first_seg_string) { + cfg.0.get_mut(&first_seg_string).unwrap() + } else { + cfg.0.insert(first_seg_string.clone(), Value::Null); + cfg.0.get_mut(&first_seg_string).unwrap() + }; + if let Some(accessor_mut) = &accessor_mut { + accessor_mut(var, cfgs) + } else { + Some(var) + } + })) +} + +fn compile_bool_var(var: Pairs) -> CompiledRule { + let var = compile_var(var); + Box::new(move |cfg, cfgs| { + var(cfg, cfgs) + .map(|a| match a { + Value::Bool(false) | Value::Null => false, + _ => true, + }) + .resolve() + }) +} + +fn compile_num_var(var: Pairs) -> CompiledExpr> { + let var = compile_var(var); + Box::new(move |cfg, cfgs| { + var(cfg, cfgs).map(|a| match a { + Value::Number(n) => n, + Value::String(s) => match s.parse() { + Ok(n) => n, + Err(_) => std::f64::NAN, + }, + Value::Bool(b) => { + if b { + 1.0 + } else { + 0.0 + } + } + _ => std::f64::NAN, + }) + }) +} + +fn compile_num(num_str: &str) -> CompiledExpr> { + let num = VarRes::Exactly(num_str.parse().unwrap()); + Box::new(move |_, _| num.clone()) +} + +fn compile_num_expr(pairs: Pairs) -> CompiledExpr> { + NUM_PREC_CLIMBER.climb( + pairs, + |pair| match pair.as_rule() { + Rule::num_var => compile_num_var(pair.into_inner()), + Rule::num => compile_num(pair.as_str()), + Rule::num_expr => compile_num_expr(pair.into_inner()), + _ => unreachable!(), + }, + |lhs, op, rhs| match op.as_rule() { + Rule::add => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs).and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs + rhs)) + }), + Rule::sub => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs).and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs - rhs)) + }), + Rule::mul => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs).and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs * rhs)) + }), + Rule::div => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs).and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs / rhs)) + }), + Rule::pow => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs).and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs.powf(rhs))) + }), + _ => unreachable!(), + }, + ) +} + +fn compile_num_cmp_expr(mut pairs: Pairs) -> CompiledRule { + let lhs = compile_num_expr(pairs.next().unwrap().into_inner()); + let op = pairs.next().unwrap(); + let rhs = compile_num_expr(pairs.next().unwrap().into_inner()); + match op.as_rule() { + Rule::lt => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs < rhs)) + .resolve() + }), + Rule::lte => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs <= rhs)) + .resolve() + }), + Rule::eq => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs == rhs)) + .resolve() + }), + Rule::neq => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs != rhs)) + .resolve() + }), + Rule::gt => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs > rhs)) + .resolve() + }), + Rule::gte => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs >= rhs)) + .resolve() + }), + _ => unreachable!(), + } +} + +fn compile_str_var(var: Pairs) -> CompiledExpr>> { + let var = compile_var(var); + Box::new(move |cfg, cfgs| { + var(cfg, cfgs).map(|a| match a { + Value::String(s) => Some(s), + Value::Number(n) => Some(format!("{}", n)), + Value::Bool(b) => Some(format!("{}", b)), + _ => None, + }) + }) +} + +fn compile_str(str_str: &str) -> CompiledExpr>> { + let str_str = &str_str[1..str_str.len() - 1]; + let mut out = String::with_capacity(str_str.len()); + let mut escape = false; + for c in str_str.chars() { + match c { + '\\' => { + if escape { + out.push('\\'); + } else { + escape = true; + } + } + 'n' if escape => out.push('\n'), + 'r' if escape => out.push('\r'), + 't' if escape => out.push('\t'), + '0' if escape => out.push('\0'), + '"' if escape => out.push('"'), + '\'' if escape => out.push('\''), + _ => { + if escape { + out.push('\\') + } + out.push(c) + } + } + } + let res = VarRes::Exactly(Some(out)); + Box::new(move |_, _| res.clone()) +} + +fn compile_str_expr(pairs: Pairs) -> CompiledExpr>> { + STR_PREC_CLIMBER.climb( + pairs, + |pair| match pair.as_rule() { + Rule::str_var => compile_str_var(pair.into_inner()), + Rule::str => compile_str(pair.as_str()), + Rule::str_expr => compile_str_expr(pair.into_inner()), + _ => unreachable!(), + }, + |lhs, op, rhs| match op.as_rule() { + Rule::add => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs).and_then(|lhs| { + rhs(cfg, cfgs).map(|rhs| { + let lhs = lhs.clone()?; + let rhs = rhs?; + Some(lhs + &rhs) + }) + }) + }), + _ => unreachable!(), + }, + ) +} + +fn compile_str_cmp_expr(mut pairs: Pairs) -> CompiledRule { + let lhs = compile_str_expr(pairs.next().unwrap().into_inner()); + let op = pairs.next().unwrap(); + let rhs = compile_str_expr(pairs.next().unwrap().into_inner()); + match op.as_rule() { + Rule::lt => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| { + rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { + (Some(lhs), Some(rhs)) => rhs.contains(lhs) && lhs.len() < rhs.len(), + _ => false, + }) + }) + .resolve() + }), + Rule::lte => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| { + rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { + (Some(lhs), Some(rhs)) => rhs.contains(lhs), + _ => false, + }) + }) + .resolve() + }), + Rule::eq => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| { + rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { + (Some(lhs), Some(rhs)) => lhs == rhs, + (None, None) => true, + _ => false, + }) + }) + .resolve() + }), + Rule::neq => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| { + rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { + (Some(lhs), Some(rhs)) => lhs != rhs, + (None, None) => false, + _ => true, + }) + }) + .resolve() + }), + Rule::gt => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| { + rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { + (Some(lhs), Some(rhs)) => lhs.contains(rhs) && lhs.len() > rhs.len(), + _ => true, + }) + }) + .resolve() + }), + Rule::gte => Box::new(move |cfg, cfgs| { + lhs(cfg, cfgs) + .and_then(|lhs| { + rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { + (Some(lhs), Some(rhs)) => lhs.contains(rhs), + _ => true, + }) + }) + .resolve() + }), + _ => unreachable!(), + } +} + +fn compile_inv_bool_expr(mut pairs: Pairs) -> CompiledRule { + let expr = compile_bool_expr(pairs.next().unwrap().into_inner()); + Box::new(move |cfg, cfgs| !expr(cfg, cfgs)) +} + +fn compile_bool_expr(pairs: Pairs) -> CompiledRule { + BOOL_PREC_CLIMBER.climb( + pairs, + |pair| match pair.as_rule() { + Rule::bool_var => compile_bool_var(pair.into_inner()), + Rule::bool_expr => compile_bool_expr(pair.into_inner()), + Rule::inv_bool_expr => compile_inv_bool_expr(pair.into_inner()), + Rule::num_cmp_expr => compile_num_cmp_expr(pair.into_inner()), + Rule::str_cmp_expr => compile_str_cmp_expr(pair.into_inner()), + _ => unreachable!(), + }, + |lhs, op, rhs| -> CompiledRule { + match op.as_rule() { + Rule::and => Box::new(move |cfg, cfgs| lhs(cfg, cfgs) && rhs(cfg, cfgs)), + Rule::or => Box::new(move |cfg, cfgs| lhs(cfg, cfgs) || rhs(cfg, cfgs)), + Rule::xor => Box::new(move |cfg, cfgs| lhs(cfg, cfgs) ^ rhs(cfg, cfgs)), + _ => unreachable!(), + } + }, + ) +} + +fn compile_value_expr(mut pairs: Pairs) -> CompiledExpr> { + let expr = pairs.next().unwrap(); + match expr.as_rule() { + Rule::any_var => compile_var(expr.into_inner()), + Rule::str_expr => { + let expr = compile_str_expr(expr.into_inner()); + Box::new(move |cfg, cfgs| { + expr(cfg, cfgs).map(|s| s.map(Value::String).unwrap_or(Value::Null)) + }) + } + Rule::num_expr => { + let expr = compile_num_expr(expr.into_inner()); + Box::new(move |cfg, cfgs| expr(cfg, cfgs).map(Value::Number)) + } + Rule::bool_expr => { + let expr = compile_bool_expr(expr.into_inner()); + Box::new(move |cfg, cfgs| VarRes::Exactly(expr(cfg, cfgs)).map(Value::Bool)) + } + _ => unreachable!(), + } +} + +fn compile_del_action(mut pairs: Pairs) -> Result { + let list_mut = compile_var_mut(pairs.next().unwrap().into_inner())?; + let var = pairs.next().unwrap().as_str().to_owned(); + let predicate = compile_bool_expr(pairs.next().unwrap().into_inner()); + Ok(Box::new(move |cfg, cfgs| match (&list_mut)(cfg, cfgs) { + Some(Value::List(ref mut l)) => { + *l = std::mem::take(l) + .into_iter() + .filter(|item| { + let mut obj = Config::default(); + obj.0.insert(var.clone(), item.clone()); + !predicate(&obj, cfgs) + }) + .collect(); + } + Some(Value::Object(Config(ref mut o))) => { + *o = std::mem::take(o) + .into_iter() + .filter(|(_, item)| { + let mut obj = Config::default(); + obj.0.insert(var.clone(), item.clone()); + !predicate(&obj, cfgs) + }) + .collect(); + } + _ => return, + })) +} + +fn compile_push_action(mut pairs: Pairs, value: Value) -> Result { + let list_mut = compile_var_mut(pairs.next().unwrap().into_inner())?; + Ok(Box::new(move |cfg, cfgs| { + let vec = match (&list_mut)(cfg, cfgs) { + Some(Value::List(ref mut a)) => a, + _ => return, + }; + vec.push(value.clone()) + })) +} + +fn compile_set_action(var: &str, to: &SetVariant) -> Result { + let mut var = RuleParser::parse(Rule::reference, var)?; + let get_mut = compile_var_mut(var.next().unwrap().into_inner())?; + Ok(match to { + SetVariant::To(expr) => { + let expr = compile_expr(&expr)?; + Box::new(move |cfg, cfgs| { + let val = expr(cfg, cfgs); + if let Some(var) = get_mut(cfg, cfgs) { + *var = val; + } + }) + } + SetVariant::ToValue(val) => { + let val = val.clone(); + Box::new(move |cfg, cfgs| { + if let Some(var) = get_mut(cfg, cfgs) { + *var = val.clone() + } + }) + } + SetVariant::ToEntropy(entropy) => { + let entropy = entropy.clone(); + Box::new(move |cfg, cfgs| { + if let Some(var) = get_mut(cfg, cfgs) { + *var = Value::String(entropy.gen(&mut rand::rngs::StdRng::from_entropy())); + } + }) + } + }) +} + +pub fn validate_key(key: &str) -> Result<(), pest::error::Error> { + RuleParser::parse(Rule::obj_key, key)?; + Ok(()) +} + +pub fn parse_and) -> T>( + rule: &str, + f: F, +) -> Result> { + let mut parsed = RuleParser::parse(Rule::rule, rule)?; + let pairs = parsed.next().unwrap().into_inner(); + Ok(f(pairs)) +} + +pub fn compile(rule: &str) -> Result { + parse_and(rule, compile_bool_expr).map_err(From::from) +} + +pub fn compile_expr(expr: &str) -> Result, failure::Error> { + let compiled = compile_value_expr(RuleParser::parse(Rule::value, expr)?); + Ok(Box::new(move |cfg, cfgs| match compiled(cfg, cfgs) { + VarRes::Exactly(v) => v, + _ => Value::Null, + })) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_compile_str() { + assert_eq!( + compile_str("\"foo\"")(&Default::default(), &Default::default()), + VarRes::Exactly(Some("foo".to_owned())) + ); + } + + #[test] + fn test_access_expr() { + let mut cfg = Config::default(); + let mut cfgs = LinearMap::new(); + let mut foo = Config::default(); + foo.0.insert("bar!\"".to_owned(), Value::Number(3.0)); + cfg.0.insert( + "foo".to_owned(), + Value::List(vec![Value::Null, Value::Object(foo), Value::Number(3.0)]), + ); + cfgs.insert("my-app", Cow::Borrowed(&cfg)); + assert!((compile("#[my-app].foo.1.[\"ba\" + \"r!\\\"\"] = 3") + .map_err(|e| eprintln!("{}", e)) + .expect("compile failed"))(&cfg, &cfgs)); + assert!((compile("#[my-app].foo.[0 + 1].[\"bar!\\\"\"] = 3") + .map_err(|e| eprintln!("{}", e)) + .expect("compile failed"))(&cfg, &cfgs)); + } + + #[test] + fn test_any_all() { + let mut cfg = Config::default(); + let mut cfgs = LinearMap::new(); + let mut foo = Config::default(); + foo.0.insert("bar".to_owned(), Value::Number(3.0)); + cfg.0.insert( + "foo".to_owned(), + Value::List(vec![Value::Null, Value::Object(foo), Value::Number(3.0)]), + ); + cfgs.insert("my-app", Cow::Borrowed(&cfg)); + assert!((compile("#[my-app].foo.*.bar = 3") + .map_err(|e| eprintln!("{}", e)) + .expect("compile failed"))(&cfg, &cfgs)); + assert!(!(compile("#[my-app].foo.&.bar = 3") + .map_err(|e| eprintln!("{}", e)) + .expect("compile failed"))(&cfg, &cfgs)); + } + + #[test] + fn test_first_last() { + let mut cfg = Config::default(); + let mut cfgs = LinearMap::new(); + let mut foo = Config::default(); + foo.0.insert("bar".to_owned(), Value::Number(3.0)); + foo.0.insert("baz".to_owned(), Value::Number(4.0)); + let mut qux = Config::default(); + qux.0.insert("bar".to_owned(), Value::Number(7.0)); + qux.0.insert("baz".to_owned(), Value::Number(4.0)); + cfg.0.insert( + "foo".to_owned(), + Value::List(vec![ + Value::Null, + Value::Object(foo), + Value::Object(qux), + Value::Number(3.0), + ]), + ); + cfgs.insert("my-app", Cow::Borrowed(&cfg)); + assert!((compile("#foo.[first(item => #item.baz = 4)].bar = 3") + .map_err(|e| eprintln!("{}", e)) + .expect("compile failed"))(&cfg, &cfgs)); + assert!((compile("#foo.[last(item => #item.baz = 4)].bar = 7") + .map_err(|e| eprintln!("{}", e)) + .expect("compile failed"))(&cfg, &cfgs)); + } + + #[test] + fn test_app_id() { + let mut dependent_cfg = Config::default(); + let mut dependency_cfg = Config::default(); + let mut cfgs = LinearMap::new(); + dependent_cfg + .0 + .insert("foo".to_owned(), Value::String("bar".to_owned())); + dependency_cfg + .0 + .insert("foo".to_owned(), Value::String("bar!".to_owned())); + cfgs.insert("my-dependent", Cow::Borrowed(&dependent_cfg)); + cfgs.insert("my-dependency", Cow::Borrowed(&dependency_cfg)); + assert!((compile("'foo = '[my-dependent].foo + \"!\"") + .map_err(|e| eprintln!("{}", e)) + .expect("compile failed"))( + &dependency_cfg, &cfgs + )) + } +} diff --git a/appmgr/src/config/spec.rs b/appmgr/src/config/spec.rs new file mode 100644 index 000000000..b5ea6da5b --- /dev/null +++ b/appmgr/src/config/spec.rs @@ -0,0 +1,1874 @@ +use std::borrow::{Borrow, Cow}; +use std::fmt; +use std::fmt::Debug; +use std::ops::RangeBounds; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use itertools::Itertools; +use linear_map::{set::LinearSet, LinearMap}; +use rand::{CryptoRng, Rng}; +use regex::Regex; + +use super::util::{self, CharSet, NumRange, UniqueBy, STATIC_NULL}; +use super::value::{Config, Value}; +use super::{MatchError, NoMatchWithPath, TimeoutError}; + +use crate::config::ConfigurationError; +use crate::manifest::ManifestLatest; +use crate::util::PersistencePath; + +// Config Value Specifications +#[async_trait] +pub trait ValueSpec { + // This function defines whether the value supplied in the argument is + // consistent with the spec in &self + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath>; + // This function checks whether the value spec is consistent with itself, + // since not all invariants can be checked by the type + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath>; + // update is to fill in values for environment pointers recursively + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError>; + // requires returns whether the app id is the target of a pointer within it + fn requires(&self, id: &str, value: &Value) -> bool; + // defines if 2 values of this type are equal for the purpose of uniqueness + fn eq(&self, lhs: &Value, rhs: &Value) -> bool; +} + +// Config Value Default Generation +// +// This behavior is defined by two independent traits as well as a third that +// represents a conjunction of those two traits: +// +// DefaultableWith - defines an associated type describing the information it +// needs to be able to generate a default value, as well as a function for +// extracting relevant pieces of that information and using it to actually +// generate the default value +// +// HasDefaultSpec - only purpose is to summon the default spec for the type +// +// Defaultable - this is a redundant trait that may replace 'DefaultableWith' +// and 'HasDefaultSpec'. +pub trait DefaultableWith { + type DefaultSpec: Sync; + type Error: failure::Fail; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + rng: &mut R, + timeout: &Option, + ) -> Result; +} +pub trait HasDefaultSpec: DefaultableWith { + fn default_spec(&self) -> &Self::DefaultSpec; +} + +pub trait Defaultable { + type Error; + + fn gen( + &self, + rng: &mut R, + timeout: &Option, + ) -> Result; +} +impl Defaultable for T +where + T: HasDefaultSpec + DefaultableWith + Sync, + E: failure::Fail, +{ + type Error = E; + + fn gen( + &self, + rng: &mut R, + timeout: &Option, + ) -> Result { + self.gen_with(self.default_spec().borrow(), rng, timeout) + } +} + +// WithDefault - trivial wrapper that pairs a 'DefaultableWith' type with a +// default spec +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct WithDefault { + #[serde(flatten)] + pub inner: T, + pub default: T::DefaultSpec, +} +impl DefaultableWith for WithDefault +where + T: DefaultableWith + Sync + Send, + T::DefaultSpec: Send, +{ + type DefaultSpec = T::DefaultSpec; + type Error = T::Error; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + rng: &mut R, + timeout: &Option, + ) -> Result { + self.inner.gen_with(spec, rng, timeout) + } +} +impl HasDefaultSpec for WithDefault +where + T: DefaultableWith + Sync + Send, + T::DefaultSpec: Send, +{ + fn default_spec(&self) -> &Self::DefaultSpec { + &self.default + } +} +#[async_trait] +impl ValueSpec for WithDefault +where + T: ValueSpec + DefaultableWith + Send + Sync, + Self: Send + Sync, +{ + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + self.inner.matches(value) + } + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + self.inner.validate(manifest) + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + self.inner.update(value).await + } + fn requires(&self, id: &str, value: &Value) -> bool { + self.inner.requires(id, value) + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + self.inner.eq(lhs, rhs) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct WithNullable { + #[serde(flatten)] + pub inner: T, + pub nullable: bool, +} +#[async_trait] +impl ValueSpec for WithNullable +where + T: ValueSpec + Send + Sync, + Self: Send + Sync, +{ + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + match (self.nullable, value) { + (true, &Value::Null) => Ok(()), + _ => self.inner.matches(value), + } + } + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + self.inner.validate(manifest) + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + self.inner.update(value).await + } + fn requires(&self, id: &str, value: &Value) -> bool { + self.inner.requires(id, value) + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + self.inner.eq(lhs, rhs) + } +} + +impl DefaultableWith for WithNullable +where + T: DefaultableWith + Sync + Send, +{ + type DefaultSpec = T::DefaultSpec; + type Error = T::Error; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + rng: &mut R, + timeout: &Option, + ) -> Result { + self.inner.gen_with(spec, rng, timeout) + } +} + +impl Defaultable for WithNullable +where + T: Defaultable + Sync + Send, +{ + type Error = T::Error; + + fn gen( + &self, + rng: &mut R, + timeout: &Option, + ) -> Result { + self.inner.gen(rng, timeout) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WithDescription { + #[serde(flatten)] + pub inner: T, + pub description: Option, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub change_warning: Option, +} +#[async_trait] +impl ValueSpec for WithDescription +where + T: ValueSpec + Sync + Send, + Self: Sync + Send, +{ + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + self.inner.matches(value) + } + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + self.inner.validate(manifest) + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + self.inner.update(value).await + } + fn requires(&self, id: &str, value: &Value) -> bool { + self.inner.requires(id, value) + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + self.inner.eq(lhs, rhs) + } +} + +impl DefaultableWith for WithDescription +where + T: DefaultableWith + Sync + Send, +{ + type DefaultSpec = T::DefaultSpec; + type Error = T::Error; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + rng: &mut R, + timeout: &Option, + ) -> Result { + self.inner.gen_with(spec, rng, timeout) + } +} + +impl Defaultable for WithDescription +where + T: Defaultable + Sync + Send, +{ + type Error = T::Error; + + fn gen( + &self, + rng: &mut R, + timeout: &Option, + ) -> Result { + self.inner.gen(rng, timeout) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "type")] +pub enum ValueSpecAny { + Boolean(WithDescription>), + Enum(WithDescription>), + List(ValueSpecList), + Number(WithDescription>>), + Object(WithDescription>), + String(WithDescription>>), + Union(WithDescription>), + Pointer(WithDescription), +} +impl ValueSpecAny { + pub fn name<'a>(&'a self) -> &'a str { + match self { + ValueSpecAny::Boolean(b) => b.name.as_str(), + ValueSpecAny::Enum(e) => e.name.as_str(), + ValueSpecAny::List(l) => match l { + ValueSpecList::Enum(e) => e.name.as_str(), + ValueSpecList::Number(n) => n.name.as_str(), + ValueSpecList::Object(o) => o.name.as_str(), + ValueSpecList::String(s) => s.name.as_str(), + ValueSpecList::Union(u) => u.name.as_str(), + }, + ValueSpecAny::Number(n) => n.name.as_str(), + ValueSpecAny::Object(o) => o.name.as_str(), + ValueSpecAny::Pointer(p) => p.name.as_str(), + ValueSpecAny::String(s) => s.name.as_str(), + ValueSpecAny::Union(u) => u.name.as_str(), + } + } +} +#[async_trait] +impl ValueSpec for ValueSpecAny { + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + match self { + ValueSpecAny::Boolean(a) => a.matches(value), + ValueSpecAny::Enum(a) => a.matches(value), + ValueSpecAny::List(a) => a.matches(value), + ValueSpecAny::Number(a) => a.matches(value), + ValueSpecAny::Object(a) => a.matches(value), + ValueSpecAny::String(a) => a.matches(value), + ValueSpecAny::Union(a) => a.matches(value), + ValueSpecAny::Pointer(a) => a.matches(value), + } + } + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + match self { + ValueSpecAny::Boolean(a) => a.validate(manifest), + ValueSpecAny::Enum(a) => a.validate(manifest), + ValueSpecAny::List(a) => a.validate(manifest), + ValueSpecAny::Number(a) => a.validate(manifest), + ValueSpecAny::Object(a) => a.validate(manifest), + ValueSpecAny::String(a) => a.validate(manifest), + ValueSpecAny::Union(a) => a.validate(manifest), + ValueSpecAny::Pointer(a) => a.validate(manifest), + } + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + match self { + ValueSpecAny::Boolean(a) => a.update(value).await, + ValueSpecAny::Enum(a) => a.update(value).await, + ValueSpecAny::List(a) => a.update(value).await, + ValueSpecAny::Number(a) => a.update(value).await, + ValueSpecAny::Object(a) => a.update(value).await, + ValueSpecAny::String(a) => a.update(value).await, + ValueSpecAny::Union(a) => a.update(value).await, + ValueSpecAny::Pointer(a) => a.update(value).await, + } + } + fn requires(&self, id: &str, value: &Value) -> bool { + match self { + ValueSpecAny::Boolean(a) => a.requires(id, value), + ValueSpecAny::Enum(a) => a.requires(id, value), + ValueSpecAny::List(a) => a.requires(id, value), + ValueSpecAny::Number(a) => a.requires(id, value), + ValueSpecAny::Object(a) => a.requires(id, value), + ValueSpecAny::String(a) => a.requires(id, value), + ValueSpecAny::Union(a) => a.requires(id, value), + ValueSpecAny::Pointer(a) => a.requires(id, value), + } + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + match self { + ValueSpecAny::Boolean(a) => a.eq(lhs, rhs), + ValueSpecAny::Enum(a) => a.eq(lhs, rhs), + ValueSpecAny::List(a) => a.eq(lhs, rhs), + ValueSpecAny::Number(a) => a.eq(lhs, rhs), + ValueSpecAny::Object(a) => a.eq(lhs, rhs), + ValueSpecAny::String(a) => a.eq(lhs, rhs), + ValueSpecAny::Union(a) => a.eq(lhs, rhs), + ValueSpecAny::Pointer(a) => a.eq(lhs, rhs), + } + } +} +impl Defaultable for ValueSpecAny { + type Error = ConfigurationError; + + fn gen( + &self, + rng: &mut R, + timeout: &Option, + ) -> Result { + match self { + ValueSpecAny::Boolean(a) => a.gen(rng, timeout).map_err(crate::util::absurd), + ValueSpecAny::Enum(a) => a.gen(rng, timeout).map_err(crate::util::absurd), + ValueSpecAny::List(a) => a.gen(rng, timeout), + ValueSpecAny::Number(a) => a.gen(rng, timeout).map_err(crate::util::absurd), + ValueSpecAny::Object(a) => a.gen(rng, timeout), + ValueSpecAny::String(a) => a.gen(rng, timeout).map_err(ConfigurationError::from), + ValueSpecAny::Union(a) => a.gen(rng, timeout), + ValueSpecAny::Pointer(a) => a.gen(rng, timeout), + } + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct ValueSpecBoolean {} +#[async_trait] +impl ValueSpec for ValueSpecBoolean { + fn matches(&self, val: &Value) -> Result<(), NoMatchWithPath> { + match val { + Value::Bool(_) => Ok(()), + Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), + a => Err(NoMatchWithPath::new(MatchError::InvalidType( + "boolean", + a.type_of(), + ))), + } + } + fn validate(&self, _manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + Ok(()) + } + async fn update(&self, _value: &mut Value) -> Result<(), ConfigurationError> { + Ok(()) + } + fn requires(&self, _id: &str, _value: &Value) -> bool { + false + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + match (lhs, rhs) { + (Value::Bool(lhs), Value::Bool(rhs)) => lhs == rhs, + _ => false, + } + } +} +impl DefaultableWith for ValueSpecBoolean { + type DefaultSpec = bool; + type Error = crate::util::Never; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + _rng: &mut R, + _timeout: &Option, + ) -> Result { + Ok(Value::Bool(*spec)) + } +} + +#[derive(Clone, Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ValueSpecEnum { + pub values: LinearSet, + pub value_names: LinearMap, +} +impl<'de> serde::de::Deserialize<'de> for ValueSpecEnum { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct _ValueSpecEnum { + pub values: LinearSet, + #[serde(default)] + pub value_names: LinearMap, + } + + let mut r#enum = _ValueSpecEnum::deserialize(deserializer)?; + for name in &r#enum.values { + if !r#enum.value_names.contains_key(name) { + r#enum.value_names.insert(name.clone(), name.clone()); + } + } + Ok(ValueSpecEnum { + values: r#enum.values, + value_names: r#enum.value_names, + }) + } +} +#[async_trait] +impl ValueSpec for ValueSpecEnum { + fn matches(&self, val: &Value) -> Result<(), NoMatchWithPath> { + match val { + Value::String(b) => { + if self.values.contains(b) { + Ok(()) + } else { + Err(NoMatchWithPath::new(MatchError::Enum( + b.clone(), + self.values.clone(), + ))) + } + } + Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), + a => Err(NoMatchWithPath::new(MatchError::InvalidType( + "string", + a.type_of(), + ))), + } + } + fn validate(&self, _manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + Ok(()) + } + async fn update(&self, _value: &mut Value) -> Result<(), ConfigurationError> { + Ok(()) + } + fn requires(&self, _id: &str, _value: &Value) -> bool { + false + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + match (lhs, rhs) { + (Value::String(lhs), Value::String(rhs)) => lhs == rhs, + _ => false, + } + } +} +impl DefaultableWith for ValueSpecEnum { + type DefaultSpec = String; + type Error = crate::util::Never; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + _rng: &mut R, + _timeout: &Option, + ) -> Result { + Ok(Value::String(spec.clone())) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct ListSpec { + pub spec: T, + pub range: NumRange, +} +#[async_trait] +impl ValueSpec for ListSpec +where + T: ValueSpec + Sync + Send, + Self: Sync + Send, +{ + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + match value { + Value::List(l) => { + if !self.range.contains(&l.len()) { + Err(NoMatchWithPath { + path: Vec::new(), + error: MatchError::LengthMismatch(self.range.clone(), l.len()), + }) + } else { + l.iter() + .enumerate() + .map(|(i, v)| { + self.spec + .matches(v) + .map_err(|e| e.prepend(format!("{}", i)))?; + if l.iter() + .enumerate() + .any(|(i2, v2)| i != i2 && self.spec.eq(v, v2)) + { + Err(NoMatchWithPath::new(MatchError::ListUniquenessViolation) + .prepend(format!("{}", i))) + } else { + Ok(()) + } + }) + .collect() + } + } + Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), + a => Err(NoMatchWithPath::new(MatchError::InvalidType( + "list", + a.type_of(), + ))), + } + } + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + self.spec.validate(manifest) + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + if let Value::List(ref mut ls) = value { + for (i, val) in ls.into_iter().enumerate() { + match self.spec.update(val).await { + Err(ConfigurationError::NoMatch(e)) => { + Err(ConfigurationError::NoMatch(e.prepend(format!("{}", i)))) + } + a => a, + }?; + } + Ok(()) + } else { + Err(ConfigurationError::NoMatch(NoMatchWithPath::new( + MatchError::InvalidType("list", value.type_of()), + ))) + } + } + fn requires(&self, id: &str, value: &Value) -> bool { + if let Value::List(ref ls) = value { + ls.into_iter().any(|v| self.spec.requires(id, v)) + } else { + false + } + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + match (lhs, rhs) { + (Value::List(lhs), Value::List(rhs)) => { + lhs.iter().zip_longest(rhs.iter()).all(|zip| match zip { + itertools::EitherOrBoth::Both(lhs, rhs) => lhs == rhs, + _ => false, + }) + } + _ => false, + } + } +} + +impl DefaultableWith for ListSpec +where + T: DefaultableWith + Sync + Send, +{ + type DefaultSpec = Vec; + type Error = T::Error; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + rng: &mut R, + timeout: &Option, + ) -> Result { + let mut res = Vec::new(); + for spec_member in spec.iter() { + res.push(self.spec.gen_with(spec_member, rng, timeout)?); + } + Ok(Value::List(res)) + } +} + +unsafe impl Sync for ValueSpecObject {} // TODO: remove +unsafe impl Send for ValueSpecObject {} // TODO: remove +unsafe impl Sync for ValueSpecUnion {} // TODO: remove +unsafe impl Send for ValueSpecUnion {} // TODO: remove + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "subtype")] +pub enum ValueSpecList { + Enum(WithDescription>>), + Number(WithDescription>>), + Object(WithDescription>>), + String(WithDescription>>), + Union(WithDescription>>>), +} +#[async_trait] +impl ValueSpec for ValueSpecList { + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + match self { + ValueSpecList::Enum(a) => a.matches(value), + ValueSpecList::Number(a) => a.matches(value), + ValueSpecList::Object(a) => a.matches(value), + ValueSpecList::String(a) => a.matches(value), + ValueSpecList::Union(a) => a.matches(value), + } + } + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + match self { + ValueSpecList::Enum(a) => a.validate(manifest), + ValueSpecList::Number(a) => a.validate(manifest), + ValueSpecList::Object(a) => a.validate(manifest), + ValueSpecList::String(a) => a.validate(manifest), + ValueSpecList::Union(a) => a.validate(manifest), + } + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + match self { + ValueSpecList::Enum(a) => a.update(value).await, + ValueSpecList::Number(a) => a.update(value).await, + ValueSpecList::Object(a) => a.update(value).await, + ValueSpecList::String(a) => a.update(value).await, + ValueSpecList::Union(a) => a.update(value).await, + } + } + fn requires(&self, id: &str, value: &Value) -> bool { + match self { + ValueSpecList::Enum(a) => a.requires(id, value), + ValueSpecList::Number(a) => a.requires(id, value), + ValueSpecList::Object(a) => a.requires(id, value), + ValueSpecList::String(a) => a.requires(id, value), + ValueSpecList::Union(a) => a.requires(id, value), + } + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + match self { + ValueSpecList::Enum(a) => a.eq(lhs, rhs), + ValueSpecList::Number(a) => a.eq(lhs, rhs), + ValueSpecList::Object(a) => a.eq(lhs, rhs), + ValueSpecList::String(a) => a.eq(lhs, rhs), + ValueSpecList::Union(a) => a.eq(lhs, rhs), + } + } +} + +impl Defaultable for ValueSpecList { + type Error = ConfigurationError; + + fn gen( + &self, + rng: &mut R, + timeout: &Option, + ) -> Result { + match self { + ValueSpecList::Enum(a) => a.gen(rng, timeout).map_err(crate::util::absurd), + ValueSpecList::Number(a) => a.gen(rng, timeout).map_err(crate::util::absurd), + ValueSpecList::Object(a) => { + let mut ret = match a.gen(rng, timeout).unwrap() { + Value::List(l) => l, + a => { + return Err(ConfigurationError::NoMatch(NoMatchWithPath::new( + MatchError::InvalidType("list", a.type_of()), + ))) + } + }; + while !( + a.inner.inner.range.start_bound(), + std::ops::Bound::Unbounded, + ) + .contains(&ret.len()) + { + ret.push( + a.inner + .inner + .spec + .gen(rng, timeout) + .map_err(ConfigurationError::from)?, + ); + } + Ok(Value::List(ret)) + } + ValueSpecList::String(a) => a.gen(rng, timeout).map_err(ConfigurationError::from), + ValueSpecList::Union(a) => a.gen(rng, timeout).map_err(ConfigurationError::from), + } + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct ValueSpecNumber { + range: Option>, + #[serde(default)] + integral: bool, + #[serde(skip_serializing_if = "Option::is_none")] + units: Option, +} +#[async_trait] +impl ValueSpec for ValueSpecNumber { + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + match value { + Value::Number(n) => { + if self.integral && n.floor() != *n { + return Err(NoMatchWithPath::new(MatchError::NonIntegral(*n))); + } + if let Some(range) = &self.range { + if !range.contains(n) { + return Err(NoMatchWithPath::new(MatchError::OutOfRange( + range.clone(), + *n, + ))); + } + } + Ok(()) + } + Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), + a => Err(NoMatchWithPath::new(MatchError::InvalidType( + "object", + a.type_of(), + ))), + } + } + fn validate(&self, _manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + Ok(()) + } + async fn update(&self, _value: &mut Value) -> Result<(), ConfigurationError> { + Ok(()) + } + fn requires(&self, _id: &str, _value: &Value) -> bool { + false + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + match (lhs, rhs) { + (Value::Number(lhs), Value::Number(rhs)) => lhs == rhs, + _ => false, + } + } +} +#[derive(Clone, Copy, Debug, serde::Serialize)] +pub struct Number(pub f64); +impl<'de> serde::de::Deserialize<'de> for Number { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + use serde::de::*; + struct NumberVisitor; + impl<'de> Visitor<'de> for NumberVisitor { + type Value = Number; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a number") + } + fn visit_i8(self, value: i8) -> Result { + Ok(Number(value.into())) + } + fn visit_i16(self, value: i16) -> Result { + Ok(Number(value.into())) + } + fn visit_i32(self, value: i32) -> Result { + Ok(Number(value.into())) + } + fn visit_i64(self, value: i64) -> Result { + Ok(Number(value as f64)) + } + fn visit_u8(self, value: u8) -> Result { + Ok(Number(value.into())) + } + fn visit_u16(self, value: u16) -> Result { + Ok(Number(value.into())) + } + fn visit_u32(self, value: u32) -> Result { + Ok(Number(value.into())) + } + fn visit_u64(self, value: u64) -> Result { + Ok(Number(value as f64)) + } + fn visit_f32(self, value: f32) -> Result { + Ok(Number(value.into())) + } + fn visit_f64(self, value: f64) -> Result { + Ok(Number(value)) + } + } + deserializer.deserialize_any(NumberVisitor) + } +} +impl DefaultableWith for ValueSpecNumber { + type DefaultSpec = Option; + type Error = crate::util::Never; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + _rng: &mut R, + _timeout: &Option, + ) -> Result { + Ok(spec.map(|s| Value::Number(s.0)).unwrap_or(Value::Null)) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ValueSpecObject { + pub spec: ConfigSpec, + #[serde(default)] + pub null_by_default: bool, + pub display_as: Option, + #[serde(default)] + pub unique_by: UniqueBy, +} +#[async_trait] +impl ValueSpec for ValueSpecObject { + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + match value { + Value::Object(o) => self.spec.matches(o), + Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), + a => Err(NoMatchWithPath::new(MatchError::InvalidType( + "object", + a.type_of(), + ))), + } + } + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + self.spec.validate(manifest) + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + if let Value::Object(o) = value { + self.spec.update(o).await + } else { + Err(ConfigurationError::NoMatch(NoMatchWithPath::new( + MatchError::InvalidType("object", value.type_of()), + ))) + } + } + fn requires(&self, id: &str, value: &Value) -> bool { + if let Value::Object(o) = value { + self.spec.requires(id, o) + } else { + false + } + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + match (lhs, rhs) { + (Value::Object(lhs), Value::Object(rhs)) => self.unique_by.eq(lhs, rhs), + _ => false, + } + } +} +impl DefaultableWith for ValueSpecObject { + type DefaultSpec = Config; + type Error = crate::util::Never; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + _rng: &mut R, + _timeout: &Option, + ) -> Result { + if self.null_by_default { + Ok(Value::Null) + } else { + Ok(Value::Object(spec.clone())) + } + } +} +impl Defaultable for ValueSpecObject { + type Error = ConfigurationError; + + fn gen( + &self, + rng: &mut R, + timeout: &Option, + ) -> Result { + if self.null_by_default { + Ok(Value::Null) + } else { + self.spec.gen(rng, timeout).map(Value::Object) + } + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct ConfigSpec(pub LinearMap); +impl ConfigSpec { + pub fn matches(&self, value: &Config) -> Result<(), NoMatchWithPath> { + for (key, val) in self.0.iter() { + if let Some(v) = value.0.get(key) { + val.matches(v).map_err(|e| e.prepend(key.clone()))?; + } else { + val.matches(&Value::Null) + .map_err(|e| e.prepend(key.clone()))?; + } + } + Ok(()) + } + + pub fn gen( + &self, + rng: &mut R, + timeout: &Option, + ) -> Result { + let mut res = LinearMap::new(); + for (key, val) in self.0.iter() { + res.insert(key.clone(), val.gen(rng, timeout)?); + } + Ok(Config(res)) + } + + pub fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + for (name, val) in &self.0 { + if let Err(_) = super::rules::validate_key(&name) { + return Err(NoMatchWithPath::new(MatchError::InvalidKey( + name.to_owned(), + ))); + } + val.validate(manifest) + .map_err(|e| e.prepend(name.clone()))?; + } + Ok(()) + } + + pub async fn update(&self, cfg: &mut Config) -> Result<(), ConfigurationError> { + for (k, v) in cfg.0.iter_mut() { + match self.0.get(k) { + None => (), + Some(vs) => match vs.update(v).await { + Err(ConfigurationError::NoMatch(e)) => { + Err(ConfigurationError::NoMatch(e.prepend(k.clone()))) + } + a => a, + }?, + }; + } + Ok(()) + } + pub fn requires(&self, id: &str, cfg: &Config) -> bool { + self.0 + .iter() + .any(|(k, v)| v.requires(id, cfg.0.get(k).unwrap_or(&STATIC_NULL))) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Pattern { + #[serde(with = "util::serde_regex")] + pub pattern: Regex, + pub pattern_description: String, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct ValueSpecString { + #[serde(flatten)] + pub pattern: Option, + #[serde(default)] + pub copyable: bool, + #[serde(default)] + pub masked: bool, +} +#[async_trait] +impl ValueSpec for ValueSpecString { + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + match value { + Value::String(s) => { + if let Some(pattern) = &self.pattern { + if pattern.pattern.is_match(s) { + Ok(()) + } else { + Err(NoMatchWithPath::new(MatchError::Pattern( + s.to_owned(), + pattern.pattern.clone(), + ))) + } + } else { + Ok(()) + } + } + Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), + a => Err(NoMatchWithPath::new(MatchError::InvalidType( + "string", + a.type_of(), + ))), + } + } + fn validate(&self, _manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + Ok(()) + } + async fn update(&self, _value: &mut Value) -> Result<(), ConfigurationError> { + Ok(()) + } + fn requires(&self, _id: &str, _value: &Value) -> bool { + false + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + match (lhs, rhs) { + (Value::String(lhs), Value::String(rhs)) => lhs == rhs, + _ => false, + } + } +} +impl DefaultableWith for ValueSpecString { + type DefaultSpec = Option; + type Error = TimeoutError; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + rng: &mut R, + timeout: &Option, + ) -> Result { + if let Some(spec) = spec { + let now = timeout.as_ref().map(|_| std::time::Instant::now()); + loop { + let candidate = spec.gen(rng); + match (spec, &self.pattern) { + (DefaultString::Entropy(_), Some(pattern)) + if !pattern.pattern.is_match(&candidate) => + { + () + } + _ => { + return Ok(Value::String(candidate)); + } + } + if let (Some(now), Some(timeout)) = (now, timeout) { + if &now.elapsed() > timeout { + return Err(TimeoutError); + } + } + } + } else { + Ok(Value::Null) + } + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum DefaultString { + Literal(String), + Entropy(Entropy), +} +impl DefaultString { + pub fn gen(&self, rng: &mut R) -> String { + match self { + DefaultString::Literal(s) => s.clone(), + DefaultString::Entropy(e) => e.gen(rng), + } + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct Entropy { + pub charset: Option, + pub len: usize, +} +impl Entropy { + pub fn gen(&self, rng: &mut R) -> String { + let len = self.len; + let set = self + .charset + .as_ref() + .map(|cs| Cow::Borrowed(cs)) + .unwrap_or_else(|| Cow::Owned(Default::default())); + std::iter::repeat_with(|| set.gen(rng)).take(len).collect() + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UnionTag { + pub id: String, + pub name: String, + pub description: Option, + pub variant_names: LinearMap, +} + +#[derive(Clone, Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ValueSpecUnion { + pub tag: UnionTag, + pub variants: LinearMap, + pub display_as: Option, + pub unique_by: UniqueBy, +} + +impl<'de> serde::de::Deserialize<'de> for ValueSpecUnion { + fn deserialize>(deserializer: D) -> Result { + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + #[serde(untagged)] + pub enum _UnionTag { + Old(String), + New(UnionTag), + } + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct _ValueSpecUnion { + pub variants: LinearMap, + pub tag: _UnionTag, + pub display_as: Option, + #[serde(default)] + pub unique_by: UniqueBy, + } + + let union = _ValueSpecUnion::deserialize(deserializer)?; + Ok(ValueSpecUnion { + tag: match union.tag { + _UnionTag::Old(id) => UnionTag { + id: id.clone(), + name: id, + description: None, + variant_names: union + .variants + .keys() + .map(|k| (k.to_owned(), k.to_owned())) + .collect(), + }, + _UnionTag::New(UnionTag { + id, + name, + description, + mut variant_names, + }) => UnionTag { + id, + name, + description, + variant_names: { + let mut iter = union.variants.keys(); + while variant_names.len() < union.variants.len() { + if let Some(variant) = iter.next() { + variant_names.insert(variant.to_owned(), variant.to_owned()); + } else { + break; + } + } + variant_names + }, + }, + }, + variants: union.variants, + display_as: union.display_as, + unique_by: union.unique_by, + }) + } +} + +#[async_trait] +impl ValueSpec for ValueSpecUnion { + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + match value { + Value::Object(o) => { + if let Some(Value::String(ref tag)) = o.0.get(&self.tag.id) { + if let Some(obj_spec) = self.variants.get(tag) { + let mut without_tag = o.clone(); + without_tag.0.remove(&self.tag.id); + obj_spec.matches(&without_tag) + } else { + Err(NoMatchWithPath::new(MatchError::Union( + tag.clone(), + self.variants.keys().cloned().collect(), + ))) + } + } else { + Err(NoMatchWithPath::new(MatchError::MissingTag( + self.tag.id.clone(), + ))) + } + } + Value::Null => Err(NoMatchWithPath::new(MatchError::NotNullable)), + a => Err(NoMatchWithPath::new(MatchError::InvalidType( + "object", + a.type_of(), + ))), + } + } + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + for (name, variant) in &self.variants { + if variant.0.get(&self.tag.id).is_some() { + return Err(NoMatchWithPath::new(MatchError::PropertyMatchesUnionTag( + self.tag.id.clone(), + name.clone(), + ))); + } + variant.validate(manifest)?; + } + Ok(()) + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + if let Value::Object(o) = value { + match o.0.get(&self.tag.id) { + None => Err(ConfigurationError::NoMatch(NoMatchWithPath::new( + MatchError::MissingTag(self.tag.id.clone()), + ))), + Some(Value::String(tag)) => match self.variants.get(tag) { + None => Err(ConfigurationError::InvalidVariant(tag.clone())), + Some(spec) => spec.update(o).await, + }, + Some(other) => Err(ConfigurationError::NoMatch( + NoMatchWithPath::new(MatchError::InvalidType("string", other.type_of())) + .prepend(self.tag.id.clone()), + )), + } + } else { + Err(ConfigurationError::NoMatch(NoMatchWithPath::new( + MatchError::InvalidType("object", value.type_of()), + ))) + } + } + fn requires(&self, id: &str, value: &Value) -> bool { + if let Value::Object(o) = value { + match o.0.get(&self.tag.id) { + Some(Value::String(tag)) => match self.variants.get(tag) { + None => false, + Some(spec) => spec.requires(id, o), + }, + _ => false, + } + } else { + false + } + } + fn eq(&self, lhs: &Value, rhs: &Value) -> bool { + match (lhs, rhs) { + (Value::Object(lhs), Value::Object(rhs)) => self.unique_by.eq(lhs, rhs), + _ => false, + } + } +} +impl DefaultableWith for ValueSpecUnion { + type DefaultSpec = String; + type Error = ConfigurationError; + + fn gen_with( + &self, + spec: &Self::DefaultSpec, + rng: &mut R, + timeout: &Option, + ) -> Result { + let variant = if let Some(v) = self.variants.get(spec) { + v + } else { + return Err(ConfigurationError::InvalidVariant(spec.clone())); + }; + let cfg_res = variant.gen(rng, timeout)?; + + let mut tagged_cfg = LinearMap::new(); + tagged_cfg.insert(self.tag.id.clone(), Value::String(spec.clone())); + tagged_cfg.extend(cfg_res.0.into_iter()); + + Ok(Value::Object(Config(tagged_cfg))) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(tag = "subtype")] +#[serde(rename_all = "kebab-case")] +pub enum ValueSpecPointer { + App(AppPointerSpec), + System(SystemPointerSpec), +} +impl fmt::Display for ValueSpecPointer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ValueSpecPointer::App(p) => write!(f, "{}", p), + ValueSpecPointer::System(p) => write!(f, "{}", p), + } + } +} +impl Defaultable for ValueSpecPointer { + type Error = ConfigurationError; + fn gen( + &self, + _rng: &mut R, + _timeout: &Option, + ) -> Result { + Ok(Value::Null) + } +} +#[async_trait] +impl ValueSpec for ValueSpecPointer { + fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { + match self { + ValueSpecPointer::App(a) => a.matches(value), + ValueSpecPointer::System(a) => a.matches(value), + } + } + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + match self { + ValueSpecPointer::App(a) => a.validate(manifest), + ValueSpecPointer::System(a) => a.validate(manifest), + } + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + match self { + ValueSpecPointer::App(a) => a.update(value).await, + ValueSpecPointer::System(a) => a.update(value).await, + } + } + fn requires(&self, id: &str, value: &Value) -> bool { + match self { + ValueSpecPointer::App(a) => a.requires(id, value), + ValueSpecPointer::System(a) => a.requires(id, value), + } + } + fn eq(&self, _lhs: &Value, _rhs: &Value) -> bool { + false + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppPointerSpec { + pub app_id: String, + #[serde(flatten)] + pub target: AppPointerSpecVariants, +} +impl fmt::Display for AppPointerSpec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "[{}].{}", self.app_id, self.target) + } +} +impl AppPointerSpec { + async fn deref(&self) -> Result { + match self.target { + AppPointerSpecVariants::TorAddress => { + let mut apps = crate::apps::list_info() + .await + .map_err(ConfigurationError::SystemError)?; + let info = apps.remove(&self.app_id); + Ok(info + .and_then(|info| info.tor_address) + .map(Value::String) + .unwrap_or(Value::Null)) + } + AppPointerSpecVariants::TorKey => { + let services_path = PersistencePath::from_ref(crate::SERVICES_YAML); + let service_map = crate::tor::services_map(&services_path) + .await + .map_err(ConfigurationError::SystemError)?; + let service = + service_map + .map + .get(&self.app_id) + .ok_or(ConfigurationError::SystemError(crate::Error::new( + failure::format_err!("App Not Found"), + Some(crate::error::NOT_FOUND), + )))?; + Ok( + crate::tor::read_tor_key(&self.app_id, service.hidden_service_version, None) + .await + .map(Value::String) + .unwrap_or(Value::Null), + ) + } + AppPointerSpecVariants::LanAddress => { + let services_path = PersistencePath::from_ref(crate::SERVICES_YAML); + let mut service_map = crate::tor::services_map(&services_path) + .await + .map_err(ConfigurationError::SystemError)?; + let service = service_map.map.remove(&self.app_id); + Ok(service + .map(|service| Value::String(format!("{}", service.ip))) + .unwrap_or(Value::Null)) + } + AppPointerSpecVariants::Config { ref index } => { + // check if the app exists + if !crate::apps::list_info() + .await + .map_err(ConfigurationError::SystemError)? + .contains_key(&self.app_id) + { + return Ok(Value::Null); + } + // fetch the config of the pointer target + let app_config = crate::apps::config(&self.app_id) + .await + .map_err(ConfigurationError::SystemError)?; + let cfg = if let Some(cfg) = app_config.config { + cfg + } else { + return Ok(Value::Null); + }; + let mut cfgs = LinearMap::new(); + cfgs.insert(self.app_id.as_str(), Cow::Borrowed(&cfg)); + + Ok((index.compiled)(&cfg, &cfgs)) + } + } + } +} +impl Defaultable for AppPointerSpec { + type Error = ConfigurationError; + fn gen( + &self, + _rng: &mut R, + _timeout: &Option, + ) -> Result { + Ok(Value::Null) + } +} +#[async_trait] +impl ValueSpec for AppPointerSpec { + fn matches(&self, _value: &Value) -> Result<(), NoMatchWithPath> { + Ok(()) + } + fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + if manifest.id != self.app_id && !manifest.dependencies.0.contains_key(&self.app_id) { + return Err(NoMatchWithPath::new(MatchError::InvalidPointer( + ValueSpecPointer::App(self.clone()), + ))); + } + match self.target { + AppPointerSpecVariants::TorKey if manifest.id != self.app_id => { + Err(NoMatchWithPath::new(MatchError::InvalidPointer( + ValueSpecPointer::App(self.clone()), + ))) + } + _ => Ok(()), + } + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + *value = self.deref().await?; + Ok(()) + } + fn requires(&self, id: &str, _value: &Value) -> bool { + self.app_id == id + } + fn eq(&self, _lhs: &Value, _rhs: &Value) -> bool { + false + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(tag = "target")] +#[serde(rename_all = "kebab-case")] +pub enum AppPointerSpecVariants { + TorAddress, + TorKey, + LanAddress, + Config { index: Arc }, +} +impl fmt::Display for AppPointerSpecVariants { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::TorAddress => write!(f, "TOR_ADDRESS"), + Self::TorKey => write!(f, "TOR_KEY"), + Self::LanAddress => write!(f, "LAN_ADDRESS"), + Self::Config { index } => write!(f, "{}", index.src), + } + } +} + +#[derive(Clone)] +pub struct ConfigPointer { + pub src: String, + pub compiled: Arc>, +} +impl std::fmt::Debug for ConfigPointer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConfigPointer") + .field("src", &self.src) + .field("compiled", &"Fn(&Config, &Config) -> bool") + .finish() + } +} +impl<'de> serde::de::Deserialize<'de> for ConfigPointer { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let src = String::deserialize(deserializer)?; + let compiled = super::rules::compile_expr(&src).map_err(serde::de::Error::custom)?; + Ok(ConfigPointer { + src, + compiled: Arc::new(compiled), + }) + } +} +impl serde::ser::Serialize for ConfigPointer { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(&self.src) + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "target")] +pub enum SystemPointerSpec { + HostIp, +} +impl fmt::Display for SystemPointerSpec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "[SYSTEM].{}", + match self { + SystemPointerSpec::HostIp => "HOST_IP", + } + ) + } +} +impl SystemPointerSpec { + async fn deref(&self) -> Result { + Ok(match self { + SystemPointerSpec::HostIp => { + Value::String(format!("{}", std::net::Ipv4Addr::from(crate::HOST_IP))) + } + }) + } +} +impl Defaultable for SystemPointerSpec { + type Error = ConfigurationError; + fn gen( + &self, + _rng: &mut R, + _timeout: &Option, + ) -> Result { + Ok(Value::Null) + } +} +#[async_trait] +impl ValueSpec for SystemPointerSpec { + fn matches(&self, _value: &Value) -> Result<(), NoMatchWithPath> { + Ok(()) + } + fn validate(&self, _manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + Ok(()) + } + async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + *value = self.deref().await?; + Ok(()) + } + fn requires(&self, _id: &str, _value: &Value) -> bool { + false + } + fn eq(&self, _lhs: &Value, _rhs: &Value) -> bool { + false + } +} + +#[cfg(test)] +mod test { + use rand::SeedableRng; + + use super::*; + + #[test] + fn test_config() { + let spec = serde_json::json!({ + "randomEnum": { + "name": "Random Enum", + "type": "enum", + "default": "null", + "description": "This is not even real.", + "changeWarning": "Be careful chnaging this!", + "values": [ + "null", + "option1", + "option2", + "option3" + ] + }, + "testnet": { + "name": "Testnet", + "type": "boolean", + "description": "determines whether your node is running ontestnet or mainnet", + "changeWarning": "Chain will have to resync!", + "default": false + }, + "favoriteNumber": { + "name": "Favorite Number", + "type": "number", + "integral": false, + "description": "Your favorite number of all time", + "changeWarning": "Once you set this number, it can never be changed without severe consequences.", + "nullable": false, + "default": 7, + "range": "(-100,100]" + }, + "secondaryNumbers": { + "name": "Unlucky Numbers", + "type": "list", + "subtype": "number", + "description": "Numbers that you like but are not your top favorite.", + "spec": { + "type": "number", + "integral": false, + "range": "[-100,200)" + }, + "range": "[0,10]", + "default": [ + 2, + 3 + ] + }, + "rpcsettings": { + "name": "RPC Settings", + "type": "object", + "description": "rpc username and password", + "changeWarning": "Adding RPC users gives them special permissions on your node.", + "nullable": false, + "nullByDefault": false, + "spec": { + "laws": { + "name": "Laws", + "type": "object", + "description": "the law of the realm", + "nullable": true, + "nullByDefault": true, + "spec": { + "law1": { + "name": "First Law", + "type": "string", + "description": "the first law", + "nullable": true + }, + "law2": { + "name": "Second Law", + "type": "string", + "description": "the second law", + "nullable": true + } + } + }, + "rulemakers": { + "name": "Rule Makers", + "type": "list", + "subtype": "object", + "description": "the people who make the rules", + "range": "[0,2]", + "default": [], + "spec": { + "type": "object", + "spec": { + "rulemakername": { + "name": "Rulemaker Name", + "type": "string", + "description": "the name of the rule maker", + "nullable": false, + "default": { + "charset": "a-g,2-9", + "len": 12 + } + }, + "rulemakerip": { + "name": "Rulemaker IP", + "type": "string", + "description": "the ip of the rule maker", + "nullable": false, + "default": "192.168.1.0", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$", + "patternDescription": "may only contain numbers and periods" + } + } + } + }, + "rpcuser": { + "name": "RPC Username", + "type": "string", + "description": "rpc username", + "nullable": false, + "default": "defaultrpcusername", + "pattern": "^[a-zA-Z]+$", + "patternDescription": "must contain only letters." + }, + "rpcpass": { + "name": "RPC User Password", + "type": "string", + "description": "rpc password", + "nullable": false, + "default": { + "charset": "a-z,A-Z,2-9", + "len": 20 + } + } + } + }, + "advanced": { + "name": "Advanced", + "type": "object", + "description": "Advanced settings", + "nullable": false, + "nullByDefault": false, + "spec": { + "notifications": { + "name": "Notification Preferences", + "type": "list", + "subtype": "enum", + "description": "how you want to be notified", + "range": "[1,3]", + "default": [ + "email" + ], + "spec": { + "type": "enum", + "values": [ + "email", + "text", + "call", + "push", + "webhook" + ] + } + } + } + }, + "bitcoinNode": { + "name": "Bitcoin Node Settings", + "type": "union", + "description": "The node settings", + "default": "internal", + "tag": { + "id": "type", + "name": "Type", + "variantNames": {} + }, + "variants": { + "internal": { + "lan-address": { + "name": "LAN Address", + "type": "pointer", + "subtype": "app", + "target": "lan-address", + "app-id": "bitcoind", + "description": "the lan address" + } + }, + "external": { + "public-domain": { + "name": "Public Domain", + "type": "string", + "description": "the public address of the node", + "nullable": false, + "default": "bitcoinnode.com", + "pattern": ".*", + "patternDescription": "anything" + } + } + } + }, + "port": { + "name": "Port", + "type": "number", + "integral": true, + "description": "the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444", + "nullable": true, + "default": 8333, + "range": "[0, 9999]", + "units": "m/s" + }, + "maxconnections": { + "name": "Max Connections", + "type": "string", + "description": "the maximum number of commections allowed to your Bitcoin node", + "nullable": true + }, + "rpcallowip": { + "name": "RPC Allowed IPs", + "type": "list", + "subtype": "string", + "description": "external ip addresses that are authorized to access your Bitcoin node", + "changeWarning": "Any IP you allow here will have RPC access to your Bitcoin node.", + "range": "[1,10]", + "default": [ + "192.168.1.1" + ], + "spec": { + "type": "string", + "pattern": "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))", + "patternDescription": "must be a valid ipv4, ipv6, or domain name" + } + }, + "rpcauth": { + "name": "RPC Auth", + "type": "list", + "subtype": "string", + "description": "api keys that are authorized to access your Bitcoin node.", + "range": "[0,*)", + "default": [], + "spec": { + "type": "string" + } + } + }); + let spec: ConfigSpec = serde_json::from_value(spec).unwrap(); + let mut deps = crate::dependencies::Dependencies::default(); + deps.0.insert( + "bitcoind".to_owned(), + crate::dependencies::DepInfo { + version: "^0.20.0".parse().unwrap(), + description: None, + mount_public: false, + mount_shared: false, + optional: Some("Could be external.".to_owned()), + config: Vec::new(), + }, + ); + spec.validate(&crate::manifest::ManifestV0 { + id: "test-app".to_owned(), + version: "0.1.0".parse().unwrap(), + title: "Test App".to_owned(), + description: crate::manifest::Description { + short: "A test app.".to_owned(), + long: "A super cool test app for testing".to_owned(), + }, + release_notes: "Some things changed".to_owned(), + ports: Vec::new(), + image: crate::manifest::ImageConfig::Tar, + shm_size_mb: None, + mount: "/root".parse().unwrap(), + public: None, + shared: None, + has_instructions: false, + os_version_required: ">=0.2.5".parse().unwrap(), + os_version_recommended: ">=0.2.5".parse().unwrap(), + assets: Vec::new(), + hidden_service_version: crate::tor::HiddenServiceVersion::V3, + dependencies: deps, + extra: LinearMap::new(), + }) + .unwrap(); + let config = spec + .gen(&mut rand::rngs::StdRng::from_entropy(), &None) + .unwrap(); + spec.matches(&config).unwrap(); + } +} diff --git a/appmgr/src/config/util.rs b/appmgr/src/config/util.rs new file mode 100644 index 000000000..d7595e52d --- /dev/null +++ b/appmgr/src/config/util.rs @@ -0,0 +1,367 @@ +use std::ops::Bound; +use std::ops::RangeBounds; +use std::ops::RangeInclusive; + +use rand::{distributions::Distribution, Rng}; + +use super::value::Config; + +pub const STATIC_NULL: super::value::Value = super::value::Value::Null; + +#[derive(Clone, Debug)] +pub struct CharSet(pub Vec<(RangeInclusive, usize)>, usize); +impl CharSet { + pub fn contains(&self, c: &char) -> bool { + self.0.iter().any(|r| r.0.contains(c)) + } + pub fn gen(&self, rng: &mut R) -> char { + let mut idx = rng.gen_range(0, self.1); + for r in &self.0 { + if idx < r.1 { + return std::convert::TryFrom::try_from( + rand::distributions::Uniform::new_inclusive( + u32::from(*r.0.start()), + u32::from(*r.0.end()), + ) + .sample(rng), + ) + .unwrap(); + } else { + idx -= r.1; + } + } + unreachable!() + } +} +impl Default for CharSet { + fn default() -> Self { + CharSet(vec![('!'..='~', 94)], 94) + } +} +impl<'de> serde::de::Deserialize<'de> for CharSet { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let mut res = Vec::new(); + let mut len = 0; + let mut a: Option = None; + let mut b: Option = None; + let mut in_range = false; + for c in s.chars() { + match c { + ',' => match (a, b, in_range) { + (Some(start), Some(end), _) => { + if !end.is_ascii() { + return Err(serde::de::Error::custom("Invalid Character")); + } + if start >= end { + return Err(serde::de::Error::custom("Invalid Bounds")); + } + let l = u32::from(end) - u32::from(start) + 1; + res.push((start..=end, l as usize)); + len += l as usize; + a = None; + b = None; + in_range = false; + } + (Some(start), None, false) => { + len += 1; + res.push((start..=start, 1)); + a = None; + } + (Some(_), None, true) => { + b = Some(','); + } + (None, None, false) => { + a = Some(','); + } + _ => { + return Err(serde::de::Error::custom("Syntax Error")); + } + }, + '-' => { + if a.is_none() { + a = Some('-'); + } else if !in_range { + in_range = true; + } else if b.is_none() { + b = Some('-') + } else { + return Err(serde::de::Error::custom("Syntax Error")); + } + } + _ => { + if a.is_none() { + a = Some(c); + } else if in_range && b.is_none() { + b = Some(c); + } else { + return Err(serde::de::Error::custom("Syntax Error")); + } + } + } + } + match (a, b) { + (Some(start), Some(end)) => { + if !end.is_ascii() { + return Err(serde::de::Error::custom("Invalid Character")); + } + if start >= end { + return Err(serde::de::Error::custom("Invalid Bounds")); + } + let l = u32::from(end) - u32::from(start) + 1; + res.push((start..=end, l as usize)); + len += l as usize; + } + (Some(c), None) => { + len += 1; + res.push((c..=c, 1)); + } + _ => (), + } + + Ok(CharSet(res, len)) + } +} +impl serde::ser::Serialize for CharSet { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + <&str>::serialize( + &self + .0 + .iter() + .map(|r| match r.1 { + 1 => format!("{}", r.0.start()), + _ => format!("{}-{}", r.0.start(), r.0.end()), + }) + .collect::>() + .join(",") + .as_str(), + serializer, + ) + } +} + +pub mod serde_regex { + use regex::Regex; + use serde::*; + + pub fn serialize(regex: &Regex, serializer: S) -> Result + where + S: Serializer, + { + <&str>::serialize(®ex.as_str(), serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Regex::new(&s).map_err(|e| de::Error::custom(e)) + } +} + +#[derive(Clone, Debug)] +pub struct NumRange( + pub (Bound, Bound), +); +impl std::ops::Deref for NumRange +where + T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd, +{ + type Target = (Bound, Bound); + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl<'de, T> serde::de::Deserialize<'de> for NumRange +where + T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd, + ::Err: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let mut split = s.split(","); + let start = split + .next() + .map(|s| match s.get(..1) { + Some("(") => match s.get(1..2) { + Some("*") => Ok(Bound::Unbounded), + _ => s[1..] + .trim() + .parse() + .map(Bound::Excluded) + .map_err(|e| serde::de::Error::custom(e)), + }, + Some("[") => s[1..] + .trim() + .parse() + .map(Bound::Included) + .map_err(|e| serde::de::Error::custom(e)), + _ => Err(serde::de::Error::custom(format!( + "Could not parse left bound: {}", + s + ))), + }) + .transpose()? + .unwrap(); + let end = split + .next() + .map(|s| match s.get(s.len() - 1..) { + Some(")") => match s.get(s.len() - 2..s.len() - 1) { + Some("*") => Ok(Bound::Unbounded), + _ => s[..s.len() - 1] + .trim() + .parse() + .map(Bound::Excluded) + .map_err(|e| serde::de::Error::custom(e)), + }, + Some("]") => s[..s.len() - 1] + .trim() + .parse() + .map(Bound::Included) + .map_err(|e| serde::de::Error::custom(e)), + _ => Err(serde::de::Error::custom(format!( + "Could not parse right bound: {}", + s + ))), + }) + .transpose()? + .unwrap_or(Bound::Unbounded); + + Ok(NumRange((start, end))) + } +} +impl std::fmt::Display for NumRange +where + T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.start_bound() { + Bound::Excluded(n) => write!(f, "({},", n)?, + Bound::Included(n) => write!(f, "[{},", n)?, + Bound::Unbounded => write!(f, "(*,")?, + }; + match self.end_bound() { + Bound::Excluded(n) => write!(f, "{})", n), + Bound::Included(n) => write!(f, "{}]", n), + Bound::Unbounded => write!(f, "*)"), + } + } +} +impl serde::ser::Serialize for NumRange +where + T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + <&str>::serialize(&format!("{}", self).as_str(), serializer) + } +} + +#[derive(Clone, Debug)] +pub enum UniqueBy { + Any(Vec), + All(Vec), + Exactly(String), + NotUnique, +} +impl UniqueBy { + pub fn eq(&self, lhs: &Config, rhs: &Config) -> bool { + match self { + UniqueBy::Any(any) => any.iter().any(|u| u.eq(lhs, rhs)), + UniqueBy::All(all) => all.iter().all(|u| u.eq(lhs, rhs)), + UniqueBy::Exactly(key) => lhs.0.get(key) == rhs.0.get(key), + UniqueBy::NotUnique => false, + } + } +} +impl Default for UniqueBy { + fn default() -> Self { + UniqueBy::NotUnique + } +} +impl<'de> serde::de::Deserialize<'de> for UniqueBy { + fn deserialize>(deserializer: D) -> Result { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = UniqueBy; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a key, an \"any\" object, or an \"all\" object") + } + fn visit_str(self, v: &str) -> Result { + Ok(UniqueBy::Exactly(v.to_owned())) + } + fn visit_string(self, v: String) -> Result { + Ok(UniqueBy::Exactly(v)) + } + fn visit_map>( + self, + mut map: A, + ) -> Result { + let mut variant = None; + while let Some(key) = map.next_key()? { + match key { + "any" => { + return Ok(UniqueBy::Any(map.next_value()?)); + } + "all" => { + return Ok(UniqueBy::All(map.next_value()?)); + } + _ => { + variant = Some(key); + } + } + } + Err(serde::de::Error::unknown_variant( + variant.unwrap_or_default(), + &["any", "all"], + )) + } + fn visit_unit(self) -> Result { + Ok(UniqueBy::NotUnique) + } + fn visit_none(self) -> Result { + Ok(UniqueBy::NotUnique) + } + } + deserializer.deserialize_any(Visitor) + } +} + +impl serde::ser::Serialize for UniqueBy { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + use serde::ser::SerializeMap; + + match self { + UniqueBy::Any(any) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_key("any")?; + map.serialize_value(any)?; + map.end() + } + UniqueBy::All(all) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_key("all")?; + map.serialize_value(all)?; + map.end() + } + UniqueBy::Exactly(key) => serializer.serialize_str(key), + UniqueBy::NotUnique => serializer.serialize_unit(), + } + } +} diff --git a/appmgr/src/config/value.rs b/appmgr/src/config/value.rs new file mode 100644 index 000000000..deb8439a9 --- /dev/null +++ b/appmgr/src/config/value.rs @@ -0,0 +1,66 @@ +use linear_map::LinearMap; + +#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Config(pub LinearMap); + +impl Config { + pub fn merge_with(&mut self, other: Config) { + for (key, val) in other.0.into_iter() { + match (self.0.get_mut(&key), &val) { + (Some(Value::Object(l_obj)), Value::Object(_)) => { + // gross, I know. https://github.com/rust-lang/rust/issues/45600 + let r_obj = match val { + Value::Object(r_obj) => r_obj, + _ => unreachable!(), + }; + l_obj.merge_with(r_obj) + } + (Some(Value::List(l_vec)), Value::List(_)) => { + let mut r_vec = match val { + Value::List(r_vec) => r_vec, + _ => unreachable!(), + }; + l_vec.append(&mut r_vec); + } + _ => { + self.0.insert(key, val); + } + } + } + } +} + +fn serialize_num(num: &f64, serializer: S) -> Result { + if *num < (1_i64 << f64::MANTISSA_DIGITS) as f64 + && *num > -(1_i64 << f64::MANTISSA_DIGITS) as f64 + && num.trunc() == *num + { + serializer.serialize_i64(*num as i64) + } else { + serializer.serialize_f64(*num) + } +} + +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum Value { + String(String), + #[serde(serialize_with = "serialize_num")] + Number(f64), + Bool(bool), + List(Vec), + Object(Config), + Null, +} +impl Value { + pub fn type_of(&self) -> &'static str { + match self { + Value::String(_) => "string", + Value::Number(_) => "number", + Value::Bool(_) => "boolean", + Value::List(_) => "list", + Value::Object(_) => "object", + Value::Null => "null", + } + } +} diff --git a/appmgr/src/control.rs b/appmgr/src/control.rs new file mode 100644 index 000000000..72e464ca8 --- /dev/null +++ b/appmgr/src/control.rs @@ -0,0 +1,194 @@ +use std::path::Path; + +use futures::future::{BoxFuture, FutureExt}; +use linear_map::LinearMap; + +use crate::dependencies::{DependencyError, TaggedDependencyError}; +use crate::Error; + +pub async fn start_app(name: &str, update_metadata: bool) -> Result<(), Error> { + let lock = crate::util::lock_file( + format!( + "{}", + Path::new(crate::PERSISTENCE_DIR) + .join("apps") + .join(name) + .join("control.lock") + .display() + ), + true, + ) + .await?; + let status = crate::apps::status(name).await?.status; + if status == crate::apps::DockerStatus::Stopped { + if update_metadata { + crate::config::configure(name, None, None, false).await?; + crate::dependencies::update_shared(name).await?; + crate::dependencies::update_binds(name).await?; + } + crate::apps::set_needs_restart(name, false).await?; + let output = tokio::process::Command::new("docker") + .args(&["start", name]) + .stdout(std::process::Stdio::null()) + .output() + .await?; + crate::ensure_code!( + output.status.success(), + crate::error::DOCKER_ERROR, + "Failed to Start Application: {}", + std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error") + ); + } else if status == crate::apps::DockerStatus::Paused { + resume_app(name).await?; + } + crate::util::unlock(lock).await?; + Ok(()) +} + +pub async fn stop_app( + name: &str, + cascade: bool, + dry_run: bool, +) -> Result, Error> { + let mut res = LinearMap::new(); + if cascade { + stop_dependents(name, dry_run, DependencyError::NotRunning, &mut res).await?; + } + if !dry_run { + let lock = crate::util::lock_file( + format!( + "{}", + Path::new(crate::PERSISTENCE_DIR) + .join("apps") + .join(name) + .join("control.lock") + .display() + ), + true, + ) + .await?; + log::info!("Stopping {}", name); + let output = tokio::process::Command::new("docker") + .args(&["stop", "-t", "25", name]) + .stdout(std::process::Stdio::null()) + .output() + .await?; + crate::ensure_code!( + output.status.success(), + crate::error::DOCKER_ERROR, + "Failed to Stop Application: {}", + std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error") + ); + crate::util::unlock(lock).await?; + } + Ok(res) +} + +pub async fn stop_dependents( + name: &str, + dry_run: bool, + err: DependencyError, + res: &mut LinearMap, +) -> Result<(), Error> { + fn stop_dependents_rec<'a>( + name: &'a str, + dry_run: bool, + err: DependencyError, + res: &'a mut LinearMap, + ) -> BoxFuture<'a, Result<(), Error>> { + async move { + for dependent in crate::apps::dependents(name, false).await? { + if crate::apps::status(&dependent).await?.status + != crate::apps::DockerStatus::Stopped + { + stop_dependents_rec(&dependent, dry_run, DependencyError::NotRunning, res) + .await?; + stop_app(&dependent, false, dry_run).await?; + res.insert( + dependent, + TaggedDependencyError { + dependency: name.to_owned(), + error: err.clone(), + }, + ); + } + } + Ok(()) + } + .boxed() + } + stop_dependents_rec(name, dry_run, err, res).await +} + +pub async fn restart_app(name: &str) -> Result<(), Error> { + stop_app(name, false, false).await?; + if let Err(e) = start_app(name, true).await { + log::warn!("Stopping dependents"); + stop_dependents( + name, + false, + crate::dependencies::DependencyError::NotRunning, + &mut linear_map::LinearMap::new(), + ) + .await?; + return Err(e); + } + Ok(()) +} + +pub async fn pause_app(name: &str) -> Result<(), Error> { + let lock = crate::util::lock_file( + format!( + "{}", + Path::new(crate::PERSISTENCE_DIR) + .join("apps") + .join(name) + .join("control.lock") + .display() + ), + true, + ) + .await?; + let output = tokio::process::Command::new("docker") + .args(&["pause", name]) + .stdout(std::process::Stdio::null()) + .output() + .await?; + crate::ensure_code!( + output.status.success(), + crate::error::DOCKER_ERROR, + "Failed to Pause Application: {}", + std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error") + ); + + crate::util::unlock(lock).await?; + Ok(()) +} + +pub async fn resume_app(name: &str) -> Result<(), Error> { + let lock = crate::util::lock_file( + format!( + "{}", + Path::new(crate::PERSISTENCE_DIR) + .join("apps") + .join(name) + .join("control.lock") + .display() + ), + true, + ) + .await?; + let output = tokio::process::Command::new("docker") + .args(&["unpause", name]) + .stdout(std::process::Stdio::null()) + .output() + .await?; + crate::ensure_code!( + output.status.success(), + crate::error::DOCKER_ERROR, + "Failed to Resume Application: {}", + std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error") + ); + crate::util::unlock(lock).await?; + Ok(()) +} diff --git a/appmgr/src/dependencies.rs b/appmgr/src/dependencies.rs new file mode 100644 index 000000000..df7732461 --- /dev/null +++ b/appmgr/src/dependencies.rs @@ -0,0 +1,276 @@ +use std::borrow::Cow; +use std::path::Path; + +use emver::{Version, VersionRange}; +use linear_map::LinearMap; +use rand::SeedableRng; + +use crate::config::{Config, ConfigRuleEntryWithSuggestions, ConfigSpec}; +use crate::manifest::ManifestLatest; +use crate::Error; +use crate::ResultExt as _; + +#[derive(Clone, Debug, Fail, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum DependencyError { + NotInstalled, // "not-installed" + NotRunning, // "not-running" + IncorrectVersion { + expected: VersionRange, + received: Version, + }, // { "incorrect-version": { "expected": "0.1.0", "received": "^0.2.0" } } + ConfigUnsatisfied(Vec), // { "config-unsatisfied": ["Bitcoin Core must have pruning set to manual."] } + PointerUpdateError(String), // { "pointer-update-error": "Bitcoin Core RPC Port must not be 18332" } + Other(String), // { "other": "Well fuck." } +} +impl std::fmt::Display for DependencyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DependencyError::*; + match self { + NotInstalled => write!(f, "Not Installed"), + NotRunning => write!(f, "Not Running"), + IncorrectVersion { expected, received } => write!( + f, + "Incorrect Version: Expected {}, Received {}", + expected, received + ), + ConfigUnsatisfied(rules) => { + write!(f, "Configuration Rule(s) Violated: {}", rules.join(", ")) + } + PointerUpdateError(e) => write!(f, "Pointer Update Caused {}", e), + Other(e) => write!(f, "System Error: {}", e), + } + } +} + +#[derive(Clone, Debug, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct TaggedDependencyError { + pub dependency: String, + pub error: DependencyError, +} +impl std::fmt::Display for TaggedDependencyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.dependency, self.error) + } +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Dependencies(pub LinearMap); + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DepInfo { + pub version: VersionRange, + pub optional: Option, + pub description: Option, + #[serde(default)] + pub mount_public: bool, + #[serde(default)] + pub mount_shared: bool, + #[serde(default)] + pub config: Vec, +} +impl DepInfo { + pub async fn satisfied( + &self, + dependency_id: &str, + dependency_config: Option, // fetch if none + dependent_id: &str, + dependent_config: &Config, + ) -> Result, Error> { + let info = if let Some(info) = crate::apps::list_info().await?.remove(dependency_id) { + info + } else { + return Ok(Err(DependencyError::NotInstalled)); + }; + if !&info.version.satisfies(&self.version) { + return Ok(Err(DependencyError::IncorrectVersion { + expected: self.version.clone(), + received: info.version.clone(), + })); + } + let dependency_config = if let Some(cfg) = dependency_config { + cfg + } else { + let app_config = crate::apps::config(dependency_id).await?; + if let Some(cfg) = app_config.config { + cfg + } else { + app_config + .spec + .gen(&mut rand::rngs::StdRng::from_entropy(), &None) + .unwrap_or_default() + } + }; + let mut errors = Vec::new(); + let mut cfgs = LinearMap::with_capacity(2); + cfgs.insert(dependency_id, Cow::Borrowed(&dependency_config)); + cfgs.insert(dependent_id, Cow::Borrowed(dependent_config)); + for rule in self.config.iter() { + if !(rule.entry.rule.compiled)(&dependency_config, &cfgs) { + errors.push(rule.entry.description.clone()); + } + } + if !errors.is_empty() { + return Ok(Err(DependencyError::ConfigUnsatisfied(errors))); + } + if crate::apps::status(dependency_id).await?.status != crate::apps::DockerStatus::Running { + return Ok(Err(DependencyError::NotRunning)); + } + Ok(Ok(())) + } +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppDepInfo { + #[serde(flatten)] + pub info: DepInfo, + pub required: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppDependencies(pub LinearMap); + +pub async fn check_dependencies( + manifest: ManifestLatest, + dependent_config: &Config, + dependent_config_spec: &ConfigSpec, +) -> Result { + let mut deps = AppDependencies::default(); + for (dependency_id, dependency_info) in manifest.dependencies.0.into_iter() { + let required = dependency_info.optional.is_none() + || dependent_config_spec.requires(&dependency_id, dependent_config); + let error = dependency_info + .satisfied(&dependency_id, None, &manifest.id, dependent_config) + .await? + .err(); + let app_dep_info = AppDepInfo { + error, + required, + info: dependency_info, + }; + deps.0.insert(dependency_id, app_dep_info); + } + Ok(deps) +} + +pub async fn auto_configure( + dependent: &str, + dependency: &str, + dry_run: bool, +) -> Result { + let (dependent_config, mut dependency_config, manifest) = futures::try_join!( + crate::apps::config_or_default(dependent), + crate::apps::config_or_default(dependency), + crate::apps::manifest(dependent) + )?; + let mut cfgs = LinearMap::new(); + cfgs.insert(dependent, Cow::Borrowed(&dependent_config)); + cfgs.insert(dependency, Cow::Owned(dependency_config.clone())); + let dep_info = manifest + .dependencies + .0 + .get(dependency) + .ok_or_else(|| failure::format_err!("{} Does Not Depend On {}", dependent, dependency)) + .no_code()?; + for rule in &dep_info.config { + if let Err(e) = rule.apply(dependency, &mut dependency_config, &mut cfgs) { + log::warn!("Rule Unsatisfied After Applying Suggestions: {}", e); + } + } + crate::config::configure(dependency, Some(dependency_config), None, dry_run).await +} + +pub async fn update_shared(dependency_id: &str) -> Result<(), Error> { + let dependency_manifest = crate::apps::manifest(dependency_id).await?; + if let Some(shared) = dependency_manifest.shared { + for dependent_id in &crate::apps::dependents(dependency_id, false).await? { + let dependent_manifest = crate::apps::manifest(&dependent_id).await?; + if dependent_manifest + .dependencies + .0 + .get(dependency_id) + .ok_or_else(|| failure::format_err!("failed to index dependent: {}", dependent_id))? + .mount_shared + { + tokio::fs::create_dir_all( + Path::new(crate::VOLUMES) + .join(dependency_id) + .join(&shared) + .join(&dependent_id), + ) + .await?; + } + } + } + Ok(()) +} + +pub async fn update_binds(dependent_id: &str) -> Result<(), Error> { + let dependent_manifest = crate::apps::manifest(dependent_id).await?; + let dependency_manifests = futures::future::try_join_all( + dependent_manifest + .dependencies + .0 + .into_iter() + .filter(|(_, info)| info.mount_public || info.mount_shared) + .map(|(id, info)| async { + crate::apps::manifest(&id).await.map(|man| (id, info, man)) + }), + ) + .await?; + // i just have a gut feeling this shouldn't be concurrent + for (dependency_id, info, dependency_manifest) in dependency_manifests { + match (dependency_manifest.public, info.mount_public) { + (Some(public), true) => { + let public_path = Path::new(crate::VOLUMES).join(&dependency_id).join(public); + if let Ok(metadata) = tokio::fs::metadata(&public_path).await { + if metadata.is_dir() { + crate::disks::bind( + public_path, + Path::new(crate::VOLUMES) + .join(&dependent_id) + .join("start9") + .join("public") + .join(&dependency_id), + true, + ) + .await? + } + } + } + _ => (), + } + match (dependency_manifest.shared, info.mount_shared) { + (Some(shared), true) => { + let shared_path = Path::new(crate::VOLUMES) + .join(&dependency_id) + .join(shared) + .join(dependent_id); // namespaced by dependent + tokio::fs::create_dir_all(&shared_path).await?; + if let Ok(metadata) = tokio::fs::metadata(&shared_path).await { + if metadata.is_dir() { + crate::disks::bind( + shared_path, + Path::new(crate::VOLUMES) + .join(&dependent_id) + .join("start9") + .join("shared") + .join(&dependency_id), + false, + ) + .await? + } + } + } + _ => (), + } + } + + Ok(()) +} diff --git a/appmgr/src/disks.rs b/appmgr/src/disks.rs new file mode 100644 index 000000000..47efe7191 --- /dev/null +++ b/appmgr/src/disks.rs @@ -0,0 +1,230 @@ +use std::path::Path; + +use futures::future::try_join_all; + +use crate::util::Invoke; +use crate::Error; +use crate::ResultExt; + +pub const FSTAB: &'static str = "/etc/fstab"; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DiskInfo { + pub logicalname: String, + pub size: String, + pub description: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct PartitionInfo { + pub logicalname: String, + pub is_mounted: bool, + pub size: Option, + pub label: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Disk { + #[serde(flatten)] + pub info: DiskInfo, + pub partitions: Vec, +} + +pub async fn list() -> Result, Error> { + let output = tokio::process::Command::new("parted") + .arg("-lm") + .invoke("GNU Parted") + .await?; + let output_str = std::str::from_utf8(&output).no_code()?; + let disks = output_str.split("\n\n").filter_map(|s| -> Option { + let mut lines = s.split("\n"); + let has_size = lines.next()? == "BYT;"; + let disk_info_line = lines.next()?; + let mut disk_info_iter = disk_info_line.split(":"); + let logicalname = disk_info_iter.next()?.to_owned(); + let partition_prefix = if logicalname.ends_with(|c: char| c.is_digit(10)) { + logicalname.clone() + "p" + } else { + logicalname.clone() + }; + let size = disk_info_iter.next()?.to_owned(); + disk_info_iter.next()?; // transport-type + disk_info_iter.next()?; // logical-sector-size + disk_info_iter.next()?; // physical-sector-size + disk_info_iter.next()?; // partition-table-type + let description = disk_info_iter.next()?; + let description = if description.is_empty() { + None + } else { + Some(description.to_owned()) + }; + let info = DiskInfo { + logicalname, + size, + description, + }; + let partitions = lines + .filter_map(|partition_info_line| -> Option { + let mut partition_info_iter = partition_info_line.split(":"); + let partition_idx = partition_info_iter.next()?; + let logicalname = partition_prefix.clone() + partition_idx; + let size = if has_size { + partition_info_iter.next()?; // begin + partition_info_iter.next()?; // end + Some(partition_info_iter.next()?.to_owned()) + } else { + None + }; + Some(PartitionInfo { + logicalname, + is_mounted: false, + size, + label: None, + }) + }) + .collect(); + Some(Disk { info, partitions }) + }); + try_join_all(disks.map(|disk| async move { + Ok(Disk { + info: disk.info, + partitions: try_join_all(disk.partitions.into_iter().map(|mut partition| async move { + let mut blkid_command = tokio::process::Command::new("blkid"); + let (blkid_res, findmnt_status) = futures::join!( + blkid_command + .arg(&partition.logicalname) + .arg("-s") + .arg("LABEL") + .arg("-o") + .arg("value") + .invoke("BLKID"), + tokio::process::Command::new("findmnt") + .arg(&partition.logicalname) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + ); + let blkid_output = blkid_res?; + let label = std::str::from_utf8(&blkid_output).no_code()?.trim(); + if !label.is_empty() { + partition.label = Some(label.to_owned()); + } + if findmnt_status?.success() { + partition.is_mounted = true; + } + Ok::<_, Error>(partition) + })) + .await?, + }) + })) + .await +} + +pub async fn mount>(logicalname: &str, mount_point: P) -> Result<(), Error> { + let is_mountpoint = tokio::process::Command::new("mountpoint") + .arg(mount_point.as_ref()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .await?; + if is_mountpoint.success() { + unmount(mount_point.as_ref()).await?; + } + tokio::fs::create_dir_all(&mount_point).await?; + let mount_output = tokio::process::Command::new("mount") + .arg(logicalname) + .arg(mount_point.as_ref()) + .output() + .await?; + crate::ensure_code!( + mount_output.status.success(), + crate::error::FILESYSTEM_ERROR, + "Error Mounting Drive: {}", + std::str::from_utf8(&mount_output.stderr).unwrap_or("Unknown Error") + ); + Ok(()) +} + +pub async fn bind, P1: AsRef>( + src: P0, + dst: P1, + read_only: bool, +) -> Result<(), Error> { + let is_mountpoint = tokio::process::Command::new("mountpoint") + .arg(dst.as_ref()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .await?; + if is_mountpoint.success() { + unmount(dst.as_ref()).await?; + } + tokio::fs::create_dir_all(&dst).await?; + let mut mount_cmd = tokio::process::Command::new("mount"); + mount_cmd.arg("--bind"); + if read_only { + mount_cmd.arg("-o").arg("ro"); + } + let mount_output = mount_cmd + .arg(src.as_ref()) + .arg(dst.as_ref()) + .output() + .await?; + crate::ensure_code!( + mount_output.status.success(), + crate::error::FILESYSTEM_ERROR, + "Error Binding {} to {}: {}", + src.as_ref().display(), + dst.as_ref().display(), + std::str::from_utf8(&mount_output.stderr).unwrap_or("Unknown Error") + ); + Ok(()) +} + +pub async fn unmount>(mount_point: P) -> Result<(), Error> { + let umount_output = tokio::process::Command::new("umount") + .arg(mount_point.as_ref()) + .output() + .await?; + crate::ensure_code!( + umount_output.status.success(), + crate::error::FILESYSTEM_ERROR, + "Error Unmounting Drive: {}", + std::str::from_utf8(&umount_output.stderr).unwrap_or("Unknown Error") + ); + tokio::fs::remove_dir_all(mount_point.as_ref()).await?; + Ok(()) +} + +#[must_use] +pub struct MountGuard> { + path: Option

, +} +impl> MountGuard

{ + pub async fn new(logicalname: &str, mount_point: P) -> Result { + mount(logicalname, mount_point.as_ref()).await?; + Ok(Self { + path: Some(mount_point), + }) + } + pub async fn unmount(mut self) -> Result<(), Error> { + if let Some(ref path) = self.path { + unmount(path).await?; + self.path = None; + } + Ok(()) + } +} +impl> Drop for MountGuard

{ + fn drop(&mut self) { + if let Some(ref path) = self.path { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(unmount(path)) + .unwrap() + } + } +} diff --git a/appmgr/src/error.rs b/appmgr/src/error.rs new file mode 100644 index 000000000..6b3f12c1b --- /dev/null +++ b/appmgr/src/error.rs @@ -0,0 +1,107 @@ +use std::fmt::Display; + +pub const GENERAL_ERROR: i32 = 1; +pub const FILESYSTEM_ERROR: i32 = 2; +pub const DOCKER_ERROR: i32 = 3; +pub const CFG_SPEC_VIOLATION: i32 = 4; +pub const CFG_RULES_VIOLATION: i32 = 5; +pub const NOT_FOUND: i32 = 6; +pub const INVALID_BACKUP_PASSWORD: i32 = 7; +pub const VERSION_INCOMPATIBLE: i32 = 8; +pub const NETWORK_ERROR: i32 = 9; +pub const REGISTRY_ERROR: i32 = 10; +pub const SERDE_ERROR: i32 = 11; + +#[derive(Debug, Fail)] +#[fail(display = "{}", _0)] +pub struct Error { + pub failure: failure::Error, + pub code: Option, +} +impl Error { + pub fn new>(e: E, code: Option) -> Self { + Error { + failure: e.into(), + code, + } + } + pub fn from>(e: E) -> Self { + Error { + failure: e.into(), + code: None, + } + } +} +impl From for Error { + fn from(e: failure::Error) -> Self { + Error { + failure: e, + code: None, + } + } +} +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error { + failure: e.into(), + code: Some(2), + } + } +} +pub trait ResultExt +where + Self: Sized, +{ + fn with_code(self, code: i32) -> Result; + fn with_ctx (Option, D), D: Display + Send + Sync + 'static>( + self, + f: F, + ) -> Result; + fn no_code(self) -> Result; +} +impl ResultExt for Result +where + failure::Error: From, +{ + fn with_code(self, code: i32) -> Result { + #[cfg(not(feature = "production"))] + assert!(code != 0); + self.map_err(|e| Error { + failure: e.into(), + code: Some(code), + }) + } + + fn with_ctx (Option, D), D: Display + Send + Sync + 'static>( + self, + f: F, + ) -> Result { + self.map_err(|e| { + let (code, ctx) = f(&e); + let failure = failure::Error::from(e).context(ctx); + Error { + code, + failure: failure.into(), + } + }) + } + + fn no_code(self) -> Result { + self.map_err(|e| Error { + failure: e.into(), + code: None, + }) + } +} + +#[macro_export] +macro_rules! ensure_code { + ($x:expr, $c:expr, $fmt:expr $(, $arg:expr)*) => { + if !($x) { + return Err(crate::Error { + failure: format_err!($fmt, $($arg, )*), + code: Some($c), + }); + } + }; +} diff --git a/appmgr/src/index.rs b/appmgr/src/index.rs new file mode 100644 index 000000000..95e45e16f --- /dev/null +++ b/appmgr/src/index.rs @@ -0,0 +1,130 @@ +use std::cmp::Ord; +use std::ffi::OsStr; +use std::iter::FromIterator; +use std::path::Path; + +use emver::{Version, VersionRange}; +use futures::future::{BoxFuture, FutureExt}; +use linear_map::LinearMap; + +use crate::inspect::info_full; +use crate::manifest::{Description, ManifestLatest}; +use crate::{Error, ResultExt}; + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct AppIndex(pub LinearMap); + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct IndexInfo { + pub title: String, + pub description: Description, + pub version_info: Vec, + pub icon_type: String, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct VersionInfo { + pub version: Version, + pub release_notes: String, + pub os_version_required: VersionRange, + pub os_version_recommended: VersionRange, +} + +const NULL_VERSION: Version = Version::new(0, 0, 0, 0); + +impl AppIndex { + fn add(&mut self, manifest: ManifestLatest) { + if let Some(ref mut entry) = self.0.get_mut(&manifest.id) { + if entry + .version_info + .get(0) + .map(|i| &i.version) + .unwrap_or(&NULL_VERSION) + <= &manifest.version + { + entry.title = manifest.title; + entry.description = manifest.description; + } + entry.version_info.push(VersionInfo { + version: manifest.version, + release_notes: manifest.release_notes, + os_version_required: manifest.os_version_required, + os_version_recommended: manifest.os_version_recommended, + }); + entry + .version_info + .sort_unstable_by(|a, b| b.version.cmp(&a.version)); + entry.version_info.dedup_by(|a, b| a.version == b.version); + } else { + self.0.insert( + manifest.id, + IndexInfo { + title: manifest.title, + description: manifest.description, + version_info: vec![VersionInfo { + version: manifest.version, + release_notes: manifest.release_notes, + os_version_required: manifest.os_version_required, + os_version_recommended: manifest.os_version_recommended, + }], + icon_type: "png".to_owned(), // TODO + }, + ); + } + } +} + +impl Extend for AppIndex { + fn extend>(&mut self, iter: I) { + for manifest in iter { + self.add(manifest); + } + } +} + +impl FromIterator for AppIndex { + fn from_iter>(iter: I) -> Self { + let mut res = Self::default(); + res.extend(iter); + res + } +} + +pub async fn index>(dir: P) -> Result { + let dir_path = dir.as_ref(); + let mut idx = AppIndex::default(); + fn index_rec<'a, P: AsRef + Send + Sync + 'a>( + idx: &'a mut AppIndex, + dir: P, + ) -> BoxFuture<'a, Result<(), Error>> { + async move { + let dir_path = dir.as_ref(); + if let Ok(_) = tokio::fs::metadata(dir_path.join(".ignore")).await { + log::info!("Skipping {}", dir_path.display()); + return Ok(()); + } + let mut entry_stream = tokio::fs::read_dir(dir_path).await?; + while let Some(entry) = entry_stream.next_entry().await? { + let path = entry.path(); + let metadata = entry.metadata().await?; + if metadata.is_file() { + let ext = path.extension(); + if ext == Some(OsStr::new("s9pk")) { + let info = info_full(&path, true, false) + .await + .with_ctx(|e| (e.code.clone(), format!("{}: {}", path.display(), e)))?; + idx.add(info.manifest.unwrap()); + } + } else if metadata.is_dir() { + index_rec(idx, &path).await?; + } + } + Ok(()) + } + .boxed() + } + index_rec(&mut idx, dir_path).await?; + Ok(idx) +} diff --git a/appmgr/src/inspect.rs b/appmgr/src/inspect.rs new file mode 100644 index 000000000..6ef9c5b63 --- /dev/null +++ b/appmgr/src/inspect.rs @@ -0,0 +1,195 @@ +use std::path::Path; + +use failure::ResultExt as _; +use futures::stream::StreamExt; +use tokio_tar as tar; + +use crate::config::{ConfigRuleEntry, ConfigSpec}; +use crate::manifest::{Manifest, ManifestLatest}; +use crate::util::from_cbor_async_reader; +use crate::version::VersionT; +use crate::Error; +use crate::ResultExt as _; + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppInfoFull { + #[serde(flatten)] + pub info: AppInfo, + #[serde(skip_serializing_if = "Option::is_none")] + pub manifest: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub config: Option, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppInfo { + pub title: String, + pub version: emver::Version, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppConfig { + pub spec: ConfigSpec, + pub rules: Vec, +} + +pub async fn info_full>( + path: P, + with_manifest: bool, + with_config: bool, +) -> Result { + let p = path.as_ref(); + log::info!("Opening file."); + let r = tokio::fs::File::open(p) + .await + .with_context(|e| format!("{}: {}", p.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + log::info!("Extracting archive."); + let mut pkg = tar::Archive::new(r); + let mut entries = pkg.entries()?; + log::info!("Opening manifest from archive."); + let manifest = entries + .next() + .await + .ok_or(crate::install::Error::CorruptedPkgFile("missing manifest")) + .no_code()??; + crate::ensure_code!( + manifest.path()?.to_str() == Some("manifest.cbor"), + crate::error::GENERAL_ERROR, + "Package File Invalid or Corrupted" + ); + log::trace!("Deserializing manifest."); + let manifest: Manifest = from_cbor_async_reader(manifest).await?; + let manifest = manifest.into_latest(); + crate::ensure_code!( + crate::version::Current::new() + .semver() + .satisfies(&manifest.os_version_required), + crate::error::VERSION_INCOMPATIBLE, + "AppMgr Version Not Compatible: needs {}", + manifest.os_version_required + ); + Ok(AppInfoFull { + info: AppInfo { + title: manifest.title.clone(), + version: manifest.version.clone(), + }, + manifest: if with_manifest { Some(manifest) } else { None }, + config: if with_config { + log::info!("Opening config spec from archive."); + let spec = entries + .next() + .await + .ok_or(crate::install::Error::CorruptedPkgFile( + "missing config spec", + )) + .no_code()??; + crate::ensure_code!( + spec.path()?.to_str() == Some("config_spec.cbor"), + crate::error::GENERAL_ERROR, + "Package File Invalid or Corrupted" + ); + log::trace!("Deserializing config spec."); + let spec = from_cbor_async_reader(spec).await?; + log::info!("Opening config rules from archive."); + let rules = entries + .next() + .await + .ok_or(crate::install::Error::CorruptedPkgFile( + "missing config rules", + )) + .no_code()??; + crate::ensure_code!( + rules.path()?.to_str() == Some("config_rules.cbor"), + crate::error::GENERAL_ERROR, + "Package File Invalid or Corrupted" + ); + log::trace!("Deserializing config rules."); + let rules = from_cbor_async_reader(rules).await?; + Some(AppConfig { spec, rules }) + } else { + None + }, + }) +} + +pub async fn print_instructions>(path: P) -> Result<(), Error> { + let p = path.as_ref(); + log::info!("Opening file."); + let r = tokio::fs::File::open(p) + .await + .with_context(|e| format!("{}: {}", p.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + log::info!("Extracting archive."); + let mut pkg = tar::Archive::new(r); + let mut entries = pkg.entries()?; + log::info!("Opening manifest from archive."); + let manifest = entries + .next() + .await + .ok_or(crate::install::Error::CorruptedPkgFile("missing manifest")) + .no_code()??; + crate::ensure_code!( + manifest.path()?.to_str() == Some("manifest.cbor"), + crate::error::GENERAL_ERROR, + "Package File Invalid or Corrupted" + ); + log::trace!("Deserializing manifest."); + let manifest: Manifest = from_cbor_async_reader(manifest).await?; + let manifest = manifest.into_latest(); + crate::ensure_code!( + crate::version::Current::new() + .semver() + .satisfies(&manifest.os_version_required), + crate::error::VERSION_INCOMPATIBLE, + "AppMgr Version Not Compatible: needs {}", + manifest.os_version_required + ); + entries + .next() + .await + .ok_or(crate::install::Error::CorruptedPkgFile( + "missing config spec", + )) + .no_code()??; + entries + .next() + .await + .ok_or(crate::install::Error::CorruptedPkgFile( + "missing config rules", + )) + .no_code()??; + + if manifest.has_instructions { + use tokio::io::AsyncWriteExt; + + let mut instructions = entries + .next() + .await + .ok_or(crate::install::Error::CorruptedPkgFile( + "missing instructions", + )) + .no_code()??; + + let mut stdout = tokio::io::stdout(); + tokio::io::copy(&mut instructions, &mut stdout) + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + stdout + .flush() + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + stdout + .shutdown() + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + } else { + return Err(failure::format_err!("No instructions for {}", p.display())) + .with_code(crate::error::NOT_FOUND); + } + + Ok(()) +} diff --git a/appmgr/src/install.rs b/appmgr/src/install.rs new file mode 100644 index 000000000..bdeb72550 --- /dev/null +++ b/appmgr/src/install.rs @@ -0,0 +1,564 @@ +use std::borrow::Cow; +use std::ffi::{OsStr, OsString}; +use std::marker::Unpin; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::sync::{ + atomic::{self, AtomicBool, AtomicU64}, + Arc, +}; +use std::task::Context; +use std::task::Poll; +use std::time::Duration; + +use failure::ResultExt as _; +use futures::stream::StreamExt; +use futures::stream::TryStreamExt; +use tokio::io::AsyncRead; +use tokio::io::AsyncWriteExt; +use tokio_tar as tar; + +use crate::config::{ConfigRuleEntry, ConfigSpec}; +use crate::manifest::{ImageConfig, Manifest, ManifestV0}; +use crate::util::{from_cbor_async_reader, to_yaml_async_writer, AsyncCompat, PersistencePath}; +use crate::version::VersionT; +use crate::ResultExt as _; + +#[derive(Fail, Debug, Clone)] +pub enum Error { + #[fail(display = "Package File Invalid or Corrupted: {}", _0)] + CorruptedPkgFile(&'static str), + #[fail(display = "Invalid File Name")] + InvalidFileName, +} + +pub async fn install_name(name_version: &str, use_cache: bool) -> Result<(), crate::Error> { + let name = name_version.split("@").next().unwrap(); + let tmp_path = Path::new(crate::TMP_DIR).join(format!("{}.s9pk", name)); + if !use_cache || !tmp_path.exists() { + download_name(name_version).await?; + } + install_path( + &tmp_path + .as_os_str() + .to_str() + .ok_or(Error::InvalidFileName) + .with_code(crate::error::FILESYSTEM_ERROR)?, + Some(name), + ) + .await?; + tokio::fs::remove_file(&tmp_path) + .await + .with_context(|e| format!("{}: {}", tmp_path.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + Ok(()) +} + +struct CountingReader(pub R, pub Arc); +impl AsyncRead for CountingReader +where + R: AsyncRead, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let atomic = self.as_ref().1.clone(); // TODO: not efficient + match unsafe { self.map_unchecked_mut(|a| &mut a.0) }.poll_read(cx, buf) { + Poll::Ready(Ok(res)) => { + atomic.fetch_add(res as u64, atomic::Ordering::SeqCst); + Poll::Ready(Ok(res)) + } + a => a, + } + } +} + +pub async fn download_name(name_version: &str) -> Result { + let mut split = name_version.split("@"); + let name = split.next().unwrap(); + let req: Option = split.next().map(|a| a.parse()).transpose().no_code()?; + if let Some(req) = req { + download( + &format!("{}/{}.s9pk?spec={}", &*crate::APP_REGISTRY_URL, name, req), + Some(name), + ) + .await + } else { + download( + &format!("{}/{}.s9pk", &*crate::APP_REGISTRY_URL, name), + Some(name), + ) + .await + } +} + +pub async fn download(url: &str, name: Option<&str>) -> Result { + let url = reqwest::Url::parse(url).no_code()?; + log::info!("Downloading {}.", url.as_str()); + let response = reqwest::get(url) + .await + .with_code(crate::error::NETWORK_ERROR)? + .error_for_status() + .with_code(crate::error::REGISTRY_ERROR)?; + tokio::fs::create_dir_all(crate::TMP_DIR).await?; + let tmp_file_path = + Path::new(crate::TMP_DIR).join(&format!("{}.s9pk", name.unwrap_or("download"))); + let mut f = tokio::fs::File::create(&tmp_file_path).await?; + let len: Option = response.content_length().map(|a| { + log::info!("{}KiB to download.", a / 1024); + a + }); + let done = Arc::new(AtomicBool::new(false)); + let counter = Arc::new(AtomicU64::new(0)); + let mut reader = CountingReader( + AsyncCompat( + response + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .into_async_read(), + ), + counter.clone(), + ); + let done_handle = done.clone(); + let download_handle = tokio::spawn(async move { + let res = tokio::io::copy(&mut reader, &mut f).await; + done_handle.store(true, atomic::Ordering::SeqCst); + res + }); + let poll_handle = tokio::spawn(async move { + loop { + let is_done = done.load(atomic::Ordering::SeqCst); + let downloaded_bytes = counter.load(atomic::Ordering::SeqCst); + if !*crate::QUIET.read().await { + if let Some(len) = len { + print!("\rDownloading... {}%", downloaded_bytes * 100 / len); + } else { + print!("\rDownloading... {}KiB", downloaded_bytes / 1024); + } + } + if is_done { + break; + } + tokio::time::delay_for(Duration::from_millis(10)).await; + } + if !*crate::QUIET.read().await { + println!("\rDownloading... 100%"); + } + }); + download_handle.await.unwrap()?; + poll_handle.await.unwrap(); + Ok(tmp_file_path) +} + +pub async fn install_url(url: &str, name: Option<&str>) -> Result<(), crate::Error> { + let tmp_file_path = download(url, name).await?; + install_path(&tmp_file_path, name).await?; + tokio::fs::remove_file(&tmp_file_path) + .await + .with_context(|e| format!("{}: {}", tmp_file_path.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + Ok(()) +} + +pub async fn install_path>(p: P, name: Option<&str>) -> Result<(), crate::Error> { + let path = p.as_ref(); + log::info!( + "Starting install of {}.", + path.file_name() + .and_then(|a| a.to_str()) + .ok_or(Error::InvalidFileName) + .no_code()? + ); + let file = tokio::fs::File::open(&path) + .await + .with_context(|e| format!("{}: {}", path.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + let len = file.metadata().await?.len(); + let done = Arc::new(AtomicBool::new(false)); + let counter = Arc::new(AtomicU64::new(0)); + let done_handle = done.clone(); + let name_clone = name.map(|a| a.to_owned()); + let counter_clone = counter.clone(); + let poll_handle = tokio::spawn(async move { + loop { + let is_done = done.load(atomic::Ordering::SeqCst); + let installed_bytes = counter.load(atomic::Ordering::SeqCst); + if !*crate::QUIET.read().await { + print!("\rInstalling... {}%", installed_bytes * 100 / len); + } + if is_done { + break; + } + tokio::time::delay_for(Duration::from_millis(10)).await; + } + if !*crate::QUIET.read().await { + println!("\rInstalling... 100%"); + } + }); + let reader = CountingReader(file, counter_clone); + let res = install(reader, name_clone.as_ref().map(|a| a.as_str())).await; + done_handle.store(true, atomic::Ordering::SeqCst); + res?; + poll_handle.await.unwrap(); + if !*crate::QUIET.read().await { + println!("Complete."); + } + Ok(()) +} + +pub async fn install( + r: R, + name: Option<&str>, +) -> Result<(), crate::Error> { + log::info!("Extracting archive."); + let mut pkg = tar::Archive::new(r); + let mut entries = pkg.entries()?; + log::info!("Opening manifest from archive."); + let manifest = entries + .next() + .await + .ok_or(Error::CorruptedPkgFile("missing manifest")) + .no_code()??; + crate::ensure_code!( + manifest.path()?.to_str() == Some("manifest.cbor"), + crate::error::GENERAL_ERROR, + "Package File Invalid or Corrupted" + ); + log::trace!("Deserializing manifest."); + let manifest: Manifest = from_cbor_async_reader(manifest).await.no_code()?; + match manifest { + Manifest::V0(m) => install_v0(m, entries, name).await?, + }; + Ok(()) +} + +pub async fn install_v0( + manifest: ManifestV0, + mut entries: tar::Entries, + name: Option<&str>, +) -> Result<(), crate::Error> { + crate::ensure_code!( + crate::version::Current::new() + .semver() + .satisfies(&manifest.os_version_required), + crate::error::VERSION_INCOMPATIBLE, + "OS Version Not Compatible: need {}", + manifest.os_version_required + ); + if let Some(name) = name { + crate::ensure_code!( + manifest.id == name, + crate::error::GENERAL_ERROR, + "Package Name Does Not Match Expected" + ); + } + let (ip, tor_addr, tor_key) = crate::tor::set_svc( + &manifest.id, + crate::tor::NewService { + ports: manifest.ports.clone(), + hidden_service_version: manifest.hidden_service_version, + }, + ) + .await?; + + let recoverable = Path::new(crate::VOLUMES).join(&manifest.id).exists(); + + log::info!("Creating volume {}/{}.", crate::VOLUMES, manifest.id); + tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id)).await?; + + let app_dir = PersistencePath::from_ref("apps").join(&manifest.id); + let app_dir_path = app_dir.path(); + if app_dir_path.exists() { + tokio::fs::remove_dir_all(&app_dir_path).await?; + } + tokio::fs::create_dir_all(&app_dir_path).await?; + let _lock = app_dir.lock(true).await?; + log::info!("Saving manifest."); + let mut manifest_out = app_dir.join("manifest.yaml").write(None).await?; + to_yaml_async_writer(&mut *manifest_out, &Manifest::V0(manifest.clone())).await?; + manifest_out.commit().await?; + log::info!("Opening config spec from archive."); + let config_spec = entries + .next() + .await + .ok_or(Error::CorruptedPkgFile("missing config spec")) + .no_code()??; + crate::ensure_code!( + config_spec.path()?.to_str() == Some("config_spec.cbor"), + crate::error::GENERAL_ERROR, + "Package File Invalid or Corrupted" + ); + log::trace!("Deserializing config spec."); + let config_spec: ConfigSpec = from_cbor_async_reader(config_spec).await?; + log::info!("Saving config spec."); + let mut config_spec_out = app_dir.join("config_spec.yaml").write(None).await?; + to_yaml_async_writer(&mut *config_spec_out, &config_spec).await?; + config_spec_out.commit().await?; + log::info!("Opening config rules from archive."); + let config_rules = entries + .next() + .await + .ok_or(Error::CorruptedPkgFile("missing config rules")) + .no_code()??; + crate::ensure_code!( + config_rules.path()?.to_str() == Some("config_rules.cbor"), + crate::error::GENERAL_ERROR, + "Package File Invalid or Corrupted" + ); + log::trace!("Deserializing config rules."); + let config_rules: Vec = from_cbor_async_reader(config_rules).await?; + log::info!("Saving config rules."); + let mut config_rules_out = app_dir.join("config_rules.yaml").write(None).await?; + to_yaml_async_writer(&mut *config_rules_out, &config_rules).await?; + config_rules_out.commit().await?; + if manifest.has_instructions { + log::info!("Opening instructions from archive."); + let mut instructions = entries + .next() + .await + .ok_or(Error::CorruptedPkgFile("missing config rules")) + .no_code()??; + crate::ensure_code!( + instructions.path()?.to_str() == Some("instructions.md"), + crate::error::GENERAL_ERROR, + "Package File Invalid or Corrupted" + ); + log::info!("Saving instructions."); + let mut instructions_out = app_dir.join("instructions.md").write(None).await?; + tokio::io::copy(&mut instructions, &mut *instructions_out) + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + instructions_out.commit().await?; + } + + log::info!("Copying over assets."); + for asset in manifest.assets.iter() { + let dst_path = Path::new(crate::VOLUMES) + .join(&manifest.id) + .join(&asset.dst); + log::info!("Copying {} to {}", asset.src.display(), dst_path.display()); + let src_path = Path::new(&asset.src); + log::info!("Opening {} from archive.", src_path.display()); + let mut src = entries + .next() + .await + .ok_or(Error::CorruptedPkgFile("missing asset")) + .no_code()??; + crate::ensure_code!( + src.path()? == src_path, + crate::error::GENERAL_ERROR, + "Package File Invalid or Corrupted" + ); + let dst_path_file = dst_path.join(src_path); + if dst_path_file.exists() && !asset.overwrite { + log::info!("{} already exists, skipping.", dst_path_file.display()); + } else { + if dst_path_file.exists() { + if dst_path_file.is_dir() { + tokio::fs::remove_dir_all(&dst_path_file) + .await + .with_context(|e| format!("{}: {}", dst_path_file.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + } else { + tokio::fs::remove_file(&dst_path_file) + .await + .with_context(|e| format!("{}: {}", dst_path_file.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + } + } + src.unpack_in(&dst_path).await?; + if src.header().entry_type().is_dir() { + loop { + let mut file = entries + .next() + .await + .ok_or(Error::CorruptedPkgFile("missing asset")) + .no_code()??; + if file + .path()? + .starts_with(format!("APPMGR_DIR_END:{}", asset.src.display())) + { + break; + } else { + file.unpack_in(&dst_path).await?; + } + } + } + } + } + + let tag = match &manifest.image { + ImageConfig::Tar => { + let image_name = format!("start9/{}", manifest.id); + let tag = format!("{}:latest", image_name); + if tokio::process::Command::new("docker") + .arg("images") + .arg("-q") + .arg(&image_name) + .output() + .await? + .stdout + .len() + > 0 + { + tokio::process::Command::new("docker") + .arg("stop") + .arg(&manifest.id) + .spawn()? + .await?; + tokio::process::Command::new("docker") + .arg("rm") + .arg(&manifest.id) + .spawn()? + .await?; + crate::ensure_code!( + tokio::process::Command::new("docker") + .arg("rmi") + .arg(&image_name) + .output() + .await? + .status + .success(), + crate::error::DOCKER_ERROR, + "Failed to Remove Existing Image" + ) + } + log::info!("Opening image.tar from archive."); + let mut image = entries + .next() + .await + .ok_or(Error::CorruptedPkgFile("missing image.tar")) + .no_code()??; + let image_path = image.path()?; + if image_path != Path::new("image.tar") { + return Err(crate::Error::from(format_err!( + "Package File Invalid or Corrupted: expected image.tar, got {}", + image_path.display() + ))); + } + log::info!( + "Loading docker image start9/{} from image.tar.", + manifest.id + ); + let mut child = tokio::process::Command::new("docker") + .arg("load") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::inherit()) + .stderr(match log::max_level() { + log::LevelFilter::Error => std::process::Stdio::null(), + _ => std::process::Stdio::inherit(), + }) + .spawn()?; + let mut child_in = child.stdin.take().unwrap(); + tokio::io::copy(&mut image, &mut child_in).await?; + child_in.flush().await?; + child_in.shutdown().await?; + drop(child_in); + crate::ensure_code!( + child.await?.success(), + crate::error::DOCKER_ERROR, + "Failed to Load Docker Image From Tar" + ); + tag + } + }; + log::info!("Creating docker container: {} from {}.", manifest.id, tag); + let volume_arg = format!( + "type=bind,src={}/{},dst={}", + crate::VOLUMES, + manifest.id, + manifest.mount.display() + ); + let mut args = vec![ + Cow::Borrowed(OsStr::new("create")), + Cow::Borrowed(OsStr::new("--restart")), + Cow::Borrowed(OsStr::new("on-failure")), + Cow::Borrowed(OsStr::new("--name")), + Cow::Borrowed(OsStr::new(&manifest.id)), + Cow::Borrowed(OsStr::new("--mount")), + Cow::Borrowed(OsStr::new(&volume_arg)), + Cow::Borrowed(OsStr::new("--net")), + Cow::Borrowed(OsStr::new("start9")), + Cow::Borrowed(OsStr::new("--ip")), + Cow::Owned(OsString::from(format!("{}", ip))), + ]; + if let (Some(ref tor_addr), Some(ref tor_key)) = (&tor_addr, &tor_key) { + args.extend( + std::iter::empty() + .chain(std::iter::once(Cow::Borrowed(OsStr::new("--env")))) + .chain(std::iter::once(Cow::Owned(OsString::from(format!( + "TOR_ADDRESS={}", + tor_addr + ))))) + .chain(std::iter::once(Cow::Borrowed(OsStr::new("--env")))) + .chain(std::iter::once(Cow::Owned(OsString::from(format!( + "TOR_KEY={}", + tor_key + ))))), + ); + } + if let Some(shm_size_mb) = manifest.shm_size_mb { + args.push(Cow::Borrowed(OsStr::new("--shm-size"))); + args.push(Cow::Owned(OsString::from(format!("{}m", shm_size_mb)))); + } + args.push(Cow::Borrowed(OsStr::new(&tag))); + crate::ensure_code!( + std::process::Command::new("docker") + .args(&args) + .stdout(std::process::Stdio::null()) + .stderr(match log::max_level() { + log::LevelFilter::Error => std::process::Stdio::null(), + _ => std::process::Stdio::inherit(), + }) + .status()? + .success(), + crate::error::DOCKER_ERROR, + "Failed to Create Docker Container" + ); + tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id).join("start9")).await?; + if let Some(public) = manifest.public { + tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id).join(public)) + .await?; + } + if let Some(shared) = manifest.shared { + tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id).join(shared)) + .await?; + } + log::info!("Updating app list."); + crate::apps::add( + &manifest.id, + crate::apps::AppInfo { + title: manifest.title.clone(), + version: manifest.version.clone(), + tor_address: tor_addr.clone(), + configured: false, + recoverable, + needs_restart: false, + }, + ) + .await?; + let config = crate::apps::config(&manifest.id).await?; + if let Some(cfg) = config.config { + if config.spec.matches(&cfg).is_ok() { + crate::apps::set_configured(&manifest.id, true).await?; + } + } else { + let empty_config = crate::config::Config::default(); + if config.spec.matches(&empty_config).is_ok() { + crate::config::configure(&manifest.id, Some(empty_config), None, false).await?; + } + } + for (dep_id, dep_info) in manifest.dependencies.0 { + if dep_info.mount_shared + && crate::apps::list_info().await?.get(&dep_id).is_some() + && crate::apps::manifest(&dep_id).await?.shared.is_some() + && crate::apps::status(&dep_id).await?.status != crate::apps::DockerStatus::Stopped + { + crate::apps::set_needs_restart(&dep_id, true).await?; + } + } + + Ok(()) +} diff --git a/appmgr/src/lib.rs b/appmgr/src/lib.rs new file mode 100644 index 000000000..a18d01df0 --- /dev/null +++ b/appmgr/src/lib.rs @@ -0,0 +1,51 @@ +#[macro_use] +extern crate failure; +#[macro_use] +extern crate pest_derive; + +pub const TOR_RC: &'static str = "/root/appmgr/tor/torrc"; +pub const SERVICES_YAML: &'static str = "tor/services.yaml"; +pub const VOLUMES: &'static str = "/root/volumes"; +pub const PERSISTENCE_DIR: &'static str = "/root/appmgr"; +pub const TMP_DIR: &'static str = "/root/tmp/appmgr"; +pub const BACKUP_MOUNT_POINT: &'static str = "/mnt/backup_drive"; +pub const BACKUP_DIR: &'static str = "Embassy Backups"; +pub const BUFFER_SIZE: usize = 1024; +pub const HOST_IP: [u8; 4] = [172, 18, 0, 1]; + +lazy_static::lazy_static! { + pub static ref REGISTRY_URL: String = std::env::var("REGISTRY_URL").unwrap_or_else(|_| "https://registry.start9labs.com".to_owned()); + pub static ref SYS_REGISTRY_URL: String = format!("{}/sys", *REGISTRY_URL); + pub static ref APP_REGISTRY_URL: String = format!("{}/apps", *REGISTRY_URL); + pub static ref QUIET: tokio::sync::RwLock = tokio::sync::RwLock::new(!std::env::var("APPMGR_QUIET").map(|a| a == "0").unwrap_or(true)); +} + +pub mod apps; +pub mod backup; +pub mod config; +pub mod control; +pub mod dependencies; +pub mod disks; +pub mod error; +pub mod index; +pub mod inspect; +pub mod install; +pub mod logs; +pub mod manifest; +pub mod pack; +pub mod registry; +pub mod remove; +pub mod tor; +pub mod update; +pub mod util; +pub mod version; + +pub use config::{configure, Config}; +pub use control::{restart_app, start_app, stop_app, stop_dependents}; +pub use error::{Error, ResultExt}; +pub use install::{install_name, install_path, install_url}; +pub use logs::{logs, notifications, stats, LogOptions}; +pub use pack::{pack, verify}; +pub use remove::remove; +pub use update::update; +pub use version::{init, self_update}; diff --git a/appmgr/src/logs.rs b/appmgr/src/logs.rs new file mode 100644 index 000000000..da6cf5b63 --- /dev/null +++ b/appmgr/src/logs.rs @@ -0,0 +1,199 @@ +use std::borrow::Cow; +use std::ffi::{OsStr, OsString}; +use std::path::Path; + +use failure::ResultExt as _; +use futures::stream::StreamExt; +use futures::stream::TryStreamExt; +use itertools::Itertools; + +use crate::util::PersistencePath; +use crate::Error; +use crate::ResultExt as _; + +#[derive(Clone, Copy, Debug, serde::Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Level { + Error, + Warn, + Success, + Info, +} +impl std::fmt::Display for Level { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Level::Error => write!(f, "ERROR"), + Level::Warn => write!(f, "WARN"), + Level::Success => write!(f, "SUCCESS"), + Level::Info => write!(f, "INFO"), + } + } +} +impl std::str::FromStr for Level { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "ERROR" => Ok(Level::Error), + "WARN" => Ok(Level::Warn), + "SUCCESS" => Ok(Level::Success), + "INFO" => Ok(Level::Info), + _ => Err(Error::from(format_err!("Unknown Notification Level"))), + } + } +} + +#[derive(Clone, Debug, serde::Serialize)] +pub struct Notification { + pub time: i64, + pub level: Level, + pub code: usize, + pub title: String, + pub message: String, +} +impl std::fmt::Display for Notification { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}:{}:{}:{}", + self.level, + self.code, + self.title.replace(":", "\u{A789}"), + self.message.replace("\n", "\u{2026}") + ) + } +} +impl std::str::FromStr for Notification { + type Err = Error; + fn from_str(s: &str) -> Result { + let mut split = s.split(":"); + Ok(Notification { + time: split + .next() + .ok_or_else(|| format_err!("missing time"))? + .parse::() + .map(|a| a as i64) + .no_code()?, + level: split + .next() + .ok_or_else(|| format_err!("missing level"))? + .parse()?, + code: split + .next() + .ok_or_else(|| format_err!("missing code"))? + .parse() + .no_code()?, + title: split + .next() + .ok_or_else(|| format_err!("missing title"))? + .replace("\u{A789}", ":"), + message: split + .intersperse(":") + .collect::() + .replace("\u{2026}", "\n"), + }) + } +} + +pub struct LogOptions, B: AsRef> { + pub details: bool, + pub follow: bool, + pub since: Option, + pub until: Option, + pub tail: Option, + pub timestamps: bool, +} + +pub async fn logs, B: AsRef>( + name: &str, + options: LogOptions, +) -> Result<(), Error> { + let mut args = vec![Cow::Borrowed(OsStr::new("logs"))]; + if options.details { + args.push(Cow::Borrowed(OsStr::new("--details"))); + } + if options.follow { + args.push(Cow::Borrowed(OsStr::new("-f"))); + } + if let Some(since) = options.since.as_ref() { + args.push(Cow::Borrowed(OsStr::new("--since"))); + args.push(Cow::Borrowed(OsStr::new(since.as_ref()))); + } + if let Some(until) = options.until.as_ref() { + args.push(Cow::Borrowed(OsStr::new("--until"))); + args.push(Cow::Borrowed(OsStr::new(until.as_ref()))); + } + if let Some(tail) = options.tail { + args.push(Cow::Borrowed(OsStr::new("--tail"))); + args.push(Cow::Owned(OsString::from(format!("{}", tail)))); + } + if options.timestamps { + args.push(Cow::Borrowed(OsStr::new("-t"))); + } + args.push(Cow::Borrowed(OsStr::new(name))); + crate::ensure_code!( + std::process::Command::new("docker") + .args(args.into_iter()) + .status()? + .success(), + crate::error::DOCKER_ERROR, + "Failed to Collect Logs from Docker" + ); + Ok(()) +} + +pub async fn notifications(id: &str) -> Result, Error> { + let p = PersistencePath::from_ref("notifications").join(id).tmp(); + if let Some(parent) = p.parent() { + if !parent.exists() { + tokio::fs::create_dir_all(parent).await?; + } + } + match tokio::fs::rename( + Path::new(crate::VOLUMES) + .join(id) + .join("start9") + .join("notifications.log"), + &p, + ) + .await + { + Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + a => a, + }?; + let f = tokio::fs::File::open(&p) + .await + .with_context(|e| format!("{}: {}", p.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + tokio::io::AsyncBufReadExt::lines(tokio::io::BufReader::new(f)) + .map(|a| a.map_err(From::from).and_then(|a| a.parse())) + .try_collect() + .await +} + +pub async fn stats(id: &str) -> Result { + let p = PersistencePath::from_ref("stats").join(id).tmp(); + if let Some(parent) = p.parent() { + if !parent.exists() { + tokio::fs::create_dir_all(parent).await?; + } + } + match tokio::fs::copy( + Path::new(crate::VOLUMES) + .join(id) + .join("start9") + .join("stats.yaml"), + &p, + ) + .await + { + Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => { + return Ok(serde_yaml::Value::Null) + } + a => a, + }?; + let f = tokio::fs::File::open(&p) + .await + .with_context(|e| format!("{}: {}", p.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + crate::util::from_yaml_async_reader(f).await.no_code() +} diff --git a/appmgr/src/main.rs b/appmgr/src/main.rs new file mode 100644 index 000000000..41226d7b6 --- /dev/null +++ b/appmgr/src/main.rs @@ -0,0 +1,1645 @@ +#![type_length_limit = "10000000"] + +use std::borrow::Cow; +use std::path::Path; + +use appmgrlib::version::VersionT; +use appmgrlib::*; + +use clap::{App, Arg, SubCommand}; + +#[tokio::main] +async fn main() { + match inner_main().await { + Ok(()) => (), + Err(e) => { + eprintln!("{}", e.failure); + log::warn!("{:?}", e.failure); + std::process::exit(e.code.unwrap_or(1)); + } + } +} + +async fn inner_main() -> Result<(), Error> { + simple_logging::log_to_stderr(log::LevelFilter::Info); + #[cfg(not(feature = "portable"))] + { + if !Path::new(crate::PERSISTENCE_DIR).join(".lock").exists() { + tokio::fs::create_dir_all(crate::PERSISTENCE_DIR).await?; + tokio::fs::File::create(Path::new(crate::PERSISTENCE_DIR).join(".lock")).await?; + } + } + let q = *QUIET.read().await; + *QUIET.write().await = true; + #[cfg(not(feature = "portable"))] + init().await?; + *QUIET.write().await = q; + let version = format!("{}", crate::version::Current::new().semver()); + let git_version = + git_version::git_version!(args = ["--always", "--abbrev=40", "--dirty=-modified"]); + #[allow(unused_mut)] + let mut app = App::new("Start9 Application Manager") + .version(version.as_str()) + .author("Dr. BoneZ ") + .about("Manage applications installed on the Start9 Embassy") + .arg( + Arg::with_name("verbosity") + .short("v") + .help("Sets verbosity level") + .multiple(true), + ) + .subcommand(SubCommand::with_name("semver").about("Prints semantic version and exits")) + .subcommand(SubCommand::with_name("git-info").about("Prints git version info and exits")) + .subcommand( + SubCommand::with_name("pack") + .about("Creates a new application package") + .arg( + Arg::with_name("output") + .short("o") + .long("output") + .takes_value(true) + .default_value("app.s9pk"), + ) + .arg( + Arg::with_name("PATH") + .help("Path to the folder containing the application data") + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("verify") + .about("Verifies an application package") + .arg( + Arg::with_name("PATH") + .help("Path to the s9pk file to verify") + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("inspect") + .about("Inspects an application package") + .subcommand( + SubCommand::with_name("info") + .about("Prints information about an app") + .arg( + Arg::with_name("PATH") + .help("Path to the s9pk file to inspect") + .required(true), + ) + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .required_unless("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .required_unless("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ) + .arg( + Arg::with_name("include-manifest") + .long("include-manifest") + .short("m"), + ) + .arg( + Arg::with_name("include-config") + .long("include-config") + .short("c"), + ) + .arg( + Arg::with_name("only-manifest") + .long("only-manifest") + .short("M") + .conflicts_with_all(&[ + "include-manifest", + "include-config", + "only-config", + ]), + ) + .arg( + Arg::with_name("only-config") + .long("only-config") + .short("C") + .conflicts_with_all(&[ + "include-manifest", + "include-config", + "only-manifest", + ]), + ), + ) + .subcommand( + SubCommand::with_name("instructions") + .about("Prints instructions for an app") + .arg( + Arg::with_name("PATH") + .help("Path to the s9pk file to inspect") + .required(true), + ), + ), + ) + .subcommand( + SubCommand::with_name("index") + .about("Indexes all s9pk files in a directory") + .arg( + Arg::with_name("DIR") + .help("Path to the directory to index") + .required(true), + ), + ); + + #[cfg(not(feature = "portable"))] + let mut app = app + .subcommand( + SubCommand::with_name("install") + .about("Installs a new app") + .arg( + Arg::with_name("no-cache") + .long("no-cache") + .help("Replace cached download of application"), + ) + .arg( + Arg::with_name("ID|PATH|URL") + .help("The app to install") + .long_help(concat!( + "The app to install\n", + "ID: The id of the app in the Start9 registry\n", + "PATH: The path to the s9pk file on your local file system\n", + "URL: The url of the s9pk file" + )) + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("update") + .about("Updates an app") + .arg( + Arg::with_name("ID") + .help("The id of the app in the Start9 registry") + .required(true), + ) + .arg( + Arg::with_name("dry-run") + .long("dry-run") + .help("Do not commit result"), + ) + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ), + ) + .subcommand( + SubCommand::with_name("start") + .about("Starts an app") + .arg(Arg::with_name("ID").help("The app to start").required(true)), + ) + .subcommand( + SubCommand::with_name("stop") + .about("Stops an app") + .arg(Arg::with_name("ID").help("The app to stop").required(true)) + .arg( + Arg::with_name("dry-run") + .long("dry-run") + .help("Do not commit result"), + ) + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ), + ) + .subcommand( + SubCommand::with_name("restart") + .about("Restarts an app") + .arg( + Arg::with_name("ID") + .help("The app to restart") + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("configure") + .about("Configures an app") + .arg( + Arg::with_name("ID") + .help("The app to configure") + .required(true), + ) + .arg(Arg::with_name("FILE").help("The configuration file to use")) + .arg( + Arg::with_name("stdin") + .long("stdin") + .help("Use stdin for the config file") + .conflicts_with("FILE"), + ) + .arg( + Arg::with_name("timeout") + .short("t") + .long("timeout") + .help("Max seconds to attempt generating entropy per field") + .default_value("3") + .conflicts_with("no-timeout"), + ) + .arg( + Arg::with_name("no-timeout") + .long("no-timeout") + .help("Disable timeout on entropy generation") + .conflicts_with("timeout"), + ) + .arg( + Arg::with_name("dry-run") + .long("dry-run") + .help("Do not commit result"), + ) + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ), + ) + .subcommand( + SubCommand::with_name("check-dependencies") + .about("Check dependencies for an app") + .arg( + Arg::with_name("ID") + .help("The app to check dependencies for.") + .required(true), + ) + .arg(Arg::with_name("local-only").long("local-only").help( + "Disable reaching out to the Start9 registry if the app isn't installed.", + )) + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ), + ) + .subcommand( + SubCommand::with_name("autoconfigure-dependency") + .about("Automatically configure a dependency") + .arg( + Arg::with_name("ID") + .help("The app to autoconfigure a dependency for.") + .required(true), + ) + .arg( + Arg::with_name("DEPENDENCY") + .help("The dependency to autoconfigure.") + .required(true), + ) + .arg( + Arg::with_name("dry-run") + .long("dry-run") + .help("Do not commit result"), + ) + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ), + ) + .subcommand( + SubCommand::with_name("remove") + .alias("rm") + .about("Removes an installed app") + .arg( + Arg::with_name("purge") + .short("p") + .long("purge") + .help("Deletes all application data"), + ) + .arg( + Arg::with_name("ID") + .help("ID of the application to be removed") + .required(true), + ) + .arg( + Arg::with_name("dry-run") + .long("dry-run") + .help("Do not commit result"), + ) + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ), + ) + .subcommand( + SubCommand::with_name("tor") + .about("Configures tor hidden services") + .subcommand( + SubCommand::with_name("show") + .about("Shows the onion address for the hidden service") + .arg( + Arg::with_name("ID") + .help("ID of the application to get the onion address for") + .required(true), + ), + ) + .subcommand(SubCommand::with_name("reload").about("Reloads the tor configuration")), + ) + .subcommand( + SubCommand::with_name("info") + .about("Prints information about an installed app") + .arg( + Arg::with_name("ID") + .help("ID of the application to print information about") + .required(true), + ) + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .required_unless("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .required_unless("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ) + .arg( + Arg::with_name("include-status") + .long("include-status") + .short("s"), + ) + .arg( + Arg::with_name("include-manifest") + .long("include-manifest") + .short("m"), + ) + .arg( + Arg::with_name("include-config") + .long("include-config") + .short("c"), + ) + .arg( + Arg::with_name("include-dependencies") + .long("include-dependencies") + .short("d"), + ) + .arg( + Arg::with_name("only-status") + .long("only-status") + .short("S") + .conflicts_with_all(&[ + "include-status", + "include-manifest", + "include-config", + "include-dependencies", + "only-manifest", + "only-config", + "only-dependencies", + ]), + ) + .arg( + Arg::with_name("only-manifest") + .long("only-manifest") + .short("M") + .conflicts_with_all(&[ + "include-status", + "include-manifest", + "include-config", + "include-dependencies", + "only-status", + "only-config", + "only-dependencies", + ]), + ) + .arg( + Arg::with_name("only-config") + .long("only-config") + .short("C") + .conflicts_with_all(&[ + "include-status", + "include-manifest", + "include-config", + "include-dependencies", + "only-status", + "only-manifest", + "only-dependencies", + ]), + ) + .arg( + Arg::with_name("only-dependencies") + .long("only-dependencies") + .short("D") + .conflicts_with_all(&[ + "include-status", + "include-manifest", + "include-config", + "include-dependencies", + "only-status", + "only-manifest", + "only-config", + ]), + ), + ) + .subcommand( + SubCommand::with_name("instructions") + .about("Prints instructions for an installed app") + .arg( + Arg::with_name("ID") + .help("ID of the application to print instructions for") + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("list") + .alias("ls") + .about("Lists apps successfully installed on the system") + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ) + .arg( + Arg::with_name("include-status") + .long("include-status") + .short("s"), + ) + .arg( + Arg::with_name("include-manifest") + .long("include-manifest") + .short("m"), + ) + .arg( + Arg::with_name("include-config") + .long("include-config") + .short("c"), + ) + .arg( + Arg::with_name("include-dependencies") + .long("include-dependencies") + .short("d"), + ), + ) + .subcommand( + SubCommand::with_name("self-update") + .about("Updates appmgr") + .arg( + Arg::with_name("VERSION_REQUIREMENT") + .help("Version requirement to update to (i.e. ^0.1.0)"), + ), + ) + .subcommand( + SubCommand::with_name("logs") + .about("Fetch the logs of an app") + .arg( + Arg::with_name("ID") + .help("ID of the application to fetch logs for") + .required(true), + ) + .arg( + Arg::with_name("details") + .help("Show extra details provided to logs") + .long("details"), + ) + .arg( + Arg::with_name("follow") + .help("Follow log output") + .long("follow") + .short("f"), + ) + .arg( + Arg::with_name("since") + .help(concat!( + "Show logs since timestamp (e.g. 2013-01-02T13:23:37)", + " or relative (e.g. 42m for 42 minutes)" + )) + .long("since") + .takes_value(true), + ) + .arg( + Arg::with_name("tail") + .help("Number of lines to show from the end of the logs") + .long("tail") + .takes_value(true) + .default_value("all"), + ) + .arg( + Arg::with_name("timestamps") + .help("Show timestamps") + .short("t") + .long("timestamps"), + ) + .arg( + Arg::with_name("until") + .help(concat!( + "Show logs before a timestamp (e.g. 2013-01-02T13:23:37)", + " or relative (e.g. 42m for 42 minutes)" + )) + .long("until") + .takes_value(true), + ), + ) + .subcommand( + SubCommand::with_name("notifications") + .about("Get notifications broadcast by an app") + .arg( + Arg::with_name("ID") + .help("ID of the application to get notifications for") + .required(true), + ) + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ), + ) + .subcommand( + SubCommand::with_name("stats") + .about("Get stats broadcast by an app") + .arg( + Arg::with_name("ID") + .help("ID of the application to get stats for") + .required(true), + ) + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .required_unless("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .required_unless("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ), + ) + .subcommand( + SubCommand::with_name("disks") + .about("Manage external disks") + .subcommand( + SubCommand::with_name("show") + .alias("list") + .alias("ls") + .about("List external drive information") + .arg( + Arg::with_name("json") + .conflicts_with("yaml") + .long("json") + .short("j") + .help("Output as json"), + ) + .arg( + Arg::with_name("pretty") + .requires("json") + .long("pretty") + .short("p") + .help("Pretty print output"), + ) + .arg( + Arg::with_name("yaml") + .conflicts_with("json") + .long("yaml") + .short("y") + .help("Output as yaml"), + ), + ) + .subcommand(SubCommand::with_name("use")), + ) + .subcommand( + SubCommand::with_name("backup") + .about("Manage app data backups") + .subcommand( + SubCommand::with_name("create") + .about("Backup current app state") + .arg( + Arg::with_name("ID") + .help("ID of the application to backup data for") + .required(true), + ) + .arg( + Arg::with_name("PARTITION") + .help("Logical name of the partition you would like to backup to") + .required(true), + ) + .arg( + Arg::with_name("password") + .long("password") + .short("p") + .takes_value(true) + .help("Password to use for encryption of backup file"), + ), + ) + .subcommand( + SubCommand::with_name("restore") + .about("Restore app state from backup") + .arg( + Arg::with_name("ID") + .help("ID of the application to restore data for") + .required(true), + ) + .arg( + Arg::with_name("PARTITION") + .help("Logical name of the partition you would like to backup to") + .required(true), + ) + .arg( + Arg::with_name("timestamp") + .long("timestamp") + .short("t") + .takes_value(true) + .help("Timestamp of the backup to restore"), + ) + .arg( + Arg::with_name("password") + .long("password") + .short("p") + .takes_value(true) + .help("Password to use for encryption of backup file"), + ), + ), + ); + + let matches = app.clone().get_matches(); + + log::set_max_level(match matches.occurrences_of("verbosity") { + 0 => log::LevelFilter::Error, + 1 => log::LevelFilter::Warn, + 2 => log::LevelFilter::Info, + 3 => log::LevelFilter::Debug, + _ => log::LevelFilter::Trace, + }); + + match matches.subcommand() { + ("semver", _) => { + println!("{}", version); + } + ("git-info", _) => { + println!("{}", git_version); + } + #[cfg(not(feature = "portable"))] + ("install", Some(sub_m)) => { + let target = sub_m.value_of("ID|PATH|URL").unwrap(); + if target.starts_with("https://") || target.starts_with("http://") { + install_url(target, None).await?; + } else if target.ends_with(".s9pk") { + install_path(target, None).await?; + } else { + install_name(target, !sub_m.is_present("no-cache")).await?; + } + } + #[cfg(not(feature = "portable"))] + ("update", Some(sub_m)) => { + let res = update(sub_m.value_of("ID").unwrap(), sub_m.is_present("dry-run")).await?; + if sub_m.is_present("json") { + if sub_m.is_present("pretty") { + println!( + "{}", + serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } + } else if sub_m.is_present("yaml") { + println!( + "{}", + serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else if !res.is_empty() { + use prettytable::{Cell, Row, Table}; + let mut table = Table::new(); + let heading = vec![ + Cell::new("APPLICATION ID"), + Cell::new("STATUS"), + Cell::new("REASON"), + ]; + table.add_row(Row::new(heading)); + for (name, reason) in res { + table.add_row(Row::new(vec![ + Cell::new(&name), + Cell::new("Stopped"), + Cell::new(&format!("{}", reason)), + ])); + } + table.print(&mut std::io::stdout())?; + } + } + #[cfg(not(feature = "portable"))] + ("start", Some(sub_m)) => { + start_app(sub_m.value_of("ID").unwrap(), true).await?; + } + #[cfg(not(feature = "portable"))] + ("stop", Some(sub_m)) => { + let res = stop_app( + sub_m.value_of("ID").unwrap(), + true, + sub_m.is_present("dry-run"), + ) + .await?; + if sub_m.is_present("json") { + if sub_m.is_present("pretty") { + println!( + "{}", + serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } + } else if sub_m.is_present("yaml") { + println!( + "{}", + serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else if !res.is_empty() { + use prettytable::{Cell, Row, Table}; + let mut table = Table::new(); + let heading = vec![ + Cell::new("APPLICATION ID"), + Cell::new("STATUS"), + Cell::new("REASON"), + ]; + table.add_row(Row::new(heading)); + for (name, reason) in res { + table.add_row(Row::new(vec![ + Cell::new(&name), + Cell::new("Stopped"), + Cell::new(&format!("{}", reason)), + ])); + } + table.print(&mut std::io::stdout())?; + } + } + #[cfg(not(feature = "portable"))] + ("restart", Some(sub_m)) => { + restart_app(sub_m.value_of("ID").unwrap()).await?; + } + #[cfg(not(feature = "portable"))] + ("configure", Some(sub_m)) => { + let config: Option = if let Some(path) = sub_m.value_of("FILE") { + let p = Path::new(path); + if p.extension() == Some(std::ffi::OsStr::new("json")) + || (sub_m.is_present("json") + && p.extension() != Some(std::ffi::OsStr::new("yaml"))) + { + Some(util::from_json_async_reader(tokio::fs::File::open(p).await?).await?) + } else { + Some(util::from_yaml_async_reader(tokio::fs::File::open(p).await?).await?) + } + } else if sub_m.is_present("stdin") { + if sub_m.is_present("json") { + Some(util::from_yaml_async_reader(tokio::io::stdin()).await?) + } else { + Some(util::from_yaml_async_reader(tokio::io::stdin()).await?) + } + } else { + None + }; + let timeout = if sub_m.is_present("no-timeout") { + None + } else if let Some(t) = sub_m.value_of("timeout") { + Some(std::time::Duration::from_secs(t.parse().no_code()?)) + } else { + Some(std::time::Duration::from_secs(3)) + }; + let res = configure( + sub_m.value_of("ID").unwrap(), + config, + timeout, + sub_m.is_present("dry-run"), + ) + .await?; + if sub_m.is_present("json") { + if sub_m.is_present("pretty") { + println!( + "{}", + serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } + } else if sub_m.is_present("yaml") { + println!( + "{}", + serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else if !res.needs_restart.is_empty() || !res.stopped.is_empty() { + use prettytable::{Cell, Row, Table}; + let mut table = Table::new(); + let heading = vec![ + Cell::new("APPLICATION ID"), + Cell::new("STATUS"), + Cell::new("REASON"), + ]; + table.add_row(Row::new(heading)); + for name in res.needs_restart { + table.add_row(Row::new(vec![ + Cell::new(&name), + Cell::new("Needs Restart"), + Cell::new("Configuration Changed"), + ])); + } + for (name, reason) in res.stopped { + table.add_row(Row::new(vec![ + Cell::new(&name), + Cell::new("Stopped"), + Cell::new(&format!("{}", reason)), + ])); + } + table.print(&mut std::io::stdout())?; + } + } + #[cfg(not(feature = "portable"))] + ("check-dependencies", Some(sub_m)) => { + let res = apps::dependencies( + sub_m.value_of("ID").unwrap(), + sub_m.is_present("local-only"), + ) + .await?; + if sub_m.is_present("json") { + if sub_m.is_present("pretty") { + println!( + "{}", + serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } + } else if sub_m.is_present("yaml") { + println!( + "{}", + serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else if !res.0.is_empty() { + use prettytable::{Cell, Row, Table}; + let mut table = Table::new(); + let heading = vec![ + Cell::new("APPLICATION ID"), + Cell::new("REQUIRED"), + Cell::new("VIOLATION"), + ]; + table.add_row(Row::new(heading)); + for (name, info) in res.0 { + table.add_row(Row::new(vec![ + Cell::new(&name), + Cell::new(&format!("{}", info.required)), + Cell::new(&if let Some(error) = info.error { + format!("{}", error) + } else { + "N/A".to_owned() + }), + ])); + } + table.print(&mut std::io::stdout())?; + } else { + println!("No dependencies for {}", sub_m.value_of("ID").unwrap()); + } + } + ("autoconfigure-dependency", Some(sub_m)) => { + let res = dependencies::auto_configure( + sub_m.value_of("ID").unwrap(), + sub_m.value_of("DEPENDENCY").unwrap(), + sub_m.is_present("dry-run"), + ) + .await?; + if sub_m.is_present("json") { + if sub_m.is_present("pretty") { + println!( + "{}", + serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } + } else if sub_m.is_present("yaml") { + println!( + "{}", + serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else if !res.needs_restart.is_empty() || !res.stopped.is_empty() { + use prettytable::{Cell, Row, Table}; + let mut table = Table::new(); + let heading = vec![ + Cell::new("APPLICATION ID"), + Cell::new("STATUS"), + Cell::new("REASON"), + ]; + table.add_row(Row::new(heading)); + for name in res.needs_restart { + table.add_row(Row::new(vec![ + Cell::new(&name), + Cell::new("Needs Restart"), + Cell::new("Configuration Changed"), + ])); + } + for (name, reason) in res.stopped { + table.add_row(Row::new(vec![ + Cell::new(&name), + Cell::new("Stopped"), + Cell::new(&format!("{}", reason)), + ])); + } + table.print(&mut std::io::stdout())?; + } + } + #[cfg(not(feature = "portable"))] + ("remove", Some(sub_m)) | ("rm", Some(sub_m)) => { + let res = remove( + sub_m.value_of("ID").unwrap(), + sub_m.is_present("purge"), + sub_m.is_present("dry-run"), + ) + .await?; + if sub_m.is_present("json") { + if sub_m.is_present("pretty") { + println!( + "{}", + serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } + } else if sub_m.is_present("yaml") { + println!( + "{}", + serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? + ); + } else if !res.is_empty() { + use prettytable::{Cell, Row, Table}; + let mut table = Table::new(); + let heading = vec![ + Cell::new("APPLICATION ID"), + Cell::new("STATUS"), + Cell::new("REASON"), + ]; + table.add_row(Row::new(heading)); + for (name, reason) in res { + table.add_row(Row::new(vec![ + Cell::new(&name), + Cell::new("Stopped"), + Cell::new(&format!("{}", reason)), + ])); + } + table.print(&mut std::io::stdout())?; + } + } + #[cfg(not(feature = "portable"))] + ("tor", Some(sub_m)) => match sub_m.subcommand() { + ("show", Some(sub_sub_m)) => { + println!( + "{}", + crate::tor::read_tor_address(sub_sub_m.value_of("ID").unwrap(), None).await? + ); + } + ("reload", Some(_)) => { + crate::tor::reload().await?; + } + _ => { + println!("{}", sub_m.usage()); + std::process::exit(1); + } + }, + #[cfg(not(feature = "portable"))] + ("info", Some(sub_m)) => { + let name = sub_m.value_of("ID").unwrap(); + let info = crate::apps::info_full( + &name, + sub_m.is_present("include-status") || sub_m.is_present("only-status"), + sub_m.is_present("include-manifest") || sub_m.is_present("only-manifest"), + sub_m.is_present("include-config") || sub_m.is_present("only-config"), + sub_m.is_present("include-dependencies") || sub_m.is_present("only-dependencies"), + ) + .await?; + if sub_m.is_present("json") { + if sub_m.is_present("pretty") { + if sub_m.is_present("only-status") { + println!( + "{}", + serde_json::to_string_pretty(&info.status) + .with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_m.is_present("only-manifest") { + println!( + "{}", + serde_json::to_string_pretty(&info.manifest) + .with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_m.is_present("only-config") { + println!( + "{}", + serde_json::to_string_pretty(&info.config) + .with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_m.is_present("only-dependencies") { + println!( + "{}", + serde_json::to_string_pretty(&info.dependencies) + .with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string_pretty(&info) + .with_code(crate::error::SERDE_ERROR)? + ); + } + } else { + if sub_m.is_present("only-status") { + println!( + "{}", + serde_json::to_string(&info.status) + .with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_m.is_present("only-manifest") { + println!( + "{}", + serde_json::to_string(&info.manifest) + .with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_m.is_present("only-config") { + println!( + "{}", + serde_json::to_string(&info.config) + .with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_m.is_present("only-dependencies") { + println!( + "{}", + serde_json::to_string(&info.dependencies) + .with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } + } + } else if sub_m.is_present("yaml") { + if sub_m.is_present("only-status") { + println!( + "{}", + serde_yaml::to_string(&info.status).with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_m.is_present("only-manifest") { + println!( + "{}", + serde_yaml::to_string(&info.manifest) + .with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_m.is_present("only-config") { + println!( + "{}", + serde_yaml::to_string(&info.config).with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_m.is_present("only-dependencies") { + println!( + "{}", + serde_yaml::to_string(&info.dependencies) + .with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } + } + } + #[cfg(not(feature = "portable"))] + ("instructions", Some(sub_m)) => { + crate::apps::print_instructions(sub_m.value_of("ID").unwrap()).await?; + } + #[cfg(not(feature = "portable"))] + ("list", Some(sub_m)) | ("ls", Some(sub_m)) => { + let info = crate::apps::list( + sub_m.is_present("include-status"), + sub_m.is_present("include-manifest"), + sub_m.is_present("include-config"), + sub_m.is_present("include-dependencies"), + ) + .await?; + if sub_m.is_present("json") { + if sub_m.is_present("pretty") { + println!( + "{}", + serde_json::to_string_pretty(&info).with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } + } else if sub_m.is_present("yaml") { + println!( + "{}", + serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } else if !info.is_empty() { + use prettytable::{Cell, Row, Table}; + let mut table = Table::new(); + let mut heading = vec![ + Cell::new("APPLICATION ID"), + Cell::new("TITLE"), + Cell::new("VERSION"), + Cell::new("TOR ADDRESS"), + Cell::new("CONFIGURED"), + ]; + if sub_m.is_present("include-status") { + heading.push(Cell::new("STATUS")); + } + if sub_m.is_present("include-dependencies") { + heading.push(Cell::new("DEPENDENCIES MET")) + } + table.add_row(Row::new(heading)); + for (name, info) in info { + table.add_row(Row::new( + vec![ + Cell::new(&name), + Cell::new(&format!("{}", info.info.title)), + Cell::new(&format!("{}", info.info.version)), + Cell::new(&format!( + "{}", + info.info.tor_address.unwrap_or_else(|| "N/A".to_owned()) + )), + Cell::new(&format!("{}", info.info.configured)), + ] + .into_iter() + .chain( + info.status + .into_iter() + .map(|s| Cell::new(&format!("{:?}", s.status))), + ) + .chain(info.dependencies.into_iter().map(|s| { + Cell::new(&format!( + "{}", + s.0.into_iter() + .all(|(_, dep)| dep.error.is_none() || !dep.required) + )) + })) + .collect(), + )); + } + table.print(&mut std::io::stdout())?; + } else { + println!("No apps installed"); + } + } + #[cfg(not(feature = "portable"))] + ("self-update", Some(sub_m)) => { + self_update( + sub_m + .value_of("VERSION_REQUIREMENT") + .map(|a| a.parse()) + .transpose() + .no_code()? + .unwrap_or_else(|| emver::VersionRange::any()), + ) + .await?; + } + #[cfg(not(feature = "portable"))] + ("logs", Some(sub_m)) => { + logs( + sub_m.value_of("ID").unwrap(), + LogOptions { + details: sub_m.is_present("details"), + follow: sub_m.is_present("follow"), + since: sub_m.value_of("since"), + until: sub_m.value_of("until"), + tail: sub_m + .value_of("tail") + .filter(|t| t != &"all") + .map(|a| a.parse()) + .transpose() + .no_code()?, + timestamps: sub_m.is_present("timestamps"), + }, + ) + .await?; + } + #[cfg(not(feature = "portable"))] + ("notifications", Some(sub_m)) => { + let info = notifications(sub_m.value_of("ID").unwrap()).await?; + if sub_m.is_present("json") { + if sub_m.is_present("pretty") { + println!( + "{}", + serde_json::to_string_pretty(&info).with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } + } else if sub_m.is_present("yaml") { + println!( + "{}", + serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } else if !info.is_empty() { + use prettytable::{Cell, Row, Table}; + let mut table = Table::new(); + let heading = vec![ + Cell::new("LEVEL"), + Cell::new("CODE"), + Cell::new("TITLE"), + Cell::new("MESSAGE"), + ]; + table.add_row(Row::new(heading)); + for note in info { + table.add_row(Row::new(vec![ + Cell::new(&format!("{}", note.level)), + Cell::new(&format!("{}", note.code)), + Cell::new(&format!("{}", note.title)), + Cell::new(&format!("{}", note.message)), + ])); + } + table.print(&mut std::io::stdout())?; + } else { + println!("No notifications for {}", sub_m.value_of("ID").unwrap()); + } + } + #[cfg(not(feature = "portable"))] + ("stats", Some(sub_m)) => { + let info = stats(sub_m.value_of("ID").unwrap()).await?; + if sub_m.is_present("json") { + if sub_m.is_present("pretty") { + println!( + "{}", + serde_json::to_string_pretty(&info).with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } + } else if sub_m.is_present("yaml") { + println!( + "{}", + serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } else if let serde_yaml::Value::Mapping(map) = info { + use prettytable::{Cell, Row, Table}; + let mut table = Table::new(); + for (k, v) in map { + let ks = match k { + serde_yaml::Value::Bool(k) => format!("{}", k), + serde_yaml::Value::Null => "null".to_owned(), + serde_yaml::Value::Number(k) => format!("{}", k), + serde_yaml::Value::String(k) => k, + k => serde_yaml::to_string(&k).with_code(crate::error::SERDE_ERROR)?, + }; + let vs = match v { + serde_yaml::Value::Bool(v) => format!("{}", v), + serde_yaml::Value::Null => "null".to_owned(), + serde_yaml::Value::Number(v) => format!("{}", v), + serde_yaml::Value::String(v) => v, + v => serde_yaml::to_string(&v).with_code(crate::error::SERDE_ERROR)?, + }; + table.add_row(Row::new(vec![Cell::new(&ks), Cell::new(&vs)])); + } + table.print(&mut std::io::stdout())?; + } + } + #[cfg(not(feature = "portable"))] + ("disks", Some(sub_m)) => match sub_m.subcommand() { + ("show", Some(sub_sub_m)) | ("list", Some(sub_sub_m)) | ("ls", Some(sub_sub_m)) => { + let info = disks::list().await?; + if sub_sub_m.is_present("json") { + if sub_sub_m.is_present("pretty") { + println!( + "{}", + serde_json::to_string_pretty(&info) + .with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } + } else if sub_sub_m.is_present("yaml") { + println!( + "{}", + serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } else { + todo!() + } + } + _ => { + println!("{}", sub_m.usage()); + std::process::exit(1); + } + }, + #[cfg(not(feature = "portable"))] + ("backup", Some(sub_m)) => match sub_m.subcommand() { + ("create", Some(sub_sub_m)) => { + crate::backup::backup_to_partition( + sub_sub_m.value_of("PARTITION").unwrap(), + sub_sub_m.value_of("ID").unwrap(), + &match sub_sub_m.value_of("password") { + Some(a) => Cow::Borrowed(a), + None => Cow::Owned(rpassword::read_password_from_tty(Some("Password: "))?), + }, + ) + .await? + } + ("restore", Some(sub_sub_m)) => { + crate::backup::restore_from_partition( + sub_sub_m.value_of("PARTITION").unwrap(), + sub_sub_m.value_of("ID").unwrap(), + &match sub_sub_m.value_of("password") { + Some(a) => Cow::Borrowed(a), + None => Cow::Owned(rpassword::read_password_from_tty(Some("Password: "))?), + }, + ) + .await? + } + _ => { + println!("{}", sub_m.usage()); + std::process::exit(1); + } + }, + ("pack", Some(sub_m)) => { + pack( + sub_m.value_of("PATH").unwrap(), + sub_m.value_of("output").unwrap(), + ) + .await? + } + ("verify", Some(sub_m)) => verify(sub_m.value_of("PATH").unwrap()).await?, + ("inspect", Some(sub_m)) => match sub_m.subcommand() { + ("info", Some(sub_sub_m)) => { + let path = sub_sub_m.value_of("PATH").unwrap(); + let info = crate::inspect::info_full( + path, + sub_sub_m.is_present("include-manifest") + || sub_sub_m.is_present("only-manifest"), + sub_sub_m.is_present("include-config") || sub_sub_m.is_present("only-config"), + ) + .await?; + if sub_sub_m.is_present("json") { + if sub_sub_m.is_present("pretty") { + if sub_sub_m.is_present("only-manifest") { + println!( + "{}", + serde_json::to_string_pretty(&info.manifest) + .with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_sub_m.is_present("only-config") { + println!( + "{}", + serde_json::to_string_pretty(&info.config) + .with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string_pretty(&info) + .with_code(crate::error::SERDE_ERROR)? + ); + } + } else { + if sub_sub_m.is_present("only-manifest") { + println!( + "{}", + serde_json::to_string(&info.manifest) + .with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_sub_m.is_present("only-config") { + println!( + "{}", + serde_json::to_string(&info.config) + .with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_json::to_string(&info) + .with_code(crate::error::SERDE_ERROR)? + ); + } + } + } else if sub_sub_m.is_present("yaml") { + if sub_sub_m.is_present("only-manifest") { + println!( + "{}", + serde_yaml::to_string(&info.manifest) + .with_code(crate::error::SERDE_ERROR)? + ); + } else if sub_sub_m.is_present("only-config") { + println!( + "{}", + serde_yaml::to_string(&info.config) + .with_code(crate::error::SERDE_ERROR)? + ); + } else { + println!( + "{}", + serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? + ); + } + } + } + ("instructions", Some(sub_sub_m)) => { + crate::inspect::print_instructions(Path::new(sub_sub_m.value_of("PATH").unwrap())) + .await?; + } + _ => { + println!("{}", sub_m.usage()); + std::process::exit(1); + } + }, + ("index", Some(sub_m)) => { + let idx = crate::index::index(Path::new(sub_m.value_of("DIR").unwrap())).await?; + println!( + "{}", + serde_yaml::to_string(&idx).with_code(crate::error::SERDE_ERROR)? + ); + } + _ => { + app.print_long_help().unwrap(); + std::process::exit(1); + } + } + + Ok(()) +} diff --git a/appmgr/src/manifest.rs b/appmgr/src/manifest.rs new file mode 100644 index 000000000..47d92af63 --- /dev/null +++ b/appmgr/src/manifest.rs @@ -0,0 +1,76 @@ +use std::path::PathBuf; + +use linear_map::LinearMap; + +use crate::dependencies::Dependencies; +use crate::tor::HiddenServiceVersion; +use crate::tor::PortMapping; + +pub type ManifestLatest = ManifestV0; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct Description { + pub short: String, + pub long: String, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +pub enum ImageConfig { + Tar, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct Asset { + pub src: PathBuf, + pub dst: PathBuf, + pub overwrite: bool, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct ManifestV0 { + pub id: String, + pub version: emver::Version, + pub title: String, + pub description: Description, + pub release_notes: String, + #[serde(default)] + pub has_instructions: bool, + #[serde(default = "emver::VersionRange::any")] + pub os_version_required: emver::VersionRange, + #[serde(default = "emver::VersionRange::any")] + pub os_version_recommended: emver::VersionRange, + pub ports: Vec, + pub image: ImageConfig, + #[serde(default)] + pub shm_size_mb: Option, + pub mount: PathBuf, + #[serde(default)] + pub public: Option, + #[serde(default)] + pub shared: Option, + #[serde(default)] + pub assets: Vec, + #[serde(default)] + pub hidden_service_version: HiddenServiceVersion, + #[serde(default)] + pub dependencies: Dependencies, + #[serde(flatten)] + pub extra: LinearMap, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +#[serde(tag = "compat")] +#[serde(rename_all = "lowercase")] +pub enum Manifest { + V0(ManifestV0), +} +impl Manifest { + pub fn into_latest(self) -> ManifestLatest { + match self { + Manifest::V0(m) => m, + } + } +} diff --git a/appmgr/src/pack.rs b/appmgr/src/pack.rs new file mode 100644 index 000000000..86ec98619 --- /dev/null +++ b/appmgr/src/pack.rs @@ -0,0 +1,398 @@ +use std::borrow::Cow; +use std::path::{Path, PathBuf}; + +use failure::ResultExt; +use futures::stream::StreamExt; +use linear_map::LinearMap; +use rand::SeedableRng; +use tokio_tar as tar; + +use crate::config::{ConfigRuleEntry, ConfigSpec}; +use crate::manifest::{ImageConfig, Manifest}; +use crate::util::{from_cbor_async_reader, from_json_async_reader, from_yaml_async_reader}; +use crate::version::VersionT; + +#[derive(Clone, Debug, Fail)] +pub enum Error { + #[fail(display = "Invalid Directory Name: {}", _0)] + InvalidDirectoryName(String), + #[fail(display = "Invalid File Name: {}", _0)] + InvalidFileName(String), + #[fail(display = "Invalid Output Path: {}", _0)] + InvalidOutputPath(String), +} + +pub async fn pack(path: &str, output: &str) -> Result<(), failure::Error> { + let path = Path::new(path.trim_end_matches("/")); + let output = Path::new(output); + ensure!( + output + .extension() + .and_then(|a| a.to_str()) + .ok_or_else(|| Error::InvalidOutputPath(format!("{}", output.display())))? + == "s9pk", + "Extension Must Be '.s9pk'" + ); + log::info!( + "Starting pack of {} to {}.", + path.file_name() + .and_then(|a| a.to_str()) + .ok_or_else(|| Error::InvalidDirectoryName(format!("{}", path.display())))?, + output.display(), + ); + let out_file = tokio::fs::File::create(output).await?; + let mut out = tar::Builder::new(out_file); + log::info!("Reading {}/manifest.yaml.", path.display()); + let manifest: Manifest = crate::util::from_yaml_async_reader( + tokio::fs::File::open(path.join("manifest.yaml")) + .await + .with_context(|e| format!("{}: manifest.yaml", e))?, + ) + .await?; + log::info!("Writing manifest to archive."); + let bin_manifest = serde_cbor::to_vec(&manifest)?; + let mut manifest_header = tar::Header::new_gnu(); + manifest_header.set_size(bin_manifest.len() as u64); + out.append_data( + &mut manifest_header, + "manifest.cbor", + std::io::Cursor::new(bin_manifest), + ) + .await?; + let manifest = manifest.into_latest(); + ensure!( + crate::version::Current::new() + .semver() + .satisfies(&manifest.os_version_required), + "Unsupported AppMgr version: expected {}", + manifest.os_version_required + ); + log::info!("Reading {}/config_spec.yaml.", path.display()); + let config_spec: ConfigSpec = from_yaml_async_reader( + tokio::fs::File::open(path.join("config_spec.yaml")) + .await + .with_context(|e| format!("{}: config_spec.yaml", e))?, + ) + .await?; + config_spec.validate(&manifest)?; + let config = config_spec.gen(&mut rand::rngs::StdRng::from_entropy(), &None)?; + config_spec.matches(&config)?; + log::info!("Writing config spec to archive."); + let bin_config_spec = serde_cbor::to_vec(&config_spec)?; + let mut config_spec_header = tar::Header::new_gnu(); + config_spec_header.set_size(bin_config_spec.len() as u64); + out.append_data( + &mut config_spec_header, + "config_spec.cbor", + std::io::Cursor::new(bin_config_spec), + ) + .await?; + log::info!("Reading {}/config_rules.yaml.", path.display()); + let config_rules: Vec = from_yaml_async_reader( + tokio::fs::File::open(path.join("config_rules.yaml")) + .await + .with_context(|e| format!("{}: config_rules.yaml", e))?, + ) + .await?; + let mut cfgs = LinearMap::new(); + cfgs.insert(manifest.id.as_str(), Cow::Borrowed(&config)); + for rule in &config_rules { + rule.check(&config, &cfgs) + .with_context(|e| format!("Default Config does not satisfy: {}", e))?; + } + log::info!("Writing config rules to archive."); + let bin_config_rules = serde_cbor::to_vec(&config_rules)?; + let mut config_rules_header = tar::Header::new_gnu(); + config_rules_header.set_size(bin_config_rules.len() as u64); + out.append_data( + &mut config_rules_header, + "config_rules.cbor", + std::io::Cursor::new(bin_config_rules), + ) + .await?; + if manifest.has_instructions { + log::info!("Packing instructions.md"); + out.append_path_with_name(path.join("instructions.md"), "instructions.md") + .await?; + } + log::info!("Copying over assets."); + for asset in &manifest.assets { + let src_path = Path::new("assets").join(&asset.src); + log::info!("Reading {}/{}.", path.display(), src_path.display()); + let file_path = path.join(&src_path); + let src = tokio::fs::File::open(&file_path) + .await + .with_context(|e| format!("{}: {}", e, src_path.display()))?; + log::info!("Writing {} to archive.", src_path.display()); + if src.metadata().await?.is_dir() { + out.append_dir_all(&asset.src, &file_path).await?; + let mut h = tar::Header::new_gnu(); + h.set_size(0); + h.set_path(format!("APPMGR_DIR_END:{}", asset.src.display()))?; + h.set_cksum(); + out.append(&h, tokio::io::empty()).await?; + } else { + out.append_path_with_name(&file_path, &asset.src).await?; + } + } + match manifest.image { + ImageConfig::Tar => { + log::info!("Reading {}/image.tar.", path.display()); + let image = tokio::fs::File::open(path.join("image.tar")) + .await + .with_context(|e| format!("{}: image.tar", e))?; + log::info!("Writing image.tar to archive."); + let mut header = tar::Header::new_gnu(); + header.set_size(image.metadata().await?.len()); + out.append_data(&mut header, "image.tar", image).await?; + } + } + out.into_inner().await?; + + Ok(()) +} + +pub fn validate_path>(p: P) -> Result<(), Error> { + let path = p.as_ref(); + if path.is_absolute() { + return Err(Error::InvalidFileName(format!("{}", path.display()))); + } + for seg in path { + if seg == ".." { + return Err(Error::InvalidFileName(format!("{}", path.display()))); + } + } + Ok(()) +} + +pub async fn verify(path: &str) -> Result<(), failure::Error> { + let path = Path::new(path.trim_end_matches("/")); + ensure!( + path.extension() + .and_then(|a| a.to_str()) + .ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))? + == "s9pk", + "Extension Must Be '.s9pk'" + ); + let name = path + .file_stem() + .and_then(|a| a.to_str()) + .ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))?; + ensure!( + !name.starts_with("start9") + && name + .chars() + .filter(|c| !c.is_alphanumeric() && c != &'-') + .next() + .is_none(), + "Invalid Application ID" + ); + log::info!( + "Starting verification of {}.", + path.file_name() + .and_then(|a| a.to_str()) + .ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))?, + ); + {} + log::info!("Opening file."); + let r = tokio::fs::File::open(&path) + .await + .with_context(|e| format!("{}: {}", path.display(), e))?; + log::info!("Extracting archive."); + let mut pkg = tar::Archive::new(r); + let mut entries = pkg.entries()?; + log::info!("Opening manifest from archive."); + let manifest = entries + .next() + .await + .ok_or_else(|| format_err!("missing manifest"))??; + ensure!( + manifest.path()?.to_str() == Some("manifest.cbor"), + "Package File Invalid or Corrupted: expected manifest.cbor, got {}", + manifest.path()?.display() + ); + log::trace!("Deserializing manifest."); + let manifest: Manifest = from_cbor_async_reader(manifest).await?; + let manifest = manifest.into_latest(); + ensure!( + crate::version::Current::new() + .semver() + .satisfies(&manifest.os_version_required), + "Unsupported AppMgr Version: expected {}", + manifest.os_version_required + ); + ensure!(manifest.id == name, "Package Name Does Not Match Expected",); + if let (Some(public), Some(shared)) = (&manifest.public, &manifest.shared) { + ensure!( + !public.starts_with(shared) && !shared.starts_with(public), + "Public Directory Conflicts With Shared Directory" + ) + } + if let Some(public) = &manifest.public { + validate_path(public)?; + } + if let Some(shared) = &manifest.shared { + validate_path(shared)?; + } + log::info!("Opening config spec from archive."); + let config_spec = entries + .next() + .await + .ok_or_else(|| format_err!("missing config spec"))??; + ensure!( + config_spec.path()?.to_str() == Some("config_spec.cbor"), + "Package File Invalid or Corrupted: expected config_rules.cbor, got {}", + config_spec.path()?.display() + ); + log::trace!("Deserializing config spec."); + let config_spec: ConfigSpec = from_cbor_async_reader(config_spec).await?; + log::trace!("Validating config spec."); + config_spec.validate(&manifest)?; + let config = config_spec.gen(&mut rand::rngs::StdRng::from_entropy(), &None)?; + config_spec.matches(&config)?; + log::info!("Opening config rules from archive."); + let config_rules = entries + .next() + .await + .ok_or_else(|| format_err!("missing config rules"))??; + ensure!( + config_rules.path()?.to_str() == Some("config_rules.cbor"), + "Package File Invalid or Corrupted: expected config_rules.cbor, got {}", + config_rules.path()?.display() + ); + log::trace!("Deserializing config rules."); + let config_rules: Vec = from_cbor_async_reader(config_rules).await?; + log::trace!("Validating config rules against config spec."); + let mut cfgs = LinearMap::new(); + cfgs.insert(name, Cow::Borrowed(&config)); + for rule in &config_rules { + rule.check(&config, &cfgs) + .with_context(|e| format!("Default Config does not satisfy: {}", e))?; + } + if manifest.has_instructions { + let instructions = entries + .next() + .await + .ok_or_else(|| format_err!("missing instructions"))??; + ensure!( + instructions.path()?.to_str() == Some("instructions.md"), + "Package File Invalid or Corrupted: expected instructions.md, got {}", + instructions.path()?.display() + ); + } + for asset_info in manifest.assets { + validate_path(&asset_info.src)?; + validate_path(&asset_info.dst)?; + let asset = entries + .next() + .await + .ok_or_else(|| format_err!("missing asset: {}", asset_info.src.display()))??; + if asset.header().entry_type().is_file() { + ensure!( + asset.path()?.to_str() == Some(&format!("{}", asset_info.src.display())), + "Package File Invalid or Corrupted: expected {}, got {}", + asset_info.src.display(), + asset.path()?.display() + ); + } else if asset.header().entry_type().is_dir() { + ensure!( + asset.path()?.to_str() == Some(&format!("{}/", asset_info.src.display())), + "Package File Invalid or Corrupted: expected {}, got {}", + asset_info.src.display(), + asset.path()?.display() + ); + loop { + let file = entries.next().await.ok_or_else(|| { + format_err!( + "missing directory end marker: APPMGR_DIR_END:{}", + asset_info.src.display() + ) + })??; + if file + .path()? + .starts_with(format!("APPMGR_DIR_END:{}", asset_info.src.display())) + { + break; + } else { + ensure!( + file.path()? + .to_str() + .map(|p| p.starts_with(&format!("{}/", asset_info.src.display()))) + .unwrap_or(false), + "Package File Invalid or Corrupted: expected {}, got {}", + asset_info.src.display(), + asset.path()?.display() + ); + } + } + } else { + bail!("Asset Not Regular File: {}", asset_info.src.display()); + } + } + match &manifest.image { + ImageConfig::Tar => { + #[derive(Clone, Debug, serde::Deserialize)] + #[serde(rename_all = "PascalCase")] + struct DockerManifest { + config: PathBuf, + repo_tags: Vec, + layers: Vec, + } + let image_name = format!("start9/{}", manifest.id); + log::debug!("Opening image.tar from archive."); + let image = entries + .next() + .await + .ok_or_else(|| format_err!("missing image.tar"))??; + let image_path = image.path()?; + if image_path != Path::new("image.tar") { + return Err(format_err!( + "Package File Invalid or Corrupted: expected image.tar, got {}", + image_path.display() + )); + } + log::info!("Verifying image.tar."); + let mut image_tar = tar::Archive::new(image); + let image_manifest = image_tar + .entries()? + .map(|e| { + let e = e?; + Ok((e.path()?.to_path_buf(), e)) + }) + .filter_map(|res: Result<(PathBuf, tar::Entry<_>), std::io::Error>| { + futures::future::ready(match res { + Ok((path, e)) => { + if path == Path::new("manifest.json") { + Some(Ok(e)) + } else { + None + } + } + Err(e) => Some(Err(e)), + }) + }) + .next() + .await + .ok_or_else(|| format_err!("image.tar is missing manifest.json"))??; + let image_manifest: Vec = + from_json_async_reader(image_manifest).await?; + image_manifest + .into_iter() + .flat_map(|a| a.repo_tags) + .map(|t| { + if t.starts_with("start9/") { + if t.split(":").next().unwrap() != image_name { + Err(format_err!("Contains prohibited image tag: {}", t)) + } else { + Ok(()) + } + } else { + Ok(()) + } + }) + .collect::>()?; + } + }; + + Ok(()) +} diff --git a/appmgr/src/registry.rs b/appmgr/src/registry.rs new file mode 100644 index 000000000..1bd9d69fe --- /dev/null +++ b/appmgr/src/registry.rs @@ -0,0 +1,66 @@ +use emver::VersionRange; + +use crate::apps::AppConfig; +use crate::manifest::ManifestLatest; +use crate::Error; +use crate::ResultExt as _; + +pub async fn manifest(id: &str, version: &VersionRange) -> Result { + let manifest: ManifestLatest = reqwest::get(&format!( + "{}/manifest/{}?spec={}", + &*crate::APP_REGISTRY_URL, + id, + version + )) + .await + .with_code(crate::error::NETWORK_ERROR)? + .error_for_status() + .with_code(crate::error::REGISTRY_ERROR)? + .json() + .await + .with_code(crate::error::SERDE_ERROR)?; + Ok(manifest) +} + +pub async fn version(id: &str, version: &VersionRange) -> Result { + #[derive(serde::Deserialize)] + struct VersionRes { + version: emver::Version, + } + + let version: VersionRes = reqwest::get(&format!( + "{}/version/{}?spec={}", + &*crate::APP_REGISTRY_URL, + id, + version + )) + .await + .with_code(crate::error::NETWORK_ERROR)? + .error_for_status() + .with_code(crate::error::REGISTRY_ERROR)? + .json() + .await + .with_code(crate::error::SERDE_ERROR)?; + Ok(version.version) +} + +pub async fn config(id: &str, version: &VersionRange) -> Result { + let config: crate::inspect::AppConfig = reqwest::get(&format!( + "{}/config/{}?spec={}", + &*crate::APP_REGISTRY_URL, + id, + version + )) + .await + .with_code(crate::error::NETWORK_ERROR)? + .error_for_status() + .with_code(crate::error::REGISTRY_ERROR)? + .json() + .await + .with_code(crate::error::SERDE_ERROR)?; + Ok(AppConfig { + config: None, + spec: config.spec, + rules: config.rules, + }) +} diff --git a/appmgr/src/remove.rs b/appmgr/src/remove.rs new file mode 100644 index 000000000..53b75f075 --- /dev/null +++ b/appmgr/src/remove.rs @@ -0,0 +1,117 @@ +use std::path::Path; + +use linear_map::LinearMap; + +use crate::dependencies::{DependencyError, TaggedDependencyError}; +use crate::Error; + +pub async fn remove( + name: &str, + purge: bool, + dry_run: bool, +) -> Result, Error> { + let manifest = crate::apps::manifest(name).await?; + let mut res = LinearMap::new(); + crate::stop_dependents(name, dry_run, DependencyError::NotInstalled, &mut res).await?; + if dry_run { + return Ok(res); + } + let image_name = format!("start9/{}", name); + log::info!("Removing app from manifest."); + crate::apps::remove(name).await?; + log::info!("Stopping docker container."); + let res = crate::control::stop_app(name, false, false) + .await + .unwrap_or_else(|e| { + log::error!("Error stopping app: {}", e); + LinearMap::new() + }); + log::info!("Removing docker container."); + if !std::process::Command::new("docker") + .args(&["rm", name]) + .stdout(std::process::Stdio::null()) + .stderr(match log::max_level() { + log::LevelFilter::Error => std::process::Stdio::null(), + _ => std::process::Stdio::inherit(), + }) + .status()? + .success() + { + log::error!("Failed to Remove Docker Container"); + }; + if !std::process::Command::new("docker") + .args(&["rmi", &image_name]) + .stdout(std::process::Stdio::null()) + .stderr(match log::max_level() { + log::LevelFilter::Error => std::process::Stdio::null(), + _ => std::process::Stdio::inherit(), + }) + .status()? + .success() + { + log::error!("Failed to Remove Docker Image"); + }; + if purge { + log::info!("Removing tor hidden service."); + crate::tor::rm_svc(name).await?; + log::info!("Removing app metadata."); + tokio::fs::remove_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps").join(name)) + .await?; + log::info!("Destroying mounted volume."); + log::info!("Unbinding shared filesystem."); + for (dep, info) in manifest.dependencies.0.iter() { + if info.mount_public { + crate::disks::unmount( + Path::new(crate::VOLUMES) + .join(name) + .join("start9") + .join("public") + .join(&dep), + ) + .await?; + } + if info.mount_shared { + if let Some(shared) = match crate::apps::manifest(dep).await { + Ok(man) => man.shared, + Err(e) => { + log::error!("Failed to Fetch Dependency Manifest: {}", e); + None + } + } { + let path = Path::new(crate::VOLUMES) + .join(name) + .join("start9") + .join("shared") + .join(&dep); + if path.exists() { + crate::disks::unmount(&path).await?; + } + let path = Path::new(crate::VOLUMES).join(dep).join(&shared).join(name); + if path.exists() { + tokio::fs::remove_dir_all( + Path::new(crate::VOLUMES).join(dep).join(&shared).join(name), + ) + .await?; + } + } + } + } + tokio::fs::remove_dir_all(Path::new(crate::VOLUMES).join(name)).await?; + log::info!("Pruning unused docker images."); + crate::ensure_code!( + std::process::Command::new("docker") + .args(&["image", "prune", "-a", "-f"]) + .stdout(std::process::Stdio::null()) + .stderr(match log::max_level() { + log::LevelFilter::Error => std::process::Stdio::null(), + _ => std::process::Stdio::inherit(), + }) + .status()? + .success(), + crate::error::DOCKER_ERROR, + "Failed to Prune Docker Images" + ); + }; + + Ok(res) +} diff --git a/appmgr/src/tor.rs b/appmgr/src/tor.rs new file mode 100644 index 000000000..a565aa585 --- /dev/null +++ b/appmgr/src/tor.rs @@ -0,0 +1,414 @@ +use std::collections::{BTreeSet, HashMap}; +use std::net::Ipv4Addr; +use std::os::unix::process::ExitStatusExt; +use std::path::Path; +use std::time::{Duration, Instant}; + +use failure::ResultExt as _; +use tokio::io::AsyncReadExt; +use tokio::io::AsyncWriteExt; + +use crate::util::{PersistencePath, YamlUpdateHandle}; +use crate::{Error, ResultExt as _}; + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +pub struct PortMapping { + pub internal: u16, + pub tor: u16, +} + +pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc"; +pub const HIDDEN_SERVICE_DIR_ROOT: &'static str = "/var/lib/tor"; + +#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "lowercase")] +pub enum HiddenServiceVersion { + V1, + V2, + V3, +} +impl From for usize { + fn from(v: HiddenServiceVersion) -> Self { + match v { + HiddenServiceVersion::V1 => 1, + HiddenServiceVersion::V2 => 2, + HiddenServiceVersion::V3 => 3, + } + } +} +impl std::convert::TryFrom for HiddenServiceVersion { + type Error = failure::Error; + fn try_from(v: usize) -> Result { + Ok(match v { + 1 => HiddenServiceVersion::V1, + 2 => HiddenServiceVersion::V2, + 3 => HiddenServiceVersion::V3, + n => bail!("Invalid HiddenServiceVersion {}", n), + }) + } +} +impl Default for HiddenServiceVersion { + fn default() -> Self { + HiddenServiceVersion::V3 + } +} +impl std::fmt::Display for HiddenServiceVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "HiddenServiceVersion {}", usize::from(*self)) + } +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct Service { + pub ip: Ipv4Addr, + pub ports: Vec, + #[serde(default)] + pub hidden_service_version: HiddenServiceVersion, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct NewService { + pub ports: Vec, + #[serde(default)] + pub hidden_service_version: HiddenServiceVersion, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct ServicesMap { + pub map: HashMap, + pub ips: BTreeSet, +} +impl Default for ServicesMap { + fn default() -> Self { + ServicesMap { + map: Default::default(), + ips: Default::default(), + } + } +} +impl ServicesMap { + pub fn add(&mut self, name: String, service: NewService) -> Ipv4Addr { + let ip = self + .map + .get(&name) + .map(|a| a.ip.clone()) + .unwrap_or_else(|| { + Ipv4Addr::from( + u32::from( + self.ips + .range(..) + .next_back() + .cloned() + .unwrap_or_else(|| crate::HOST_IP.into()), + ) + 1, + ) + }); + self.ips.insert(ip); + self.map.insert( + name, + Service { + ip, + ports: service.ports, + hidden_service_version: service.hidden_service_version, + }, + ); + ip + } + pub fn remove(&mut self, name: &str) { + let s = self.map.remove(name); + if let Some(s) = s { + self.ips.remove(&s.ip); + } + } +} + +pub async fn services_map(path: &PersistencePath) -> Result { + let f = path.maybe_read(false).await.transpose()?; + if let Some(mut f) = f { + crate::util::from_yaml_async_reader(&mut *f).await + } else { + Ok(Default::default()) + } +} + +pub async fn services_map_mut( + path: PersistencePath, +) -> Result, Error> { + YamlUpdateHandle::new_or_default(path).await +} + +pub async fn write_services(hidden_services: &ServicesMap) -> Result<(), Error> { + tokio::fs::copy(crate::TOR_RC, ETC_TOR_RC) + .await + .with_context(|e| format!("{} -> {}: {}", crate::TOR_RC, ETC_TOR_RC, e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + let mut f = tokio::fs::OpenOptions::new() + .append(true) + .open(ETC_TOR_RC) + .await?; + f.write_all(b"\n").await?; + for (name, service) in &hidden_services.map { + if service.ports.is_empty() { + continue; + } + f.write_all(b"\n").await?; + f.write_all(format!("# HIDDEN SERVICE FOR {}\n", name).as_bytes()) + .await?; + f.write_all( + format!( + "HiddenServiceDir {}/app-{}/\n", + HIDDEN_SERVICE_DIR_ROOT, name + ) + .as_bytes(), + ) + .await?; + f.write_all(format!("{}\n", service.hidden_service_version).as_bytes()) + .await?; + for port in &service.ports { + f.write_all( + format!( + "HiddenServicePort {} {}:{}\n", + port.tor, service.ip, port.internal + ) + .as_bytes(), + ) + .await?; + } + f.write_all(b"\n").await?; + } + Ok(()) +} + +pub async fn read_tor_address(name: &str, timeout: Option) -> Result { + log::info!("Retrieving Tor hidden service address for {}.", name); + let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT) + .join(format!("app-{}", name)) + .join("hostname"); + if let Some(timeout) = timeout { + let start = Instant::now(); + while { + if addr_path.exists() { + false + } else { + if start.elapsed() >= timeout { + log::warn!("Timed out waiting for tor to start."); + false + } else { + true + } + } + } { + tokio::time::delay_for(Duration::from_millis(100)).await; + } + } + let tor_addr = match tokio::fs::read_to_string(&addr_path).await { + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(e) + .with_context(|e| format!("{}: {}", addr_path.display(), e)) + .with_code(crate::error::NOT_FOUND), + a => a + .with_context(|e| format!("{}: {}", addr_path.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR), + }?; + Ok(tor_addr.trim().to_owned()) +} + +pub async fn read_tor_key( + name: &str, + version: HiddenServiceVersion, + timeout: Option, +) -> Result { + log::info!("Retrieving Tor hidden service address for {}.", name); + let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT) + .join(format!("app-{}", name)) + .join(match version { + HiddenServiceVersion::V3 => "hs_ed25519_secret_key", + _ => "private_key", + }); + if let Some(timeout) = timeout { + let start = Instant::now(); + while { + if addr_path.exists() { + false + } else { + if start.elapsed() >= timeout { + log::warn!("Timed out waiting for tor to start."); + false + } else { + true + } + } + } { + tokio::time::delay_for(Duration::from_millis(100)).await; + } + } + let tor_key = match version { + HiddenServiceVersion::V3 => { + let mut f = tokio::fs::File::open(&addr_path) + .await + .with_context(|e| format!("{}: {}", e, addr_path.display())) + .with_code(crate::error::FILESYSTEM_ERROR)?; + let mut buf = [0; 96]; + f.read_exact(&mut buf).await?; + base32::encode(base32::Alphabet::RFC4648 { padding: false }, &buf[32..]).to_lowercase() + } + _ => tokio::fs::read_to_string(&addr_path) + .await + .with_context(|e| format!("{}: {}", e, addr_path.display())) + .with_code(crate::error::FILESYSTEM_ERROR)? + .trim_end_matches("\u{0}") + .to_string(), + }; + Ok(tor_key.trim().to_owned()) +} + +pub async fn set_svc( + name: &str, + service: NewService, +) -> Result<(Ipv4Addr, Option, Option), Error> { + log::info!( + "Adding Tor hidden service {} to {}.", + name, + crate::SERVICES_YAML + ); + let is_listening = !service.ports.is_empty(); + let path = PersistencePath::from_ref(crate::SERVICES_YAML); + let mut hidden_services = services_map_mut(path).await?; + let ver = service.hidden_service_version; + let ip = hidden_services.add(name.to_owned(), service); + log::info!("Adding Tor hidden service {} to {}.", name, ETC_TOR_RC); + write_services(&hidden_services).await?; + hidden_services.commit().await?; + log::info!("Reloading Tor."); + let svc_exit = std::process::Command::new("service") + .args(&["tor", "reload"]) + .status()?; + crate::ensure_code!( + svc_exit.success(), + crate::error::GENERAL_ERROR, + "Failed to Reload Tor: {}", + svc_exit + .code() + .or_else(|| { svc_exit.signal().map(|a| 128 + a) }) + .unwrap_or(0) + ); + Ok(( + ip, + if is_listening { + Some(read_tor_address(name, Some(Duration::from_secs(30))).await?) + } else { + None + }, + if is_listening { + Some(read_tor_key(name, ver, Some(Duration::from_secs(30))).await?) + } else { + None + }, + )) +} + +pub async fn rm_svc(name: &str) -> Result<(), Error> { + log::info!( + "Removing Tor hidden service {} from {}.", + name, + crate::SERVICES_YAML + ); + let path = PersistencePath::from_ref(crate::SERVICES_YAML); + let mut hidden_services = services_map_mut(path).await?; + hidden_services.remove(name); + let hidden_service_path = Path::new(HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", name)); + log::info!("Removing {}", hidden_service_path.display()); + if hidden_service_path.exists() { + tokio::fs::remove_dir_all(hidden_service_path).await?; + } + log::info!("Removing Tor hidden service {} from {}.", name, ETC_TOR_RC); + write_services(&hidden_services).await?; + hidden_services.commit().await?; + log::info!("Reloading Tor."); + let svc_exit = std::process::Command::new("service") + .args(&["tor", "reload"]) + .status()?; + crate::ensure_code!( + svc_exit.success(), + crate::error::GENERAL_ERROR, + "Failed to Reload Tor: {}", + svc_exit.code().unwrap_or(0) + ); + Ok(()) +} + +pub async fn change_key( + name: &str, + key: Option<&ed25519_dalek::ExpandedSecretKey>, +) -> Result<(), Error> { + let hidden_service_path = Path::new(HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", name)); + log::info!("Removing {}", hidden_service_path.display()); + if hidden_service_path.exists() { + tokio::fs::remove_dir_all(&hidden_service_path) + .await + .with_context(|e| format!("{}: {}", hidden_service_path.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + } + if let Some(key) = key { + tokio::fs::create_dir_all(&hidden_service_path).await?; + let key_path = hidden_service_path.join("hs_ed25519_secret_key"); + let mut key_data = b"== ed25519v1-secret: type0 ==".to_vec(); + key_data.extend_from_slice(&key.to_bytes()); + tokio::fs::write(&key_path, key_data) + .await + .with_context(|e| format!("{}: {}", key_path.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + } + log::info!("Reloading Tor."); + let svc_exit = std::process::Command::new("service") + .args(&["tor", "reload"]) + .status()?; + crate::ensure_code!( + svc_exit.success(), + crate::error::GENERAL_ERROR, + "Failed to Reload Tor: {}", + svc_exit.code().unwrap_or(0) + ); + let mut info = crate::apps::list_info_mut().await?; + if let Some(mut i) = info.get_mut(name) { + if i.tor_address.is_some() { + i.tor_address = Some(read_tor_address(name, Some(Duration::from_secs(30))).await?); + } + } + Ok(()) +} + +pub async fn reload() -> Result<(), Error> { + let path = PersistencePath::from_ref(crate::SERVICES_YAML); + let hidden_services = services_map(&path).await?; + log::info!("Syncing Tor hidden services to {}.", ETC_TOR_RC); + write_services(&hidden_services).await?; + log::info!("Reloading Tor."); + let svc_exit = std::process::Command::new("service") + .args(&["tor", "reload"]) + .status()?; + crate::ensure_code!( + svc_exit.success(), + crate::error::GENERAL_ERROR, + "Failed to Reload Tor: {}", + svc_exit.code().unwrap_or(0) + ); + Ok(()) +} + +pub async fn restart() -> Result<(), Error> { + let path = PersistencePath::from_ref(crate::SERVICES_YAML); + let hidden_services = services_map(&path).await?; + log::info!("Syncing Tor hidden services to {}.", ETC_TOR_RC); + write_services(&hidden_services).await?; + log::info!("Restarting Tor."); + let svc_exit = std::process::Command::new("service") + .args(&["tor", "restart"]) + .status()?; + crate::ensure_code!( + svc_exit.success(), + crate::error::GENERAL_ERROR, + "Failed to Restart Tor: {}", + svc_exit.code().unwrap_or(0) + ); + Ok(()) +} diff --git a/appmgr/src/update.rs b/appmgr/src/update.rs new file mode 100644 index 000000000..1b8ab598c --- /dev/null +++ b/appmgr/src/update.rs @@ -0,0 +1,80 @@ +use linear_map::LinearMap; + +use crate::dependencies::{DependencyError, TaggedDependencyError}; +use crate::Error; +use crate::ResultExt as _; + +pub async fn update( + name_version: &str, + dry_run: bool, +) -> Result, Error> { + let mut name_version_iter = name_version.split("@"); + let name = name_version_iter.next().unwrap(); + let version_req = name_version_iter + .next() + .map(|v| v.parse()) + .transpose() + .no_code()? + .unwrap_or_else(emver::VersionRange::any); + let version = crate::registry::version(name, &version_req).await?; + let mut res = LinearMap::new(); + for dependent in crate::apps::dependents(name, false).await? { + if crate::apps::status(&dependent).await?.status != crate::apps::DockerStatus::Stopped { + let manifest = crate::apps::manifest(&dependent).await?; + match manifest.dependencies.0.get(name) { + Some(dep) if !version.satisfies(&dep.version) => { + crate::control::stop_dependents( + &dependent, + dry_run, + DependencyError::NotRunning, + &mut res, + ) + .await?; + if crate::apps::status(name).await?.status != crate::apps::DockerStatus::Stopped + { + crate::control::stop_app(&dependent, false, dry_run).await?; + res.insert( + dependent, + TaggedDependencyError { + dependency: name.to_owned(), + error: DependencyError::IncorrectVersion { + expected: version_req.clone(), + received: version.clone(), + }, + }, + ); + } + } + _ => { + crate::control::stop_dependents( + &dependent, + dry_run, + DependencyError::NotRunning, + &mut res, + ) + .await?; + if crate::apps::status(name).await?.status != crate::apps::DockerStatus::Stopped + { + crate::control::stop_app(&dependent, false, dry_run).await?; + res.insert( + dependent, + TaggedDependencyError { + dependency: name.to_owned(), + error: DependencyError::NotRunning, + }, + ); + } + } + } + } + } + if dry_run { + return Ok(res); + } + let download_path = crate::install::download_name(name_version).await?; + crate::remove::remove(name, false, false).await?; + crate::install::install_path(download_path, Some(name)).await?; + crate::apps::set_recoverable(name, false).await?; + + Ok(res) +} diff --git a/appmgr/src/util.rs b/appmgr/src/util.rs new file mode 100644 index 000000000..18006a9bf --- /dev/null +++ b/appmgr/src/util.rs @@ -0,0 +1,553 @@ +use std::fmt; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; + +use failure::ResultExt as _; +use file_lock::FileLock; +use tokio::fs::File; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +use crate::Error; +use crate::ResultExt as _; + +#[derive(Debug, Clone)] +pub struct PersistencePath(PathBuf); +impl PersistencePath { + pub fn from_ref>(p: P) -> Self { + let path = p.as_ref(); + PersistencePath(if path.has_root() { + path.strip_prefix("/").unwrap().to_owned() + } else { + path.to_owned() + }) + } + + pub fn new(path: PathBuf) -> Self { + PersistencePath(if path.has_root() { + path.strip_prefix("/").unwrap().to_owned() + } else { + path.to_owned() + }) + } + + pub fn join>(&self, path: P) -> Self { + PersistencePath::new(self.0.join(path)) + } + + pub fn tmp(&self) -> PathBuf { + Path::new(crate::TMP_DIR).join(&self.0) + } + + pub fn path(&self) -> PathBuf { + Path::new(crate::PERSISTENCE_DIR).join(&self.0) + } + + pub async fn lock(&self, for_update: bool) -> Result { + let path = self.path(); + let lock_path = format!("{}.lock", path.display()); + if tokio::fs::metadata(Path::new(&lock_path)).await.is_err() { + // !exists + tokio::fs::File::create(&lock_path) + .await + .with_context(|e| format!("{}: {}", lock_path, e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + } + let lock = lock_file(lock_path.clone(), for_update) + .await + .with_context(|e| format!("{}: {}", lock_path, e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + Ok(lock) + } + + pub async fn exists(&self) -> bool { + tokio::fs::metadata(self.path()).await.is_ok() + } + + pub async fn maybe_read(&self, for_update: bool) -> Option> { + if self.exists().await { + // exists + Some(self.read(for_update).await) + } else { + None + } + } + + pub async fn read(&self, for_update: bool) -> Result { + let path = self.path(); + let lock = self.lock(for_update).await?; + let file = File::open(&path) + .await + .with_context(|e| format!("{}: {}", path.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + Ok(PersistenceFile::new(file, lock, None)) + } + + pub async fn write(&self, lock: Option) -> Result { + let path = self.path(); + if let Some(parent) = path.parent() { + if tokio::fs::metadata(parent).await.is_err() { + // !exists + tokio::fs::create_dir_all(parent).await?; + } + } + let lock = if let Some(lock) = lock { + lock + } else { + self.lock(true).await? + }; + Ok({ + let path = self.tmp(); + if let Some(parent) = path.parent() { + if tokio::fs::metadata(parent).await.is_err() { + // !exists + tokio::fs::create_dir_all(parent).await?; + } + } + PersistenceFile::new(File::create(path).await?, lock, Some(self.clone())) + }) + } + + pub async fn for_update(self) -> Result, Error> { + UpdateHandle::new(self).await + } +} + +#[derive(Debug)] +pub struct PersistenceFile { + file: Option, + lock: Option, + needs_commit: Option, +} +impl PersistenceFile { + pub fn new(file: File, lock: FileLock, needs_commit: Option) -> Self { + PersistenceFile { + file: Some(file), + lock: Some(lock), + needs_commit, + } + } + + pub fn take_lock(&mut self) -> Option { + self.lock.take() + } + + /// Commits the file to the persistence directory. + /// If this fails, the file was not saved. + pub async fn commit(mut self) -> Result<(), Error> { + if let Some(mut file) = self.file.take() { + file.flush().await?; + file.shutdown().await?; + drop(file); + } + if let Some(path) = self.needs_commit.take() { + tokio::fs::rename(path.tmp(), path.path()) + .await + .with_context(|e| { + format!( + "{} -> {}: {}", + path.tmp().display(), + path.path().display(), + e + ) + }) + .with_code(crate::error::FILESYSTEM_ERROR)?; + if let Some(lock) = self.lock.take() { + unlock(lock) + .await + .with_context(|e| format!("{}.lock: {}", path.path().display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + tokio::fs::remove_file(format!("{}.lock", path.path().display())) + .await + .with_context(|e| format!("{}.lock: {}", path.path().display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + } + + Ok(()) + } else { + Ok(()) + } + } +} +impl std::ops::Deref for PersistenceFile { + type Target = File; + + fn deref(&self) -> &Self::Target { + self.file.as_ref().unwrap() + } +} +impl std::ops::DerefMut for PersistenceFile { + fn deref_mut(&mut self) -> &mut Self::Target { + self.file.as_mut().unwrap() + } +} +impl AsRef for PersistenceFile { + fn as_ref(&self) -> &File { + &*self + } +} +impl AsMut for PersistenceFile { + fn as_mut(&mut self) -> &mut File { + &mut *self + } +} +impl Drop for PersistenceFile { + fn drop(&mut self) { + if let Some(path) = &self.needs_commit { + log::warn!( + "{} was dropped without being committed.", + path.path().display() + ); + } + } +} + +pub trait UpdateHandleMode {} +pub struct ForRead; +impl UpdateHandleMode for ForRead {} +pub struct ForWrite; +impl UpdateHandleMode for ForWrite {} + +pub struct UpdateHandle { + path: PersistencePath, + file: PersistenceFile, + mode: PhantomData, +} +impl UpdateHandle { + pub async fn new(path: PersistencePath) -> Result { + if !path.path().exists() { + tokio::fs::File::create(path.path()).await?; + } + Ok(UpdateHandle { + file: path.read(true).await?, + path, + mode: PhantomData, + }) + } + + pub async fn into_writer(mut self) -> Result, Error> { + let lock = self.file.take_lock(); + Ok(UpdateHandle { + file: self.path.write(lock).await?, + path: self.path, + mode: PhantomData, + }) + } +} + +impl UpdateHandle { + pub async fn commit(self) -> Result<(), Error> { + self.file.commit().await + } +} + +impl tokio::io::AsyncRead for UpdateHandle { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll> { + unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) }.poll_read(cx, buf) + } +} + +impl tokio::io::AsyncWrite for UpdateHandle { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + tokio::io::AsyncWrite::poll_write( + unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) }, + cx, + buf, + ) + } + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + tokio::io::AsyncWrite::poll_flush( + unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) }, + cx, + ) + } + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + tokio::io::AsyncWrite::poll_shutdown( + unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) }, + cx, + ) + } +} + +pub struct YamlUpdateHandle serde::Deserialize<'de>> { + inner: T, + handle: UpdateHandle, + committed: bool, +} +impl YamlUpdateHandle +where + T: serde::Serialize + for<'de> serde::Deserialize<'de>, +{ + pub async fn new(path: PersistencePath) -> Result { + let mut handle = path.for_update().await?; + let inner = from_yaml_async_reader(&mut handle).await?; + Ok(YamlUpdateHandle { + inner, + handle, + committed: false, + }) + } + + pub async fn commit(mut self) -> Result<(), Error> { + let mut file = self.handle.into_writer().await?; + to_yaml_async_writer(&mut file, &self.inner) + .await + .no_code()?; + file.commit().await?; + self.committed = true; + Ok(()) + } +} + +impl YamlUpdateHandle +where + T: serde::Serialize + for<'de> serde::Deserialize<'de> + Default, +{ + pub async fn new_or_default(path: PersistencePath) -> Result { + if !path.path().exists() { + Ok(YamlUpdateHandle { + inner: Default::default(), + handle: path.for_update().await?, + committed: false, + }) + } else { + Self::new(path).await + } + } +} + +impl std::ops::Deref for YamlUpdateHandle +where + T: serde::Serialize + for<'de> serde::Deserialize<'de> + Default, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} +impl std::ops::DerefMut for YamlUpdateHandle +where + T: serde::Serialize + for<'de> serde::Deserialize<'de> + Default, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +#[derive(Clone, Debug)] +pub enum Never {} +pub fn absurd(lol: Never) -> T { + match lol {} +} +impl fmt::Display for Never { + fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result { + absurd(self.clone()) + } +} +impl failure::Fail for Never {} + +#[derive(Clone, Debug)] +pub struct AsyncCompat(pub T); +impl futures::io::AsyncRead for AsyncCompat +where + T: tokio::io::AsyncRead, +{ + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll> { + tokio::io::AsyncRead::poll_read(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx, buf) + } +} +impl tokio::io::AsyncRead for AsyncCompat +where + T: futures::io::AsyncRead, +{ + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll> { + futures::io::AsyncRead::poll_read(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx, buf) + } +} +impl futures::io::AsyncWrite for AsyncCompat +where + T: tokio::io::AsyncWrite, +{ + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + tokio::io::AsyncWrite::poll_write(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx, buf) + } + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + tokio::io::AsyncWrite::poll_flush(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx) + } + fn poll_close( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + tokio::io::AsyncWrite::poll_shutdown(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx) + } +} +impl tokio::io::AsyncWrite for AsyncCompat +where + T: futures::io::AsyncWrite, +{ + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + futures::io::AsyncWrite::poll_write( + unsafe { self.map_unchecked_mut(|a| &mut a.0) }, + cx, + buf, + ) + } + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + futures::io::AsyncWrite::poll_flush(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx) + } + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + futures::io::AsyncWrite::poll_close(unsafe { self.map_unchecked_mut(|a| &mut a.0) }, cx) + } +} + +pub async fn lock_file(filename: String, for_write: bool) -> std::io::Result { + tokio::task::spawn_blocking(move || FileLock::lock(&filename, true, for_write)).await? +} + +pub async fn unlock(lock: FileLock) -> std::io::Result<()> { + tokio::task::spawn_blocking(move || lock.unlock()).await? +} + +pub async fn from_yaml_async_reader(mut reader: R) -> Result +where + T: for<'de> serde::Deserialize<'de>, + R: AsyncRead + Unpin, +{ + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer).await?; + serde_yaml::from_slice(&buffer) + .map_err(failure::Error::from) + .with_code(crate::error::SERDE_ERROR) +} + +pub async fn to_yaml_async_writer(mut writer: W, value: &T) -> Result<(), crate::Error> +where + T: serde::Serialize, + W: AsyncWrite + Unpin, +{ + let mut buffer = serde_yaml::to_vec(value).with_code(crate::error::SERDE_ERROR)?; + buffer.extend_from_slice(b"\n"); + writer.write_all(&buffer).await?; + Ok(()) +} + +pub async fn from_cbor_async_reader(mut reader: R) -> Result +where + T: for<'de> serde::Deserialize<'de>, + R: AsyncRead + Unpin, +{ + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer).await?; + serde_cbor::from_slice(&buffer) + .map_err(failure::Error::from) + .with_code(crate::error::SERDE_ERROR) +} + +pub async fn from_json_async_reader(mut reader: R) -> Result +where + T: for<'de> serde::Deserialize<'de>, + R: AsyncRead + Unpin, +{ + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer).await?; + serde_json::from_slice(&buffer) + .map_err(failure::Error::from) + .with_code(crate::error::SERDE_ERROR) +} + +pub async fn to_json_async_writer(mut writer: W, value: &T) -> Result<(), crate::Error> +where + T: serde::Serialize, + W: AsyncWrite + Unpin, +{ + let buffer = serde_json::to_string(value).with_code(crate::error::SERDE_ERROR)?; + writer.write_all(&buffer.as_bytes()).await?; + Ok(()) +} + +pub async fn to_json_pretty_async_writer(mut writer: W, value: &T) -> Result<(), crate::Error> +where + T: serde::Serialize, + W: AsyncWrite + Unpin, +{ + let mut buffer = serde_json::to_string_pretty(value).with_code(crate::error::SERDE_ERROR)?; + buffer.push_str("\n"); + writer.write_all(&buffer.as_bytes()).await?; + Ok(()) +} + +#[async_trait::async_trait] +pub trait Invoke { + async fn invoke(&mut self, name: &str) -> Result, failure::Error>; +} +#[async_trait::async_trait] +impl Invoke for tokio::process::Command { + async fn invoke(&mut self, name: &str) -> Result, failure::Error> { + let res = self.output().await?; + ensure!( + res.status.success(), + "{} Error: {}", + name, + std::str::from_utf8(&res.stderr).unwrap_or("Unknown Error") + ); + Ok(res.stdout) + } +} + +pub trait Apply: Sized { + fn apply O>(self, func: F) -> O { + func(self) + } +} + +pub trait ApplyRef { + fn apply_ref O>(&self, func: F) -> O { + func(&self) + } + + fn apply_mut O>(&mut self, func: F) -> O { + func(self) + } +} + +impl Apply for T {} +impl ApplyRef for T {} diff --git a/appmgr/src/version/mod.rs b/appmgr/src/version/mod.rs new file mode 100644 index 000000000..26f89cccf --- /dev/null +++ b/appmgr/src/version/mod.rs @@ -0,0 +1,251 @@ +use std::cmp::Ordering; + +use async_trait::async_trait; +use failure::ResultExt as _; +use futures::stream::TryStreamExt; + +use crate::util::{to_yaml_async_writer, AsyncCompat, PersistencePath}; +use crate::Error; +use crate::ResultExt as _; + +mod v0_1_0; +mod v0_1_1; +mod v0_1_2; +mod v0_1_3; +mod v0_1_4; +mod v0_1_5; +mod v0_2_0; +mod v0_2_1; +mod v0_2_2; +mod v0_2_3; +mod v0_2_4; +mod v0_2_5; + +pub use v0_2_5::Version as Current; + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +enum Version { + V0_0_0(Wrapper<()>), + V0_1_0(Wrapper), + V0_1_1(Wrapper), + V0_1_2(Wrapper), + V0_1_3(Wrapper), + V0_1_4(Wrapper), + V0_1_5(Wrapper), + V0_2_0(Wrapper), + V0_2_1(Wrapper), + V0_2_2(Wrapper), + V0_2_3(Wrapper), + V0_2_4(Wrapper), + V0_2_5(Wrapper), + Other(emver::Version), +} + +#[async_trait] +pub trait VersionT +where + Self: Sized + Send + Sync, +{ + type Previous: VersionT; + fn new() -> Self; + fn semver(&self) -> &'static emver::Version; + async fn up(&self) -> Result<(), Error>; + async fn down(&self) -> Result<(), Error>; + async fn commit(&self) -> Result<(), Error> { + let mut out = PersistencePath::from_ref("version").write(None).await?; + to_yaml_async_writer(out.as_mut(), &self.semver()).await?; + out.commit().await?; + Ok(()) + } + async fn migrate_to(&self, version: &V) -> Result<(), Error> { + match self.semver().cmp(version.semver()) { + Ordering::Greater => self.rollback_to_unchecked(version).await, + Ordering::Less => version.migrate_from_unchecked(self).await, + Ordering::Equal => Ok(()), + } + } + async fn migrate_from_unchecked(&self, version: &V) -> Result<(), Error> { + let previous = Self::Previous::new(); + if version.semver() != previous.semver() { + previous.migrate_from_unchecked(version).await?; + } + log::info!("{} -> {}", previous.semver(), self.semver()); + self.up().await?; + self.commit().await?; + Ok(()) + } + async fn rollback_to_unchecked(&self, version: &V) -> Result<(), Error> { + let previous = Self::Previous::new(); + log::info!("{} -> {}", self.semver(), previous.semver()); + self.down().await?; + previous.commit().await?; + if version.semver() != previous.semver() { + previous.rollback_to_unchecked(version).await?; + } + Ok(()) + } +} +struct Wrapper(T); +impl serde::Serialize for Wrapper +where + T: VersionT, +{ + fn serialize(&self, serializer: S) -> Result { + self.0.semver().serialize(serializer) + } +} +impl<'de, T> serde::Deserialize<'de> for Wrapper +where + T: VersionT, +{ + fn deserialize>(deserializer: D) -> Result { + let v = emver::Version::deserialize(deserializer)?; + let version = T::new(); + if &v == version.semver() { + Ok(Wrapper(version)) + } else { + Err(serde::de::Error::custom("Mismatched Version")) + } + } +} +const V0_0_0: emver::Version = emver::Version::new(0, 0, 0, 0); +#[async_trait] +impl VersionT for () { + type Previous = (); + fn new() -> Self { + () + } + fn semver(&self) -> &'static emver::Version { + &V0_0_0 + } + async fn up(&self) -> Result<(), Error> { + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} + +pub async fn init() -> Result<(), failure::Error> { + let _lock = PersistencePath::from_ref("").lock(true).await?; + let vpath = PersistencePath::from_ref("version"); + if let Some(mut f) = vpath.maybe_read(false).await.transpose()? { + let v: Version = crate::util::from_yaml_async_reader(&mut *f).await?; + match v { + Version::V0_0_0(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_1_0(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_1_1(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_1_2(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_1_3(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_1_4(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_1_5(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_2_0(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_2_1(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_2_2(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_2_3(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_2_4(v) => v.0.migrate_to(&Current::new()).await?, + Version::V0_2_5(v) => v.0.migrate_to(&Current::new()).await?, + Version::Other(_) => (), + // TODO find some way to automate this? + } + } else { + ().migrate_to(&Current::new()).await?; + } + Ok(()) +} + +pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error> { + let req_str: String = format!("{}", requirement) + .chars() + .filter(|c| !c.is_whitespace()) + .collect(); + let url = format!("{}/appmgr?spec={}", &*crate::SYS_REGISTRY_URL, req_str); + log::info!("Fetching new version from {}", url); + let response = reqwest::get(&url) + .await + .with_code(crate::error::NETWORK_ERROR)? + .error_for_status() + .with_code(crate::error::REGISTRY_ERROR)?; + let tmp_appmgr_path = PersistencePath::from_ref("appmgr").tmp(); + if let Some(parent) = tmp_appmgr_path.parent() { + if !parent.exists() { + tokio::fs::create_dir_all(parent) + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + } + } + let mut f = tokio::fs::OpenOptions::new() + .create(true) + .write(true) + .open(&tmp_appmgr_path) + .await + .with_context(|e| format!("{}: {}", tmp_appmgr_path.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + tokio::io::copy( + &mut AsyncCompat( + response + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .into_async_read(), + ), + &mut f, + ) + .await + .no_code()?; + drop(f); + crate::ensure_code!( + tokio::process::Command::new("chmod") + .arg("700") + .arg(&tmp_appmgr_path) + .output() + .await? + .status + .success(), + crate::error::FILESYSTEM_ERROR, + "chmod failed" + ); + let out = std::process::Command::new(&tmp_appmgr_path) + .arg("semver") + .stdout(std::process::Stdio::piped()) + .spawn()? + .wait_with_output() + .with_context(|e| format!("{} semver: {}", tmp_appmgr_path.display(), e)) + .no_code()?; + let out_str = std::str::from_utf8(&out.stdout).no_code()?; + log::info!("Migrating to version {}", out_str); + let v: Version = serde_yaml::from_str(out_str) + .with_context(|e| format!("{}: {:?}", e, out_str)) + .with_code(crate::error::SERDE_ERROR)?; + match v { + Version::V0_0_0(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_1_0(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_1_1(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_1_2(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_1_3(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_1_4(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_1_5(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_2_0(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_2_1(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_2_2(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_2_3(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_2_4(v) => Current::new().migrate_to(&v.0).await?, + Version::V0_2_5(v) => Current::new().migrate_to(&v.0).await?, + Version::Other(_) => (), + // TODO find some way to automate this? + }; + let cur_path = std::path::Path::new("/usr/local/bin/appmgr"); + tokio::fs::rename(&tmp_appmgr_path, &cur_path) + .await + .with_context(|e| { + format!( + "{} -> {}: {}", + tmp_appmgr_path.display(), + cur_path.display(), + e + ) + }) + .with_code(crate::error::FILESYSTEM_ERROR)?; + + Ok(()) +} diff --git a/appmgr/src/version/v0_1_0.rs b/appmgr/src/version/v0_1_0.rs new file mode 100644 index 000000000..f2444d928 --- /dev/null +++ b/appmgr/src/version/v0_1_0.rs @@ -0,0 +1,278 @@ +use std::path::Path; + +use super::*; + +const V0_1_0: emver::Version = emver::Version::new(0, 1, 0, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = (); + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_1_0 + } + async fn up(&self) -> Result<(), Error> { + tokio::fs::create_dir_all(Path::new(crate::PERSISTENCE_DIR).join("tor")).await?; + tokio::fs::create_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps")).await?; + tokio::fs::create_dir_all(Path::new(crate::TMP_DIR).join("tor")).await?; + tokio::fs::create_dir_all(Path::new(crate::TMP_DIR).join("apps")).await?; + let mut outfile = legacy::util::PersistencePath::from_ref("tor/torrc") + .write() + .await?; + tokio::io::copy( + &mut AsyncCompat( + reqwest::get(&format!("{}/torrc?spec==0.0.0", &*crate::SYS_REGISTRY_URL)) + .await + .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) + .with_code(crate::error::NETWORK_ERROR)? + .error_for_status() + .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) + .with_code(crate::error::REGISTRY_ERROR)? + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .into_async_read(), + ), + outfile.as_mut(), + ) + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + outfile.commit().await?; + legacy::tor::set_svc( + "start9-agent", + legacy::tor::Service { + ports: vec![5959], + hidden_service_version: Default::default(), + }, + ) + .await + .no_code()?; + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} + +mod legacy { + pub mod tor { + use failure::{Error, ResultExt}; + use linear_map::LinearMap; + use tokio::io::AsyncWriteExt; + + use crate::tor::HiddenServiceVersion; + + use super::util::PersistencePath; + + pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc"; + pub const HIDDEN_SERVICE_DIR_ROOT: &'static str = "/var/lib/tor"; + + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] + pub struct Service { + pub ports: Vec, + pub hidden_service_version: HiddenServiceVersion, + } + + async fn services_map(path: &PersistencePath) -> Result, Error> { + use crate::util::Apply; + Ok(path + .maybe_read() + .await + .transpose()? + .map(crate::util::from_yaml_async_reader) + .apply(futures::future::OptionFuture::from) + .await + .transpose()? + .unwrap_or_else(LinearMap::new)) + } + + pub async fn write_services( + hidden_services: &LinearMap, + ) -> Result<(), Error> { + tokio::fs::copy(crate::TOR_RC, ETC_TOR_RC) + .await + .with_context(|e| format!("{} -> {}: {}", crate::TOR_RC, ETC_TOR_RC, e))?; + let mut f = tokio::fs::OpenOptions::new() + .append(true) + .open(ETC_TOR_RC) + .await?; + f.write("\n".as_bytes()).await?; + for (name, service) in hidden_services { + f.write("\n".as_bytes()).await?; + f.write(format!("# HIDDEN SERVICE FOR {}\n", name).as_bytes()) + .await?; + f.write( + format!( + "HiddenServiceDir {}/app-{}/\n", + HIDDEN_SERVICE_DIR_ROOT, name + ) + .as_bytes(), + ) + .await?; + f.write(format!("{}\n", service.hidden_service_version).as_bytes()) + .await?; + for port in &service.ports { + f.write(format!("HiddenServicePort {} 127.0.0.1:{}\n", port, port).as_bytes()) + .await?; + } + f.write("\n".as_bytes()).await?; + } + Ok(()) + } + + pub async fn set_svc(name: &str, service: Service) -> Result<(), Error> { + log::info!( + "Adding Tor hidden service {} to {}.", + name, + crate::SERVICES_YAML + ); + let path = PersistencePath::from_ref(crate::SERVICES_YAML); + let mut hidden_services = services_map(&path).await?; + hidden_services.insert(name.to_owned(), service); + let mut services_yaml = path.write().await?; + crate::util::to_yaml_async_writer(services_yaml.as_mut(), &hidden_services).await?; + services_yaml.write_all("\n".as_bytes()).await?; + services_yaml.commit().await?; + log::info!("Adding Tor hidden service {} to {}.", name, ETC_TOR_RC); + write_services(&hidden_services).await?; + log::info!("Restarting Tor."); + let svc_exit = std::process::Command::new("service") + .args(&["tor", "restart"]) + .status()?; + ensure!( + svc_exit.success(), + "Failed to Restart Tor: {}", + svc_exit.code().unwrap_or(0) + ); + Ok(()) + } + } + + pub mod util { + use std::path::{Path, PathBuf}; + use tokio::fs::File; + + use crate::Error; + use crate::ResultExt as _; + use failure::ResultExt as _; + + #[derive(Clone, Debug)] + pub struct PersistencePath(PathBuf); + impl PersistencePath { + pub fn from_ref>(p: P) -> Self { + let path = p.as_ref(); + PersistencePath(if path.has_root() { + path.strip_prefix("/").unwrap().to_owned() + } else { + path.to_owned() + }) + } + + pub fn tmp(&self) -> PathBuf { + Path::new(crate::TMP_DIR).join(&self.0) + } + + pub fn path(&self) -> PathBuf { + Path::new(crate::PERSISTENCE_DIR).join(&self.0) + } + + pub async fn maybe_read(&self) -> Option> { + let path = self.path(); + if path.exists() { + Some( + File::open(&path) + .await + .with_context(|e| format!("{}: {}", path.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR), + ) + } else { + None + } + } + + pub async fn write(&self) -> Result { + let path = self.path(); + if let Some(parent) = path.parent() { + if !parent.exists() { + tokio::fs::create_dir_all(parent).await?; + } + } + Ok(if path.exists() { + let path = self.tmp(); + if let Some(parent) = path.parent() { + if !parent.exists() { + tokio::fs::create_dir_all(parent).await?; + } + } + PersistenceFile::new(File::create(path).await?, Some(self.clone())) + } else { + PersistenceFile::new(File::create(path).await?, None) + }) + } + } + + #[derive(Debug)] + pub struct PersistenceFile { + file: File, + needs_commit: Option, + } + impl PersistenceFile { + pub fn new(file: File, needs_commit: Option) -> Self { + PersistenceFile { file, needs_commit } + } + /// Commits the file to the persistence directory. + /// If this fails, the file was not saved. + pub async fn commit(mut self) -> Result<(), Error> { + if let Some(path) = self.needs_commit.take() { + tokio::fs::rename(path.tmp(), path.path()) + .await + .with_context(|e| { + format!( + "{} -> {}: {}", + path.tmp().display(), + path.path().display(), + e + ) + }) + .with_code(crate::error::FILESYSTEM_ERROR) + } else { + Ok(()) + } + } + } + impl std::ops::Deref for PersistenceFile { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.file + } + } + impl std::ops::DerefMut for PersistenceFile { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.file + } + } + impl AsRef for PersistenceFile { + fn as_ref(&self) -> &File { + &*self + } + } + impl AsMut for PersistenceFile { + fn as_mut(&mut self) -> &mut File { + &mut *self + } + } + impl Drop for PersistenceFile { + fn drop(&mut self) { + if let Some(path) = &self.needs_commit { + log::warn!( + "{} was dropped without being committed.", + path.path().display() + ); + } + } + } + } +} diff --git a/appmgr/src/version/v0_1_1.rs b/appmgr/src/version/v0_1_1.rs new file mode 100644 index 000000000..3bc1c8b62 --- /dev/null +++ b/appmgr/src/version/v0_1_1.rs @@ -0,0 +1,202 @@ +use std::path::Path; + +use super::*; + +const V0_1_1: emver::Version = emver::Version::new(0, 1, 1, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_1_0::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_1_1 + } + async fn up(&self) -> Result<(), Error> { + log::info!("Update torrc"); + let mut outfile = crate::util::PersistencePath::from_ref("tor/torrc") + .write(None) + .await?; + tokio::io::copy( + &mut AsyncCompat( + reqwest::get(&format!("{}/torrc?spec==0.1.1", &*crate::SYS_REGISTRY_URL)) + .await + .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) + .with_code(crate::error::NETWORK_ERROR)? + .error_for_status() + .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) + .with_code(crate::error::REGISTRY_ERROR)? + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .into_async_read(), + ), + outfile.as_mut(), + ) + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + outfile.commit().await?; + if !std::process::Command::new("docker") + .arg("network") + .arg("create") + .arg("-d") + .arg("bridge") + .arg("--subnet=172.18.0.0/16") + .arg("start9") + .stdout(std::process::Stdio::null()) + .status()? + .success() + { + log::warn!("Failed to Create Network") + } + + match tokio::fs::remove_file(Path::new(crate::PERSISTENCE_DIR).join(crate::SERVICES_YAML)) + .await + { + Ok(_) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(e), + } + .with_context(|e| format!("{}/{}: {}", crate::PERSISTENCE_DIR, crate::SERVICES_YAML, e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + crate::tor::reload().await?; + + for app in crate::apps::list_info().await? { + legacy::update::update(&app.0).await?; + } + + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + let mut outfile = crate::util::PersistencePath::from_ref("tor/torrc") + .write(None) + .await?; + + tokio::io::copy( + &mut AsyncCompat( + reqwest::get(&format!("{}/torrc?spec==0.1.0", &*crate::SYS_REGISTRY_URL)) + .await + .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) + .no_code()? + .error_for_status() + .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) + .no_code()? + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .into_async_read(), + ), + outfile.as_mut(), + ) + .await + .with_code(crate::error::FILESYSTEM_ERROR)?; + outfile.commit().await?; + + for app in crate::apps::list_info().await? { + legacy::remove::remove(&app.0, false).await?; + } + let tor_svcs = crate::util::PersistencePath::from_ref(crate::SERVICES_YAML).path(); + if tor_svcs.exists() { + tokio::fs::remove_file(&tor_svcs) + .await + .with_context(|e| format!("{}: {}", tor_svcs.display(), e)) + .with_code(crate::error::FILESYSTEM_ERROR)?; + } + if !std::process::Command::new("docker") + .arg("network") + .arg("rm") + .arg("start9") + .stdout(std::process::Stdio::null()) + .status()? + .success() + { + log::warn!("Failed to Remove Network"); + } + + Ok(()) + } +} + +mod legacy { + pub mod remove { + use std::path::Path; + + use crate::Error; + + pub async fn remove(name: &str, purge: bool) -> Result<(), Error> { + log::info!("Removing app from manifest."); + crate::apps::remove(name).await?; + log::info!("Stopping docker container."); + if !tokio::process::Command::new("docker") + .args(&["stop", name]) + .stdout(std::process::Stdio::null()) + .stderr(match log::max_level() { + log::LevelFilter::Error => std::process::Stdio::null(), + _ => std::process::Stdio::inherit(), + }) + .status() + .await? + .success() + { + log::error!("Failed to Stop Docker Container"); + }; + log::info!("Removing docker container."); + if !tokio::process::Command::new("docker") + .args(&["rm", name]) + .stdout(std::process::Stdio::null()) + .stderr(match log::max_level() { + log::LevelFilter::Error => std::process::Stdio::null(), + _ => std::process::Stdio::inherit(), + }) + .status() + .await? + .success() + { + log::error!("Failed to Remove Docker Container"); + }; + if purge { + log::info!("Removing tor hidden service."); + crate::tor::rm_svc(name).await?; + log::info!("Removing app metadata."); + std::fs::remove_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps").join(name))?; + log::info!("Destroying mounted volume."); + std::fs::remove_dir_all(Path::new(crate::VOLUMES).join(name))?; + log::info!("Pruning unused docker images."); + crate::ensure_code!( + std::process::Command::new("docker") + .args(&["image", "prune", "-a", "-f"]) + .stdout(std::process::Stdio::null()) + .stderr(match log::max_level() { + log::LevelFilter::Error => std::process::Stdio::null(), + _ => std::process::Stdio::inherit(), + }) + .status()? + .success(), + 3, + "Failed to Prune Docker Images" + ); + }; + + Ok(()) + } + } + pub mod update { + use crate::Error; + pub async fn update(name_version: &str) -> Result<(), Error> { + let name = name_version + .split("@") + .next() + .ok_or_else(|| failure::format_err!("invalid app id"))?; + crate::install::download_name(name_version).await?; + super::remove::remove(name, false).await?; + crate::install::install_name(name_version, true).await?; + let config = crate::apps::config(name).await?; + if let Some(cfg) = config.config { + if config.spec.matches(&cfg).is_ok() { + crate::apps::set_configured(name, true).await?; + } + } + Ok(()) + } + } +} diff --git a/appmgr/src/version/v0_1_2.rs b/appmgr/src/version/v0_1_2.rs new file mode 100644 index 000000000..1f41809dd --- /dev/null +++ b/appmgr/src/version/v0_1_2.rs @@ -0,0 +1,104 @@ +use futures::StreamExt; +use futures::TryStreamExt; +use linear_map::LinearMap; + +use super::*; + +const V0_1_2: emver::Version = emver::Version::new(0, 1, 2, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_1_1::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_1_2 + } + async fn up(&self) -> Result<(), Error> { + let app_info = legacy::apps::list_info().await?; + for (name, _) in &app_info { + let p = PersistencePath::from_ref("apps") + .join(name) + .join("manifest.yaml"); + let mut f = p.for_update().await?; + let manifest: crate::manifest::ManifestV0 = crate::util::from_yaml_async_reader(&mut f) + .await + .no_code()?; + let mut f = f.into_writer().await?; + crate::util::to_yaml_async_writer(&mut f, &crate::manifest::Manifest::V0(manifest)) + .await + .no_code()?; + f.commit().await?; + } + + let p = PersistencePath::from_ref("apps.yaml"); + let exists = p.path().exists(); + let mut f = p.for_update().await?; + let info: LinearMap = if exists { + crate::util::from_yaml_async_reader(&mut f) + .await + .no_code()? + } else { + LinearMap::new() + }; + let new_info: LinearMap = futures::stream::iter(info) + .then(|(name, i)| async move { + let title = crate::apps::manifest(&name).await?.title; + Ok::<_, Error>(( + name, + crate::apps::AppInfo { + title, + version: i.version, + tor_address: i.tor_address, + configured: i.configured, + recoverable: false, + needs_restart: false, + }, + )) + }) + .try_collect() + .await?; + let mut f = f.into_writer().await?; + crate::util::to_yaml_async_writer(&mut f, &new_info) + .await + .no_code()?; + f.commit().await?; + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} + +mod legacy { + pub mod apps { + use linear_map::LinearMap; + + use crate::util::from_yaml_async_reader; + use crate::util::Apply; + use crate::util::PersistencePath; + use crate::Error; + + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + pub struct AppInfo { + pub version: emver::Version, + pub tor_address: Option, + pub configured: bool, + } + + pub async fn list_info() -> Result, Error> { + let apps_path = PersistencePath::from_ref("apps.yaml"); + Ok(apps_path + .maybe_read(false) + .await + .transpose()? + .map(|mut f| async move { from_yaml_async_reader(&mut *f).await }) + .apply(futures::future::OptionFuture::from) + .await + .transpose()? + .unwrap_or_else(LinearMap::new)) + } + } +} diff --git a/appmgr/src/version/v0_1_3.rs b/appmgr/src/version/v0_1_3.rs new file mode 100644 index 000000000..2a5084adf --- /dev/null +++ b/appmgr/src/version/v0_1_3.rs @@ -0,0 +1,21 @@ +use super::*; + +const V0_1_3: emver::Version = emver::Version::new(0, 1, 3, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_1_2::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_1_3 + } + async fn up(&self) -> Result<(), Error> { + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} diff --git a/appmgr/src/version/v0_1_4.rs b/appmgr/src/version/v0_1_4.rs new file mode 100644 index 000000000..df302c88c --- /dev/null +++ b/appmgr/src/version/v0_1_4.rs @@ -0,0 +1,21 @@ +use super::*; + +const V0_1_4: emver::Version = emver::Version::new(0, 1, 4, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_1_3::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_1_4 + } + async fn up(&self) -> Result<(), Error> { + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} diff --git a/appmgr/src/version/v0_1_5.rs b/appmgr/src/version/v0_1_5.rs new file mode 100644 index 000000000..5ab455b58 --- /dev/null +++ b/appmgr/src/version/v0_1_5.rs @@ -0,0 +1,21 @@ +use super::*; + +const V0_1_5: emver::Version = emver::Version::new(0, 1, 5, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_1_4::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_1_5 + } + async fn up(&self) -> Result<(), Error> { + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} diff --git a/appmgr/src/version/v0_2_0.rs b/appmgr/src/version/v0_2_0.rs new file mode 100644 index 000000000..db5607fd1 --- /dev/null +++ b/appmgr/src/version/v0_2_0.rs @@ -0,0 +1,98 @@ +use linear_map::LinearMap; + +use super::*; +use crate::util::{to_yaml_async_writer, PersistencePath}; + +const V0_2_0: emver::Version = emver::Version::new(0, 2, 0, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_1_5::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_2_0 + } + async fn up(&self) -> Result<(), Error> { + let app_info: LinearMap = legacy::apps::list_info() + .await? + .into_iter() + .map(|(id, ai)| { + ( + id, + crate::apps::AppInfo { + title: ai.title, + version: ai.version, + tor_address: ai.tor_address, + configured: ai.configured, + recoverable: ai.recoverable, + needs_restart: false, + }, + ) + }) + .collect(); + let mut apps_file = PersistencePath::from_ref("apps.yaml").write(None).await?; + to_yaml_async_writer(&mut *apps_file, &app_info).await?; + apps_file.commit().await?; + + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + let app_info: LinearMap = crate::apps::list_info() + .await? + .into_iter() + .map(|(id, ai)| { + ( + id, + legacy::apps::AppInfo { + title: ai.title, + version: ai.version, + tor_address: ai.tor_address, + configured: ai.configured, + recoverable: ai.recoverable, + }, + ) + }) + .collect(); + let mut apps_file = PersistencePath::from_ref("apps.yaml").write(None).await?; + to_yaml_async_writer(&mut *apps_file, &app_info).await?; + apps_file.commit().await?; + + Ok(()) + } +} + +mod legacy { + pub mod apps { + use linear_map::LinearMap; + + use crate::util::{from_yaml_async_reader, PersistencePath}; + use crate::Error; + + fn not(b: &bool) -> bool { + !b + } + + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] + pub struct AppInfo { + pub title: String, + pub version: emver::Version, + pub tor_address: Option, + pub configured: bool, + #[serde(default)] + #[serde(skip_serializing_if = "not")] + pub recoverable: bool, + } + + pub async fn list_info() -> Result, Error> { + let apps_path = PersistencePath::from_ref("apps.yaml"); + let mut f = match apps_path.maybe_read(false).await.transpose()? { + Some(a) => a, + None => return Ok(LinearMap::new()), + }; + from_yaml_async_reader(&mut *f).await + } + } +} diff --git a/appmgr/src/version/v0_2_1.rs b/appmgr/src/version/v0_2_1.rs new file mode 100644 index 000000000..e4b8ae954 --- /dev/null +++ b/appmgr/src/version/v0_2_1.rs @@ -0,0 +1,21 @@ +use super::*; + +const V0_2_1: emver::Version = emver::Version::new(0, 2, 1, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_2_0::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_2_1 + } + async fn up(&self) -> Result<(), Error> { + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} diff --git a/appmgr/src/version/v0_2_2.rs b/appmgr/src/version/v0_2_2.rs new file mode 100644 index 000000000..322fc814e --- /dev/null +++ b/appmgr/src/version/v0_2_2.rs @@ -0,0 +1,21 @@ +use super::*; + +const V0_2_2: emver::Version = emver::Version::new(0, 2, 2, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_2_1::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_2_2 + } + async fn up(&self) -> Result<(), Error> { + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} diff --git a/appmgr/src/version/v0_2_3.rs b/appmgr/src/version/v0_2_3.rs new file mode 100644 index 000000000..6be1c670d --- /dev/null +++ b/appmgr/src/version/v0_2_3.rs @@ -0,0 +1,21 @@ +use super::*; + +const V0_2_3: emver::Version = emver::Version::new(0, 2, 3, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_2_2::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_2_3 + } + async fn up(&self) -> Result<(), Error> { + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} diff --git a/appmgr/src/version/v0_2_4.rs b/appmgr/src/version/v0_2_4.rs new file mode 100644 index 000000000..eb8274f52 --- /dev/null +++ b/appmgr/src/version/v0_2_4.rs @@ -0,0 +1,21 @@ +use super::*; + +const V0_2_4: emver::Version = emver::Version::new(0, 2, 4, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_2_3::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_2_4 + } + async fn up(&self) -> Result<(), Error> { + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} diff --git a/appmgr/src/version/v0_2_5.rs b/appmgr/src/version/v0_2_5.rs new file mode 100644 index 000000000..5b127a198 --- /dev/null +++ b/appmgr/src/version/v0_2_5.rs @@ -0,0 +1,21 @@ +use super::*; + +const V0_2_5: emver::Version = emver::Version::new(0, 2, 5, 0); + +pub struct Version; +#[async_trait] +impl VersionT for Version { + type Previous = v0_2_4::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> &'static emver::Version { + &V0_2_5 + } + async fn up(&self) -> Result<(), Error> { + Ok(()) + } + async fn down(&self) -> Result<(), Error> { + Ok(()) + } +} diff --git a/make_image.sh b/make_image.sh new file mode 100644 index 000000000..72f0672c1 --- /dev/null +++ b/make_image.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +>&2 echo "As of 0.2.5, it is not possible to programmatically generate an Embassy image." +>&2 echo "The image must be setup manually by copying over the artifacts, and installing the necessary dependencies." +exit 1 \ No newline at end of file diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..545596336 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,33 @@ +# Specifies intentionally untracked files to ignore when using Git +# http://git-scm.com/docs/gitignore + +*~ +*.sw[mnpcod] +.tmp +*.tmp +*.tmp.* +.DS_Store +Thumbs.db +UserInterfaceState.xcuserstate +$RECYCLE.BIN/ + +start9-ambassador +*.tar.gz + +ambassador.tar.gz +*.log +log.txt +npm-debug.log* + +postprocess.js + +/.idea +/.ionic +/.sass-cache +/.sourcemaps +/.vscode +/.gradle +/dist +/out-tsc +/node_modules +/www diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 000000000..a05cb70c4 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,19 @@ +# Embassy UI + +## Setup Instructions + +**Make sure you have git, node, and npm installed** + +`npm i -g @ionic/cli` + +`git clone https://github.com/Start9Labs/embassy-ui.git` + +`cd embassy-ui` + +`npm i` + +`ionic serve` + +## Production Deployment + +`ionic build --prod` diff --git a/ui/angular.json b/ui/angular.json new file mode 100644 index 000000000..1d980b5dc --- /dev/null +++ b/ui/angular.json @@ -0,0 +1,144 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "defaultProject": "app", + "newProjectRoot": "projects", + "projects": { + "app": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "prefix": "app", + "schematics": {}, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "www", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.json", + "assets": [ + { + "glob": "**/*", + "input": "src/assets", + "output": "assets" + }, + { + "glob": "**/*.svg", + "input": "node_modules/ionicons/dist/ionicons/svg", + "output": "./svg" + } + ], + "styles": [ + { + "input": "src/theme/variables.scss" + }, + { + "input": "src/global.scss" + } + ], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + } + ] + }, + "ci": { + "progress": false + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "app:build" + }, + "configurations": { + "production": { + "browserTarget": "app:build:production" + }, + "ci": { + "progress": false + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "app:build" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "tsconfig.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + }, + "ionic-cordova-build": { + "builder": "@ionic/angular-toolkit:cordova-build", + "options": { + "browserTarget": "app:build" + }, + "configurations": { + "production": { + "browserTarget": "app:build:production" + } + } + }, + "ionic-cordova-serve": { + "builder": "@ionic/angular-toolkit:cordova-serve", + "options": { + "cordovaBuildTarget": "app:ionic-cordova-build", + "devServerTarget": "app:serve" + }, + "configurations": { + "production": { + "cordovaBuildTarget": "app:ionic-cordova-build:production", + "devServerTarget": "app:serve:production" + } + } + } + } + } + }, + "cli": { + "defaultCollection": "@ionic/angular-toolkit", + "analytics": false + }, + "schematics": { + "@ionic/angular-toolkit:component": { + "styleext": "scss" + }, + "@ionic/angular-toolkit:page": { + "styleext": "scss" + } + } +} \ No newline at end of file diff --git a/ui/browserslist b/ui/browserslist new file mode 100644 index 000000000..b15c7fae5 --- /dev/null +++ b/ui/browserslist @@ -0,0 +1,12 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +> 0.5% +last 2 versions +Firefox ESR +not dead +not IE 9-11 # For IE 9-11 support, remove 'not'. diff --git a/ui/build-send-beta.sh b/ui/build-send-beta.sh new file mode 100755 index 000000000..fbbc147cd --- /dev/null +++ b/ui/build-send-beta.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -e + +echo "turn off mocks" +echo "$( jq '.useMocks = false' use-mocks.json )" > use-mocks.json + +echo "FILTER: rm -rf www" +rm -rf www + +echo "FILTER: ionic build" +npm run build-prod + +echo "FILTER: cp client-manifest.yaml www" +cp client-manifest.yaml www + +echo "FILTER: git hash" +touch git-hash.txt +git log | head -n1 > git-hash.txt +mv git-hash.txt www + +echo "FILTER: removing mock icons" +rm -rf www/assets/img/service-icons + +echo "FILTER: tar -zcvf ambassador-ui.tar.gz www" +tar -zcvf ambassador-ui.tar.gz www + +SHA_SUM=$(sha1sum ambassador-ui.tar.gz) +echo "${SHA_SUM}" + +echo "Set version" +VERSION=$(jq ".version" package.json) +echo "${VERSION}" + +echo "FILTER: mkdir beta-reg" +ssh root@beta-registry.start9labs.com "mkdir -p /var/www/html/resources/sys/ambassador-ui.tar.gz/${VERSION}" + +echo "FILTER: scp ambassador-ui.tar.gz" +scp ambassador-ui.tar.gz root@beta-registry.start9labs.com:/var/www/html/resources/sys/ambassador-ui.tar.gz/${VERSION}/ambassador-ui.tar.gz + +echo "FILTER: fin" diff --git a/ui/build-send.sh b/ui/build-send.sh new file mode 100755 index 000000000..5f31a0b73 --- /dev/null +++ b/ui/build-send.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +#echo "turn off mocks" +#echo "$( jq '.useMocks = false' use-mocks.json )" > use-mocks.json + +echo "FILTER: rm -rf www" +rm -rf www + +echo "FILTER: ionic build" +npm run build-prod + +echo "FILTER: cp client-manifest.yaml www" +cp client-manifest.yaml www + +echo "FILTER: git hash" +touch git-hash.txt +git log | head -n1 > git-hash.txt +mv git-hash.txt www + +echo "FILTER: ssh + rm -rf /var/www/html/start9-ambassador/" +ssh root@start9-$1.local "rm -rf /var/www/html/start9-ambassador" + +echo "FILTER: tar -zcvf ambassador.tar.gz www" +rm -rf start9-ambassador +mv www start9-ambassador +tar -zcvf ambassador.tar.gz start9-ambassador + +echo "FILTER: scp ambassador.tar.gz root@start9-def09913.local:/root" +scp ambassador.tar.gz root@start9-$1.local:/root/agent + +echo "FILTER: ssh root@start9-$1.local:/root 1" +ssh root@start9-$1.local "cd /root/agent && tar -C /var/www/html/ -xvf ambassador.tar.gz" + +echo "FILTER: ssh root@start9-$1.local:/root 2" +ssh root@start9-$1.local "systemctl restart nginx" + +echo "FILTER: fin" diff --git a/ui/client-manifest.yaml b/ui/client-manifest.yaml new file mode 100644 index 000000000..c9b087b52 --- /dev/null +++ b/ui/client-manifest.yaml @@ -0,0 +1,117 @@ +manifest-version: 0 +app-id: start9-ambassador +app-version: 0.2.5 +uri-rewrites: + - =/api -> http://{{start9-ambassador}}:5959/authenticate + - /api/ -> http://{{start9-ambassador}}:5959/ +main-is: index.html +error-pages: + 404: index.html +mime-types: + wasm: application/wasm + bin: application/octet-stream + json: application/json + html: text/html + htm: text/html + shtml: text/html + css: text/css + xml: text/xml + gif: image/gif + jpeg: image/jpeg + jpg: image/jpeg + js: application/javascript + atom: application/atom+xml + rss: application/rss+xml + mml: text/mathml + txt: text/plain + jad: text/vnd.sun.j2me.app-descriptor + wml: text/vnd.wap.wml + htc: text/x-component + png: image/png + tif: image/tiff + tiff: image/tiff + wbmp: image/vnd.wap.wbmp + ico: image/x-icon + jng: image/x-jng + bmp: image/x-ms-bmp + svg: image/svg+xml + svgz: image/svg+xml + webp: image/webp + woff: application/font-woff + jar: application/java-archive + war: application/java-archive + ear: application/java-archive + json: application/json + hqx: application/mac-binhex40 + doc: application/msword + pdf: application/pdf + ps: application/postscript + eps: application/postscript + ai: application/postscript + rtf: application/rtf + m3u8: application/vnd.apple.mpegur + xls: application/vnd.ms-exce + eot: application/vnd.ms-fontobjec + ppt: application/vnd.ms-powerpoin + wmlc: application/vnd.wap.wml + kml: application/vnd.google-earth.kml+xm + kmz: application/vnd.google-earth.km + 7z: application/x-7z-compresse + cco: application/x-cocoa + jardiff: application/x-java-archive-diff + jnlp: application/x-java-jnlp-file + run: application/x-makesel + pl: application/x-perl + pm: application/x-perl + prc: application/x-pilot + pdb: application/x-pilot + rar: application/x-rar-compressed + rpm: application/x-redhat-package-manager + sea: application/x-sea + swf: application/x-shockwave-flash + sit: application/x-stuffit + tk: application/x-tcl + tcl: application/x-tcl + der: application/x-x509-ca-cert + pem: application/x-x509-ca-cert + crt: application/x-x509-ca-cert + xpi: application/x-xpinstall + xhtml: application/xhtml+xml + xspf: application/xspf+xml + zip: application/zip + bin: application/octet-stream + exe: application/octet-stream + dll: application/octet-stream + deb: application/octet-stream + dmg: application/octet-stream + iso: application/octet-stream + img: application/octet-stream + msi: application/octet-stream + msp: application/octet-stream + msm: application/octet-stream + docx: application/vnd.openxmlformats-officedocument.wordprocessingml.document + xlsx: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + pptx: application/vnd.openxmlformats-officedocument.presentationml.presentation + mid: audio/mid + kar: audio/mid + midi: audio/mid + mp3: audio/mpe + ogg: audio/og + m4a: audio/x-m4 + ra: audio/x-realaudio + 3gpp: video/3gp + 3gp: video/3gp + ts: video/mp2 + mp4: video/mp + mpeg: video/mpe + mpg: video/mpe + mov: video/quicktime + webm: video/web + flv: video/x-fl + m4v: video/x-m4 + mng: video/x-mn + asx: video/x-ms-asf + asf: video/x-ms-asf + wmv: video/x-ms-wmv + avi: video/x-msvideo +mime-default: text/plain diff --git a/ui/ionic.config.json b/ui/ionic.config.json new file mode 100644 index 000000000..58dfd88ad --- /dev/null +++ b/ui/ionic.config.json @@ -0,0 +1,5 @@ +{ + "name": "Embassy", + "integrations": {}, + "type": "angular" +} \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 000000000..ee4c3bfb0 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,13885 @@ +{ + "name": "embassy-ui", + "version": "0.2.5", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.1002.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.0.tgz", + "integrity": "sha512-twM8V03ujBIGVpgV1PBlSDodUdxtUb7WakutfWafAvEHUsgwzfvQz2VtKWvjNZ9AiYjnCuwkQaclqVv0VHNo9w==", + "dev": true, + "requires": { + "@angular-devkit/core": "10.2.0", + "rxjs": "6.6.2" + }, + "dependencies": { + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@angular-devkit/build-angular": { + "version": "0.1002.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.1002.0.tgz", + "integrity": "sha512-cPkdp1GceokGHc79Wg0hACMqqmnJ4W3H9kY4c9qp1Xz18b3vk1aq09JNawOpfUN09S9vBCnn4glg22lRyqmJNA==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1002.0", + "@angular-devkit/build-optimizer": "0.1002.0", + "@angular-devkit/build-webpack": "0.1002.0", + "@angular-devkit/core": "10.2.0", + "@babel/core": "7.11.1", + "@babel/generator": "7.11.0", + "@babel/plugin-transform-runtime": "7.11.0", + "@babel/preset-env": "7.11.0", + "@babel/runtime": "7.11.2", + "@babel/template": "7.10.4", + "@jsdevtools/coverage-istanbul-loader": "3.0.5", + "@ngtools/webpack": "10.2.0", + "autoprefixer": "9.8.6", + "babel-loader": "8.1.0", + "browserslist": "^4.9.1", + "cacache": "15.0.5", + "caniuse-lite": "^1.0.30001032", + "circular-dependency-plugin": "5.2.0", + "copy-webpack-plugin": "6.0.3", + "core-js": "3.6.4", + "css-loader": "4.2.2", + "cssnano": "4.1.10", + "file-loader": "6.0.0", + "find-cache-dir": "3.3.1", + "glob": "7.1.6", + "jest-worker": "26.3.0", + "karma-source-map-support": "1.4.0", + "less-loader": "6.2.0", + "license-webpack-plugin": "2.3.0", + "loader-utils": "2.0.0", + "mini-css-extract-plugin": "0.10.0", + "minimatch": "3.0.4", + "open": "7.2.0", + "parse5": "6.0.1", + "parse5-htmlparser2-tree-adapter": "6.0.1", + "pnp-webpack-plugin": "1.6.4", + "postcss": "7.0.32", + "postcss-import": "12.0.1", + "postcss-loader": "3.0.0", + "raw-loader": "4.0.1", + "regenerator-runtime": "0.13.7", + "resolve-url-loader": "3.1.2", + "rimraf": "3.0.2", + "rollup": "2.26.5", + "rxjs": "6.6.2", + "sass": "1.26.10", + "sass-loader": "10.0.1", + "semver": "7.3.2", + "source-map": "0.7.3", + "source-map-loader": "1.0.2", + "source-map-support": "0.5.19", + "speed-measure-webpack-plugin": "1.3.3", + "style-loader": "1.2.1", + "stylus": "0.54.8", + "stylus-loader": "3.0.2", + "terser": "5.3.0", + "terser-webpack-plugin": "4.1.0", + "tree-kill": "1.2.2", + "webpack": "4.44.1", + "webpack-dev-middleware": "3.7.2", + "webpack-dev-server": "3.11.0", + "webpack-merge": "4.2.2", + "webpack-sources": "1.4.3", + "webpack-subresource-integrity": "1.4.1", + "worker-plugin": "5.0.0" + }, + "dependencies": { + "core-js": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", + "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", + "dev": true + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@angular-devkit/build-optimizer": { + "version": "0.1002.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.1002.0.tgz", + "integrity": "sha512-ACnm9doPMbRtSy1UZN5ir7smeLMx0g0oW7jX3jyPepeQKZ+9U1Bn09t10NLZQH+Z509jWZgvNJH/aOh85P6euw==", + "dev": true, + "requires": { + "loader-utils": "2.0.0", + "source-map": "0.7.3", + "tslib": "2.0.1", + "typescript": "4.0.2", + "webpack-sources": "1.4.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "dev": true + }, + "typescript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz", + "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==", + "dev": true + } + } + }, + "@angular-devkit/build-webpack": { + "version": "0.1002.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1002.0.tgz", + "integrity": "sha512-TLBBQ6ANOLKXOPxpCOnxAtoknwHA7XhsLuueN06w5qqF+QNNbWUMPoieKFGs2TnotfCgbiq6x57IDEZTyT6V0w==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1002.0", + "@angular-devkit/core": "10.2.0", + "rxjs": "6.6.2" + }, + "dependencies": { + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@angular-devkit/core": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.2.0.tgz", + "integrity": "sha512-XAszFhSF3mZw1VjoOsYGbArr5NJLcStjOvcCGjBPl1UBM2AKpuCQXHxI9XJGYKL3B93Vp5G58d8qkHvamT53OA==", + "dev": true, + "requires": { + "ajv": "6.12.4", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.2", + "source-map": "0.7.3" + }, + "dependencies": { + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@angular-devkit/schematics": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-10.1.7.tgz", + "integrity": "sha512-nk9RXA09b+7uq59HS/gyztNzUGHH/eQAUQhWHdDYSCG6v1lhJVCKx1HgDPELVxmeq9f+HArkAW7Y7c+ccdNQ7A==", + "dev": true, + "requires": { + "@angular-devkit/core": "10.1.7", + "ora": "5.0.0", + "rxjs": "6.6.2" + }, + "dependencies": { + "@angular-devkit/core": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.1.7.tgz", + "integrity": "sha512-RRyDkN2FByA+nlnRx/MzUMK1FXwj7+SsrzJcvZfWx4yA5rfKmJiJryXQEzL44GL1aoaXSuvOYu3H72wxZADN8Q==", + "dev": true, + "requires": { + "ajv": "6.12.4", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.2", + "source-map": "0.7.3" + } + }, + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@angular/cli": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-10.1.7.tgz", + "integrity": "sha512-0tbeHnPIzSV/z+KlZT7N2J1yMnwQi4xIxvbsANrLjoAxNssse84i9BDdMZYsPoV8wbzcDhFOtt5KmfTO0GIeYQ==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1001.7", + "@angular-devkit/core": "10.1.7", + "@angular-devkit/schematics": "10.1.7", + "@schematics/angular": "10.1.7", + "@schematics/update": "0.1001.7", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.1", + "debug": "4.1.1", + "ini": "1.3.5", + "inquirer": "7.3.3", + "npm-package-arg": "8.0.1", + "npm-pick-manifest": "6.1.0", + "open": "7.2.0", + "pacote": "9.5.12", + "read-package-tree": "5.3.1", + "rimraf": "3.0.2", + "semver": "7.3.2", + "symbol-observable": "1.2.0", + "universal-analytics": "0.4.23", + "uuid": "8.3.0" + }, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.1001.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1001.7.tgz", + "integrity": "sha512-uFYIvMdewU44GbIyRfsUHNMLkx+C0kokpnj7eH5NbJfbyFpCfd3ijBHh+voPdPsDRWs9lLgjbxfHpswSPj4D8w==", + "dev": true, + "requires": { + "@angular-devkit/core": "10.1.7", + "rxjs": "6.6.2" + } + }, + "@angular-devkit/core": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.1.7.tgz", + "integrity": "sha512-RRyDkN2FByA+nlnRx/MzUMK1FXwj7+SsrzJcvZfWx4yA5rfKmJiJryXQEzL44GL1aoaXSuvOYu3H72wxZADN8Q==", + "dev": true, + "requires": { + "ajv": "6.12.4", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.2", + "source-map": "0.7.3" + } + }, + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "open": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.2.0.tgz", + "integrity": "sha512-4HeyhxCvBTI5uBePsAdi55C5fmqnWZ2e2MlmvWi5KW5tdH5rxoiv/aMtbeVxKZc3eWkT1GymMnLG8XC4Rq4TDQ==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==", + "dev": true + } + } + }, + "@angular/common": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-10.1.6.tgz", + "integrity": "sha512-4ywlUHHF5ofZRTHJ/jQTHoO8Tu05Wvn+3N7swaJ9yAfiywbSE4Bop6FYsocxaxROrGS0k6Unvgj8+J7x6AeqlA==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@angular/compiler": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-10.1.6.tgz", + "integrity": "sha512-LynYIrzSV+7pVcY5a3N3mCtyZ2eMKzIk1iKLI76w4PHfJBTpBuv8L8aSy/kmnaPwCT/YM/657DMMy2A4HwU5nw==", + "dev": true, + "requires": { + "tslib": "^2.0.0" + } + }, + "@angular/compiler-cli": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-10.1.6.tgz", + "integrity": "sha512-FPb/9E4HEhFWlCPf85xtmgXDmnD+iTtfjPATEMERRY0/si1Or9JeFya2VLdWldOmBQYqnvxc9o/rpdNkpT8TYA==", + "dev": true, + "requires": { + "canonical-path": "1.0.0", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.7.2", + "fs-extra": "4.0.2", + "magic-string": "^0.25.0", + "minimist": "^1.2.0", + "reflect-metadata": "^0.1.2", + "semver": "^6.3.0", + "source-map": "^0.6.1", + "sourcemap-codec": "^1.4.8", + "tslib": "^2.0.0", + "yargs": "15.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.0.tgz", + "integrity": "sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.0" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "@angular/core": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-10.1.6.tgz", + "integrity": "sha512-sUleQouCedT87VOCb49T7cm6La2VeJg1omtO5+QfjWmifNcQ/nqV56Zxov3RT7CmsVwVbkA0X5Q62oSEPAUXrw==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@angular/forms": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-10.1.6.tgz", + "integrity": "sha512-sTPnwL0r7lniv2/XU4nK3eU9osGpGD4YdJ0qLsXfR/ku4mhgbKk/taVBTmAdQwWBUOOafzU1yG9asvsm8H1Kbw==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@angular/language-service": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-10.1.6.tgz", + "integrity": "sha512-lxZHL4RGjir6acj0eF7xihIXWtRg/Z4Y+PMX7fKEI66hc1sLxH+AKkZKG6yr+rrJK2DcakC8Izz/BO+BS2ELjw==", + "dev": true + }, + "@angular/platform-browser": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-10.1.6.tgz", + "integrity": "sha512-kN2ik35eBqFWNmKPRkZbp5qHkhNINf3PudFUy9ii8kP01OL+Nyrn0MBisIHl3sf+KOV8sf9dMQGPOyQDz22wig==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-10.1.6.tgz", + "integrity": "sha512-MOdaLnbAXVruYpV0Q5CXLb/fP6xHxWzjRhAh7sLaIIu/TnhTSZpxgxZxBx05hvzP4rH/7S2XvAiuQQomevCIXQ==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@angular/router": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-10.1.6.tgz", + "integrity": "sha512-MV8kSDhboFRH23MnrQvNGHncMb4nkdJDwS108p7oNZjjDkUUR3A5TMWmmN/3BRnue6JoPRWBCPyb53cA21schQ==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/compat-data": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.1.tgz", + "integrity": "sha512-725AQupWJZ8ba0jbKceeFblZTY90McUBWMwHhkFQ9q1zKPJ95GUktljFcgcsIVwRnTnRKlcYzfiNImg5G9m6ZQ==", + "dev": true + }, + "@babel/core": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.1.tgz", + "integrity": "sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-module-transforms": "^7.11.0", + "@babel/helpers": "^7.10.4", + "@babel/parser": "^7.11.1", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.11.0", + "@babel/types": "^7.11.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.0.tgz", + "integrity": "sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", + "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.1.tgz", + "integrity": "sha512-jtBEif7jsPwP27GPHs06v4WBV0KrE8a/P7n0N0sSvHn2hwUCYnolP/CLmz51IzAW4NlN+HuoBtb9QcwnRo9F/g==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.12.1", + "@babel/helper-validator-option": "^7.12.1", + "browserslist": "^4.12.0", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", + "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.1.tgz", + "integrity": "sha512-rsZ4LGvFTZnzdNZR5HZdmJVuXK8834R5QkF3WvcnBhrlVtF0HSIUC6zbreL9MgjTywhKokn8RIYRiq99+DLAxA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-regex": "^7.10.4", + "regexpu-core": "^4.7.1" + } + }, + "@babel/helper-define-map": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz", + "integrity": "sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", + "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz", + "integrity": "sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-module-imports": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.1.tgz", + "integrity": "sha512-ZeC1TlMSvikvJNy1v/wPIazCu3NdOwgYZLIkmIyAsGhqkNpiDoQQRmaCK8YP4Pq3GPTLPV9WXaPCJKvx06JxKA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-module-transforms": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "@babel/helper-regex": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", + "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", + "dev": true, + "requires": { + "lodash": "^4.17.19" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", + "integrity": "sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-wrap-function": "^7.10.4", + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-replace-supers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.1.tgz", + "integrity": "sha512-zJjTvtNJnCFsCXVi5rUInstLd/EIVNmIKA1Q9ynESmMBWPWd+7sdR+G4/wdu+Mppfep0XLyG2m7EBPvjCeFyrw==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-simple-access": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", + "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz", + "integrity": "sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz", + "integrity": "sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helpers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.1.tgz", + "integrity": "sha512-9JoDSBGoWtmbay98efmT2+mySkwjzeFeAL9BuWNoVQpkPFQF8SIIFUfY5os9u8wVzglzoiPRSW7cuJmBDUt43g==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.3.tgz", + "integrity": "sha512-kFsOS0IbsuhO5ojF8Hc8z/8vEIOkylVBrjiZUbLTE3XFe0Qi+uu6HjzQixkFaqr0ZPAMZcBVxEwmsnsLPZ2Xsw==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz", + "integrity": "sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1", + "@babel/plugin-syntax-async-generators": "^7.8.0" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", + "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz", + "integrity": "sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-dynamic-import": "^7.8.0" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz", + "integrity": "sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz", + "integrity": "sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.0" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz", + "integrity": "sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz", + "integrity": "sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.1.tgz", + "integrity": "sha512-MR7Ok+Af3OhNTCxYVjJZHS0t97ydnJZt/DbR4WISO39iDnhiD8XHrY12xuSJ90FFEGjir0Fzyyn7g/zY6hxbxA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", + "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-transform-parameters": "^7.12.1" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz", + "integrity": "sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.1.tgz", + "integrity": "sha512-c2uRpY6WzaVDzynVY9liyykS+kVU+WRZPMPYpkelXH8KBt1oXoI89kPbZKKG/jDT5UK92FTW2fZkZaJhdiBabw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz", + "integrity": "sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz", + "integrity": "sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", + "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", + "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", + "integrity": "sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz", + "integrity": "sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz", + "integrity": "sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz", + "integrity": "sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz", + "integrity": "sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-define-map": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz", + "integrity": "sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz", + "integrity": "sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz", + "integrity": "sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz", + "integrity": "sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz", + "integrity": "sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz", + "integrity": "sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz", + "integrity": "sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz", + "integrity": "sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz", + "integrity": "sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz", + "integrity": "sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz", + "integrity": "sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-simple-access": "^7.12.1", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz", + "integrity": "sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.10.4", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-identifier": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz", + "integrity": "sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz", + "integrity": "sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz", + "integrity": "sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz", + "integrity": "sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz", + "integrity": "sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz", + "integrity": "sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", + "integrity": "sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz", + "integrity": "sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.0.tgz", + "integrity": "sha512-LFEsP+t3wkYBlis8w6/kmnd6Kb1dxTd+wGJ8MlxTGzQo//ehtqlVL4S9DNUa53+dtPSQobN2CXx4d81FqC58cw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "resolve": "^1.8.1", + "semver": "^5.5.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz", + "integrity": "sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz", + "integrity": "sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.1.tgz", + "integrity": "sha512-CiUgKQ3AGVk7kveIaPEET1jNDhZZEl1RPMWdTBE1799bdz++SwqDHStmxfCtDfBhQgCl38YRiSnrMuUMZIWSUQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-regex": "^7.10.4" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz", + "integrity": "sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz", + "integrity": "sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", + "integrity": "sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz", + "integrity": "sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/preset-env": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.0.tgz", + "integrity": "sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.11.0", + "@babel/helper-compilation-targets": "^7.10.4", + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-proposal-async-generator-functions": "^7.10.4", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-dynamic-import": "^7.10.4", + "@babel/plugin-proposal-export-namespace-from": "^7.10.4", + "@babel/plugin-proposal-json-strings": "^7.10.4", + "@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", + "@babel/plugin-proposal-numeric-separator": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.11.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.10.4", + "@babel/plugin-proposal-optional-chaining": "^7.11.0", + "@babel/plugin-proposal-private-methods": "^7.10.4", + "@babel/plugin-proposal-unicode-property-regex": "^7.10.4", + "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-class-properties": "^7.10.4", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0", + "@babel/plugin-syntax-top-level-await": "^7.10.4", + "@babel/plugin-transform-arrow-functions": "^7.10.4", + "@babel/plugin-transform-async-to-generator": "^7.10.4", + "@babel/plugin-transform-block-scoped-functions": "^7.10.4", + "@babel/plugin-transform-block-scoping": "^7.10.4", + "@babel/plugin-transform-classes": "^7.10.4", + "@babel/plugin-transform-computed-properties": "^7.10.4", + "@babel/plugin-transform-destructuring": "^7.10.4", + "@babel/plugin-transform-dotall-regex": "^7.10.4", + "@babel/plugin-transform-duplicate-keys": "^7.10.4", + "@babel/plugin-transform-exponentiation-operator": "^7.10.4", + "@babel/plugin-transform-for-of": "^7.10.4", + "@babel/plugin-transform-function-name": "^7.10.4", + "@babel/plugin-transform-literals": "^7.10.4", + "@babel/plugin-transform-member-expression-literals": "^7.10.4", + "@babel/plugin-transform-modules-amd": "^7.10.4", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/plugin-transform-modules-systemjs": "^7.10.4", + "@babel/plugin-transform-modules-umd": "^7.10.4", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.4", + "@babel/plugin-transform-new-target": "^7.10.4", + "@babel/plugin-transform-object-super": "^7.10.4", + "@babel/plugin-transform-parameters": "^7.10.4", + "@babel/plugin-transform-property-literals": "^7.10.4", + "@babel/plugin-transform-regenerator": "^7.10.4", + "@babel/plugin-transform-reserved-words": "^7.10.4", + "@babel/plugin-transform-shorthand-properties": "^7.10.4", + "@babel/plugin-transform-spread": "^7.11.0", + "@babel/plugin-transform-sticky-regex": "^7.10.4", + "@babel/plugin-transform-template-literals": "^7.10.4", + "@babel/plugin-transform-typeof-symbol": "^7.10.4", + "@babel/plugin-transform-unicode-escapes": "^7.10.4", + "@babel/plugin-transform-unicode-regex": "^7.10.4", + "@babel/preset-modules": "^0.1.3", + "@babel/types": "^7.11.0", + "browserslist": "^4.12.0", + "core-js-compat": "^3.6.2", + "invariant": "^2.2.2", + "levenary": "^1.1.1", + "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "@babel/preset-modules": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.1.tgz", + "integrity": "sha512-MA3WPoRt1ZHo2ZmoGKNqi20YnPt0B1S0GTZEPhhd+hw2KGUzBlHuVunj6K4sNuK+reEvyiPwtp0cpaqLzJDmAw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.1", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.1", + "@babel/types": "^7.12.1", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + }, + "dependencies": { + "@babel/generator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.1.tgz", + "integrity": "sha512-DB+6rafIdc9o72Yc3/Ph5h+6hUjeOp66pF0naQBgUFFuPqzQwIlPTm3xZR7YNvduIMtkDIj2t21LSQwnbCrXvg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.1.tgz", + "integrity": "sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@ionic/angular": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-5.4.0.tgz", + "integrity": "sha512-FpAdtPfN8TgXwkkk+zG1h1QZBkyVhOjlbyMXLO2G8Z67q7eKao0AAE22BjzhKO9STGDlzPViEpzG4QZMPYih8g==", + "requires": { + "@ionic/core": "5.4.0", + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@ionic/angular-toolkit": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@ionic/angular-toolkit/-/angular-toolkit-2.3.3.tgz", + "integrity": "sha512-r87mApDLWbLaUtd5LvNHrRlZWxjQhaBBM1yPlk9M98dHOxcX3jy7kv60ZurGZutuvbhXISGvHcvvR90yywDC1A==", + "dev": true, + "requires": { + "@schematics/angular": ">=8.0.0", + "cheerio": "1.0.0-rc.3", + "colorette": "1.1.0", + "copy-webpack-plugin": "^6.0.3", + "tslib": "^1.9.0", + "ws": "^7.0.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "ws": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "dev": true + } + } + }, + "@ionic/cli-framework": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework/-/cli-framework-5.0.6.tgz", + "integrity": "sha512-CygkCCn+O3vMmt+l5y+evmcBHBI/HVr+QWQVca84ooM2lrLzIQDRC+iZ5RKOnF+eCcywGZ6a68FvXoWAvQzfmw==", + "dev": true, + "requires": { + "@ionic/cli-framework-output": "2.2.2", + "@ionic/utils-array": "2.1.5", + "@ionic/utils-fs": "3.1.5", + "@ionic/utils-object": "2.1.5", + "@ionic/utils-process": "2.1.8", + "@ionic/utils-stream": "3.1.5", + "@ionic/utils-subprocess": "2.1.8", + "@ionic/utils-terminal": "2.3.1", + "chalk": "^4.0.0", + "debug": "^4.0.0", + "lodash": "^4.17.5", + "minimist": "^1.2.0", + "rimraf": "^3.0.0", + "tslib": "^2.0.1", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@ionic/cli-framework-output": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.2.tgz", + "integrity": "sha512-eQYkqIW1/tCwSC6Bd0gjse96U11lDX/ikf3jvsjX7a8z/zwSmGzCHRizb7xogV65Ey+1/zyAZR71cpDRQuFLBQ==", + "dev": true, + "requires": { + "@ionic/utils-terminal": "2.3.1", + "debug": "^4.0.0", + "tslib": "^2.0.1" + } + }, + "@ionic/core": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.4.0.tgz", + "integrity": "sha512-VmAqWWNozVDms2tA0I0fiqgu1tRdh58uhxwM8+xOVjIy8yoJmFxc5/glg4XIrbsYRfb347UICFx75Eh464zJJw==", + "requires": { + "ionicons": "^5.1.2", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@ionic/lab": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@ionic/lab/-/lab-3.2.9.tgz", + "integrity": "sha512-+0fzd3SZ+4dZOaVHNAbhN/2R5pDqJnQLitVgkAFp4gMpyjkIW3enBMpXBLnOaqGhUABUF1Qve7qT+ySQBP8yTA==", + "dev": true, + "requires": { + "@ionic/cli-framework": "5.0.6", + "@ionic/utils-fs": "3.1.5", + "chalk": "^4.0.0", + "express": "^4.16.2", + "tslib": "^2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@ionic/storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ionic/storage/-/storage-2.2.0.tgz", + "integrity": "sha512-2pszrzmI+fAar2Rx0WmJDVpc15D1k5tvLkB49NLYWJ2pOMaO/3/vp7mg/mEbg3rdsPE9FRbYI6vdKjQ2pP1EWA==", + "requires": { + "localforage": "1.7.1", + "localforage-cordovasqlitedriver": "1.7.0", + "tslib": "^1.7.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@ionic/utils-array": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.5.tgz", + "integrity": "sha512-HD72a71IQVBmQckDwmA8RxNVMTbxnaLbgFOl+dO5tbvW9CkkSFCv41h6fUuNsSEVgngfkn0i98HDuZC8mk+lTA==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + } + }, + "@ionic/utils-fs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.5.tgz", + "integrity": "sha512-a41bY2dHqWSEQQ/80CpbXSs8McyiCFf2DnIWWLukrhYWf46h4qi6M/8dxcMKrofRiqI/3F+cL3S2mOm9Zz/o2Q==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "dependencies": { + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "dev": true + } + } + }, + "@ionic/utils-object": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.5.tgz", + "integrity": "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + } + }, + "@ionic/utils-process": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.8.tgz", + "integrity": "sha512-VBBoyTzi+m6tgKAItl+jiTQneGwTOsctcrTG4CsEgmVOVOEhUYkPhddXqzD+oC54hPDU9ROsd3I014P5CWEuhQ==", + "dev": true, + "requires": { + "@ionic/utils-object": "2.1.5", + "@ionic/utils-terminal": "2.3.1", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + } + }, + "@ionic/utils-stream": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.5.tgz", + "integrity": "sha512-hkm46uHvEC05X/8PHgdJi4l4zv9VQDELZTM+Kz69odtO9zZYfnt8DkfXHJqJ+PxmtiE5mk/ehJWLnn/XAczTUw==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + } + }, + "@ionic/utils-subprocess": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.8.tgz", + "integrity": "sha512-pkmtf1LtXcEMPn6/cctREL2aZtZoy0+0Sl+nT0NIkOHIoBUcqrcfMWdctCSM4Mp6+2/hLWtgpHE3TOIibkWfIg==", + "dev": true, + "requires": { + "@ionic/utils-array": "2.1.5", + "@ionic/utils-fs": "3.1.5", + "@ionic/utils-process": "2.1.8", + "@ionic/utils-stream": "3.1.5", + "@ionic/utils-terminal": "2.3.1", + "cross-spawn": "^7.0.0", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@ionic/utils-terminal": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.1.tgz", + "integrity": "sha512-cglsSd2AckI3Ldtdfczeq64vIIDjtPspV5QJtky8f8uIdxkeOIGeRV7bCj1+BEf1hyo+ZuggQxLviHnbMZhiRw==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jsdevtools/coverage-istanbul-loader": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz", + "integrity": "sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA==", + "dev": true, + "requires": { + "convert-source-map": "^1.7.0", + "istanbul-lib-instrument": "^4.0.3", + "loader-utils": "^2.0.0", + "merge-source-map": "^1.1.0", + "schema-utils": "^2.7.0" + } + }, + "@ngtools/webpack": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.2.0.tgz", + "integrity": "sha512-W4SSFNQhIiC8JRhIn3c4mb1+fsFKiHp+THVMAUNo+wRZEt/rgzsCdnqv0EmQJJojZhnilUIyB/wVYJu2+S/Bxg==", + "dev": true, + "requires": { + "@angular-devkit/core": "10.2.0", + "enhanced-resolve": "4.3.0", + "webpack-sources": "1.4.3" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" + } + }, + "@npmcli/move-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", + "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + } + } + }, + "@schematics/angular": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-10.1.7.tgz", + "integrity": "sha512-jcyLWDSbpgHvB/BNVSsV4uLJpC2qRx9Z5+rcQpBB1BerqIPS/1cTQg7TViHZtcqnZqWvzHR3jfqzDUSOCZpuJQ==", + "dev": true, + "requires": { + "@angular-devkit/core": "10.1.7", + "@angular-devkit/schematics": "10.1.7", + "jsonc-parser": "2.3.0" + }, + "dependencies": { + "@angular-devkit/core": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.1.7.tgz", + "integrity": "sha512-RRyDkN2FByA+nlnRx/MzUMK1FXwj7+SsrzJcvZfWx4yA5rfKmJiJryXQEzL44GL1aoaXSuvOYu3H72wxZADN8Q==", + "dev": true, + "requires": { + "ajv": "6.12.4", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.2", + "source-map": "0.7.3" + } + }, + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@schematics/update": { + "version": "0.1001.7", + "resolved": "https://registry.npmjs.org/@schematics/update/-/update-0.1001.7.tgz", + "integrity": "sha512-q7g/9YaAiqyWxYmUXiSWxB9xwc30xL5iUWY3Rp2LXSH6ihaRsLabmNr743R2YQmMj2Ss+9OhILHmj7nMmqODgw==", + "dev": true, + "requires": { + "@angular-devkit/core": "10.1.7", + "@angular-devkit/schematics": "10.1.7", + "@yarnpkg/lockfile": "1.1.0", + "ini": "1.3.5", + "npm-package-arg": "^8.0.0", + "pacote": "9.5.12", + "semver": "7.3.2", + "semver-intersect": "1.4.0" + }, + "dependencies": { + "@angular-devkit/core": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-10.1.7.tgz", + "integrity": "sha512-RRyDkN2FByA+nlnRx/MzUMK1FXwj7+SsrzJcvZfWx4yA5rfKmJiJryXQEzL44GL1aoaXSuvOYu3H72wxZADN8Q==", + "dev": true, + "requires": { + "ajv": "6.12.4", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.2", + "source-map": "0.7.3" + } + }, + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@start9labs/ambassador-sdk": { + "version": "file:../ambassador-sdk", + "requires": { + "ts-transformer-keys": "^0.4.1", + "uuid": "^8.0.0" + }, + "dependencies": { + "@types/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-PUdqTZVrNYTNcIhLHkiaYzoOIaUi5LFg/XLerAdgvwQrUCx+oSbtoBze1AMyvYbcwzUSNC+Isl58SM4Sm/6COw==" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "ts-transformer-keys": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/ts-transformer-keys/-/ts-transformer-keys-0.4.1.tgz", + "integrity": "sha512-CahLCOHt6MS8Sixz5cU8XovuKOoP6hnQd91pxG3a7iuuLsdrbWLveQvKi7d/FJjRhEtVELp3bMnqvSpm+nCgKw==" + }, + "ttypescript": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/ttypescript/-/ttypescript-1.5.10.tgz", + "integrity": "sha512-Hk7TRej1hM+p+Fo+Pyb/XK9pe9CAt3Sh5n5YRutxFS8hUgkh2u1Vd2K40kMcNP3WYhiVFBMqXwM/2E8O95Ep6g==", + "requires": { + "resolve": "^1.9.0" + } + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==" + }, + "uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + } + } + }, + "@start9labs/emver": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@start9labs/emver/-/emver-0.1.1.tgz", + "integrity": "sha512-UvholOAhRBATB/mSoovCqxZrZ/tEzIXOtGt5fEyKWiJp35cRTiku/XwI+MKY+TVJdqFPLqgPtwUQW6FqVhJUCw==" + }, + "@types/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "requires": { + "@types/node": "*" + } + }, + "@types/elliptic": { + "version": "6.4.12", + "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.12.tgz", + "integrity": "sha512-gP1KsqoouLJGH6IJa28x7PXb3cRqh83X8HCLezd2dF+XcAIMKYv53KV+9Zn6QA561E120uOqZBQ+Jy/cl+fviw==", + "requires": { + "@types/bn.js": "*" + } + }, + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/json-pointer": { + "version": "1.0.30", + "resolved": "https://registry.npmjs.org/@types/json-pointer/-/json-pointer-1.0.30.tgz", + "integrity": "sha1-uXPB95sfYdQkt9krlU4B5saO5m0=", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", + "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", + "dev": true + }, + "@types/marked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-1.1.0.tgz", + "integrity": "sha512-j8XXj6/l9kFvCwMyVqozznqpd/nk80krrW+QiIJN60Uu9gX5Pvn4/qPJ2YngQrR3QREPwmrE1f9/EWKVTFzoEw==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "14.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.10.tgz", + "integrity": "sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA==" + }, + "@types/q": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", + "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", + "dev": true + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==", + "dev": true + }, + "@types/webpack-sources": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.8.tgz", + "integrity": "sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.6.1" + } + }, + "@webassemblyjs/ast": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true + }, + "adjust-sourcemap-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz", + "integrity": "sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + } + }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "agentkeepalive": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz", + "integrity": "sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==", + "dev": true, + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "angularx-qrcode": { + "version": "10.0.11", + "resolved": "https://registry.npmjs.org/angularx-qrcode/-/angularx-qrcode-10.0.11.tgz", + "integrity": "sha512-sbtqdqAboEFNoyxgG4FQYPZDzwX9TlICT2mLpsC/Se3OuT+HntW56q8E/i1BL1fJhx7zt0JJR7bc7LfofUeAlQ==", + "requires": { + "qrcode": "1.4.2", + "tslib": "^2.0.0" + } + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arity-n": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz", + "integrity": "sha1-2edrEXM+CFacCEeuezmyhgswt0U=", + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", + "dev": true, + "requires": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + }, + "dependencies": { + "colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", + "dev": true + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", + "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", + "dev": true + }, + "babel-loader": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", + "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", + "dev": true, + "requires": { + "find-cache-dir": "^2.1.0", + "loader-utils": "^1.4.0", + "mkdirp": "^0.5.3", + "pify": "^4.0.1", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base-x": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz", + "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha1-tYLexpPC8R6JPPBk7mrFthMaIgI=" + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip174": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz", + "integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ==" + }, + "bip32": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.6.tgz", + "integrity": "sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA==", + "requires": { + "@types/node": "10.12.18", + "bs58check": "^2.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "tiny-secp256k1": "^1.1.3", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + }, + "dependencies": { + "@types/node": { + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" + } + } + }, + "bip39": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz", + "integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==", + "requires": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + }, + "dependencies": { + "@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" + } + } + }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bitcoin-ops": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", + "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==" + }, + "bitcoinjs-lib": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-5.2.0.tgz", + "integrity": "sha512-5DcLxGUDejgNBYcieMIUfjORtUeNWl828VWLHJGVKZCb4zIS1oOySTUr0LGmcqJBQgTBz3bGbRQla4FgrdQEIQ==", + "requires": { + "bech32": "^1.1.2", + "bip174": "^2.0.1", + "bip32": "^2.0.4", + "bip66": "^1.1.0", + "bitcoin-ops": "^1.4.0", + "bs58check": "^2.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.3", + "merkle-lib": "^2.0.10", + "pushdata-bitcoin": "^1.0.1", + "randombytes": "^2.0.1", + "tiny-secp256k1": "^1.1.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.0.4", + "wif": "^2.0.1" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "dev": true, + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "bn.js": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", + "dev": true + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.14.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.5.tgz", + "integrity": "sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001135", + "electron-to-chromium": "^1.3.571", + "escalade": "^3.1.0", + "node-releases": "^1.1.61" + } + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "requires": { + "base-x": "^3.0.2" + } + }, + "bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "requires": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "cacache": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.5.tgz", + "integrity": "sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==", + "dev": true, + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001150", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001150.tgz", + "integrity": "sha512-kiNKvihW0m36UhAFnl7bOAv0i1K1f6wpfVtTF5O5O82XzgtBnb05V0XeV3oZ968vfg2sRNChsHw8ASH2hDfoYQ==", + "dev": true + }, + "canonical-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/canonical-path/-/canonical-path-1.0.0.tgz", + "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "cheerio": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", + "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", + "dev": true, + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.1", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash": "^4.15.0", + "parse5": "^3.0.1" + }, + "dependencies": { + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "dev": true, + "requires": { + "@types/node": "*" + } + } + } + }, + "chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-dependency-plugin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.0.tgz", + "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.5.0.tgz", + "integrity": "sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==", + "dev": true + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", + "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.4" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", + "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorette": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.1.0.tgz", + "integrity": "sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compose-function": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz", + "integrity": "sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=", + "dev": true, + "requires": { + "arity-n": "^1.0.4" + } + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-webpack-plugin": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.0.3.tgz", + "integrity": "sha512-q5m6Vz4elsuyVEIUXr7wJdIdePWTubsqVbEMvf1WQnHGv0Q+9yPRu7MtYFPt+GBOXRav9lvIINifTQ1vSCs+eA==", + "dev": true, + "requires": { + "cacache": "^15.0.4", + "fast-glob": "^3.2.4", + "find-cache-dir": "^3.3.1", + "glob-parent": "^5.1.1", + "globby": "^11.0.1", + "loader-utils": "^2.0.0", + "normalize-path": "^3.0.0", + "p-limit": "^3.0.1", + "schema-utils": "^2.7.0", + "serialize-javascript": "^4.0.0", + "webpack-sources": "^1.4.3" + }, + "dependencies": { + "cacache": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.5.tgz", + "integrity": "sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==", + "dev": true, + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" + }, + "core-js-compat": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", + "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==", + "dev": true, + "requires": { + "browserslist": "^4.8.5", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "source-map": "^0.6.1", + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-loader": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-4.2.2.tgz", + "integrity": "sha512-omVGsTkZPVwVRpckeUnLshPp12KsmMSLqYxs12+RzM9jRR5Y+Idn/tBffjXRvOE+qW7if24cuceFJqYR5FmGBg==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^2.0.0", + "postcss": "^7.0.32", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.3", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.0", + "semver": "^7.3.2" + }, + "dependencies": { + "camelcase": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.1.0.tgz", + "integrity": "sha512-WCMml9ivU60+8rEJgELlFp1gxFcEGxwYleE3bziHEDeqsqAWGHdimB7beBFGjLzVNgPGyDsfgXLQEYMpmIFnVQ==", + "dev": true + } + } + }, + "css-parse": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz", + "integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=", + "dev": true, + "requires": { + "css": "^2.0.0" + } + }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dev": true, + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + } + }, + "css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "dev": true, + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true + }, + "csso": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz", + "integrity": "sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==", + "dev": true, + "requires": { + "css-tree": "1.0.0-alpha.39" + }, + "dependencies": { + "css-tree": { + "version": "1.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz", + "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==", + "dev": true, + "requires": { + "mdn-data": "2.0.6", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz", + "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==", + "dev": true + } + } + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + }, + "dependencies": { + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + } + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "dependency-graph": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.7.2.tgz", + "integrity": "sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ==", + "dev": true + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "dezalgo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", + "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "dijkstrajs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz", + "integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs=" + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz", + "integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.583", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.583.tgz", + "integrity": "sha512-L9BwLwJohjZW9mQESI79HRzhicPk1DFgM+8hOCfGgGCFEcA3Otpv7QK6SGtYoZvfQfE3wKLh0Hd5ptqUFv3gvQ==", + "dev": true + }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + } + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true + }, + "err-code": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=", + "dev": true + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dev": true, + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "dev": true + }, + "eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "dev": true, + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "dev": true, + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", + "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fastq": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", + "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-loader": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.0.0.tgz", + "integrity": "sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^2.6.5" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "follow-redirects": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs-extra": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz", + "integrity": "sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "genfun": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz", + "integrity": "sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hosted-git-info": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.7.tgz", + "integrity": "sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "dev": true + }, + "html-entities": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", + "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==", + "dev": true + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + } + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "dev": true, + "requires": { + "agent-base": "4", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "requires": { + "import-from": "^2.1.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "ionicons": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-5.2.3.tgz", + "integrity": "sha512-87qtgBkieKVFagwYA9Cf91B3PCahQbEOMwMt8bSvlQSgflZ4eE5qI4MGj2ZlIyadeX0dgo+0CzZsy3ow0CsBAg==" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-core-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.0.0.tgz", + "integrity": "sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-docker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "dev": true, + "requires": { + "html-comment-regex": "^1.1.0" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "jest-worker": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.3.0.tgz", + "integrity": "sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-pointer": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.1.tgz", + "integrity": "sha512-3OvjqKdCBvH41DLpV4iSt6v2XhZXV1bPB4OROuknvUXI7ZQNofieCPkmE26stEJ9zdQuvIxDHCuYhfgxFAAs+Q==", + "requires": { + "foreach": "^2.0.4" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", + "dev": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "jsonc-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.0.tgz", + "integrity": "sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA==", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsonpointerx": { + "version": "1.0.30", + "resolved": "https://registry.npmjs.org/jsonpointerx/-/jsonpointerx-1.0.30.tgz", + "integrity": "sha512-nEyQ2/CntckMpxK4ZTDOO3NmuRmxYTT5qnwnxoYKHZon1aa4TtEzLXF3FDVIH8Ra15zoKaNVmff1O3jcSlEKqA==" + }, + "jsontokens": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsontokens/-/jsontokens-3.0.0.tgz", + "integrity": "sha512-P0QZC5AjOkn3t1ej6OuI7+XqoEctYj83UK4pw0WpHY4/z6a5PpZCJSpp5NZodq94GFkw2PfB9DPFoDM5qpyp/g==", + "requires": { + "@types/elliptic": "^6.4.9", + "asn1.js": "^5.0.1", + "base64url": "^3.0.1", + "ecdsa-sig-formatter": "^1.0.11", + "elliptic": "^6.4.1", + "sha.js": "^2.4.11" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "requires": { + "source-map-support": "^0.5.5" + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", + "dev": true + }, + "less": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz", + "integrity": "sha512-+1V2PCMFkL+OIj2/HrtrvZw0BC0sYLMICJfbQjuj/K8CEnlrFX6R5cKKgzzttsZDHyxQNL1jqMREjKN3ja/E3Q==", + "dev": true, + "requires": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "native-request": "^1.0.5", + "source-map": "~0.6.0", + "tslib": "^1.10.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "less-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-6.2.0.tgz", + "integrity": "sha512-Cl5h95/Pz/PWub/tCBgT1oNMFeH1WTD33piG80jn5jr12T4XbxZcjThwNXDQ7AG649WEynuIzO4b0+2Tn9Qolg==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "less": "^3.11.3", + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.0" + } + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levenary": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", + "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", + "dev": true, + "requires": { + "leven": "^3.1.0" + } + }, + "license-webpack-plugin": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-2.3.0.tgz", + "integrity": "sha512-JK/DXrtN6UeYQSgkg5q1+pgJ8aiKPL9tnz9Wzw+Ikkf+8mJxG56x6t8O+OH/tAeF/5NREnelTEMyFtbJNkjH4w==", + "dev": true, + "requires": { + "@types/webpack-sources": "^0.1.5", + "webpack-sources": "^1.2.0" + } + }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "requires": { + "immediate": "~3.0.5" + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "localforage": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.1.tgz", + "integrity": "sha1-5JJ+BCMCuGTbMPMhHxO1xvDell0=", + "requires": { + "lie": "3.1.1" + } + }, + "localforage-cordovasqlitedriver": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/localforage-cordovasqlitedriver/-/localforage-cordovasqlitedriver-1.7.0.tgz", + "integrity": "sha1-i5OVd1nuaI06WNW6fAR39sy1ODg=", + "requires": { + "localforage": ">=1.5.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "loglevel": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz", + "integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "make-fetch-happen": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz", + "integrity": "sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag==", + "dev": true, + "requires": { + "agentkeepalive": "^3.4.1", + "cacache": "^12.0.0", + "http-cache-semantics": "^3.8.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "node-fetch-npm": "^2.0.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^6.0.0" + }, + "dependencies": { + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + } + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "marked": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-1.2.0.tgz", + "integrity": "sha512-tiRxakgbNPBr301ihe/785NntvYyhxlqcL3YaC8CaxJQh7kiaEtrN9B/eK2I2943Yjkh5gw25chYFDQhOMCwMA==" + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "merkle-lib": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/merkle-lib/-/merkle-lib-2.0.10.tgz", + "integrity": "sha1-grjbrnXieneFOItz+ddyXQ9vMyY=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.10.0.tgz", + "integrity": "sha512-QgKgJBjaJhxVPwrLNqqwNS0AGkuQQ31Hp4xGXEK/P7wehEg6qmNtReHKai3zRXqY60wGVWLYcOMJK2b98aGc3A==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "native-request": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.0.8.tgz", + "integrity": "sha512-vU2JojJVelUGp6jRcLwToPoWGxSx23z/0iX+I77J3Ht17rf2INGjrhOoQnjVo60nQd8wVsgzKkPfRXBiVdD2ag==", + "dev": true, + "optional": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-fetch-npm": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz", + "integrity": "sha512-iOuIQDWDyjhv9qSDrj9aq/klt6F9z1p2otB3AV7v3zBDcL/x+OfGsvGQZZCcMZbUf4Ujw1xGNQkjvGnVT22cKg==", + "dev": true, + "requires": { + "encoding": "^0.1.11", + "json-parse-better-errors": "^1.0.0", + "safe-buffer": "^5.1.1" + } + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "dev": true + }, + "node-html-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.3.1.tgz", + "integrity": "sha512-AwYVI6GyEKj9NGoyMfSx4j5l7Axf7obQgLWGxtasLjED6RggTTQoq5ZRzjwSUfgSZ+Mv8Nzbi3pID0gFGqNUsA==", + "dev": true, + "requires": { + "he": "1.2.0" + } + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "node-releases": { + "version": "1.1.64", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.64.tgz", + "integrity": "sha512-Iec8O9166/x2HRMJyLLLWkd0sFFLrFNy+Xf+JQfSQsdBJzPcHpNl3JQ9gD4j+aJxmCa25jNsIbM4bmACtSbkSg==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true + }, + "npm-bundled": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-install-checks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", + "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", + "dev": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "npm-package-arg": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.0.1.tgz", + "integrity": "sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ==", + "dev": true, + "requires": { + "hosted-git-info": "^3.0.2", + "semver": "^7.0.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "dev": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz", + "integrity": "sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw==", + "dev": true, + "requires": { + "npm-install-checks": "^4.0.0", + "npm-package-arg": "^8.0.0", + "semver": "^7.0.0" + } + }, + "npm-registry-fetch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.7.tgz", + "integrity": "sha512-cny9v0+Mq6Tjz+e0erFAB+RYJ/AVGzkjnISiobqP8OWj9c9FLoZZu8/SPSKJWE17F1tk4018wfjV+ZbIbqC7fQ==", + "dev": true, + "requires": { + "JSONStream": "^1.3.4", + "bluebird": "^3.5.1", + "figgy-pudding": "^3.4.1", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "npm-package-arg": "^6.1.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "npm-package-arg": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", + "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "dev": true, + "requires": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-is": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz", + "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.2.0.tgz", + "integrity": "sha512-4HeyhxCvBTI5uBePsAdi55C5fmqnWZ2e2MlmvWi5KW5tdH5rxoiv/aMtbeVxKZc3eWkT1GymMnLG8XC4Rq4TDQ==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + }, + "dependencies": { + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + } + } + }, + "ora": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.0.0.tgz", + "integrity": "sha512-s26qdWqke2kjN/wC4dy+IQPBIMWBJlSU/0JZhk30ZDBLelW25rv66yutUWARMigpGPzcXHb+Nac5pNhN/WsARw==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.4.0", + "is-interactive": "^1.0.0", + "log-symbols": "^4.0.0", + "mute-stream": "0.0.8", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "pacote": { + "version": "9.5.12", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.5.12.tgz", + "integrity": "sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.3", + "cacache": "^12.0.2", + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.1.0", + "glob": "^7.1.3", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "minimatch": "^3.0.4", + "minipass": "^2.3.5", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "normalize-package-data": "^2.4.0", + "npm-normalize-package-bin": "^1.0.0", + "npm-package-arg": "^6.1.0", + "npm-packlist": "^1.1.12", + "npm-pick-manifest": "^3.0.0", + "npm-registry-fetch": "^4.0.0", + "osenv": "^0.1.5", + "promise-inflight": "^1.0.1", + "promise-retry": "^1.1.1", + "protoduck": "^5.0.1", + "rimraf": "^2.6.2", + "safe-buffer": "^5.1.2", + "semver": "^5.6.0", + "ssri": "^6.0.1", + "tar": "^4.4.10", + "unique-filename": "^1.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "npm-package-arg": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", + "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "dev": true, + "requires": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "npm-pick-manifest": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz", + "integrity": "sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1", + "npm-package-arg": "^6.0.0", + "semver": "^5.4.1" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "dev": true, + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pbkdf2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" + }, + "pnp-webpack-plugin": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", + "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==", + "dev": true, + "requires": { + "ts-pnp": "^1.1.6" + } + }, + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "7.0.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", + "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-calc": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", + "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", + "dev": true, + "requires": { + "postcss": "^7.0.27", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.2" + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-import": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-12.0.1.tgz", + "integrity": "sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "postcss-value-parser": "^3.2.3", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-load-config": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", + "integrity": "sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + } + }, + "postcss-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", + "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "postcss": "^7.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "dev": true, + "requires": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + } + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "promise-retry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", + "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "dev": true, + "requires": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + }, + "dependencies": { + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", + "dev": true + } + } + }, + "protoduck": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz", + "integrity": "sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==", + "dev": true, + "requires": { + "genfun": "^5.0.0" + } + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "pushdata-bitcoin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", + "integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=", + "requires": { + "bitcoin-ops": "^1.3.0" + } + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "qrcode": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.2.tgz", + "integrity": "sha512-eR6RgxFYPDFH+zFLTJKtoNP/RlsHANQb52AUmQ2bGDPMuUw7jJb0F+DNEgx7qQGIElrbFxWYMc0/B91zLZPF9Q==", + "requires": { + "dijkstrajs": "^1.0.1", + "isarray": "^2.0.1", + "pngjs": "^3.3.0", + "yargs": "^13.2.4" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "raw-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.1.tgz", + "integrity": "sha512-baolhQBSi3iNh1cglJjA0mYzga+wePk7vdEX//1dTFd+v4TsQlQE0jitJSNF1OIP82rdYulH7otaVmdlDaJ64A==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^2.6.5" + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "dev": true, + "requires": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "read-package-tree": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz", + "integrity": "sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==", + "dev": true, + "requires": { + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "util-promisify": "^2.1.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "dev": true, + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, + "regenerate": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", + "integrity": "sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", + "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, + "regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "regexpu-core": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.2.0", + "regjsgen": "^0.5.1", + "regjsparser": "^0.6.4", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.2.0" + } + }, + "regjsgen": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "dev": true + }, + "regjsparser": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", + "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", + "integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==", + "dev": true, + "requires": { + "is-core-module": "^2.0.0", + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "resolve-url-loader": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz", + "integrity": "sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==", + "dev": true, + "requires": { + "adjust-sourcemap-loader": "3.0.0", + "camelcase": "5.3.1", + "compose-function": "3.0.3", + "convert-source-map": "1.7.0", + "es6-iterator": "2.0.3", + "loader-utils": "1.2.3", + "postcss": "7.0.21", + "rework": "1.0.1", + "rework-visit": "1.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "postcss": { + "version": "7.0.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz", + "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rework": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz", + "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=", + "dev": true, + "requires": { + "convert-source-map": "^0.3.3", + "css": "^2.0.0" + }, + "dependencies": { + "convert-source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", + "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=", + "dev": true + } + } + }, + "rework-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz", + "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=", + "dev": true + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rollup": { + "version": "2.26.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.26.5.tgz", + "integrity": "sha512-rCyFG3ZtQdnn9YwfuAVH0l/Om34BdO5lwCA0W6Hq+bNB21dVEBbCRxhaHOmu1G7OBFDWytbzAC104u7rxHwGjA==", + "dev": true, + "requires": { + "fsevents": "~2.1.2" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", + "dev": true + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sass": { + "version": "1.26.10", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.26.10.tgz", + "integrity": "sha512-bzN0uvmzfsTvjz0qwccN1sPm2HxxpNI/Xa+7PlUEMS+nQvbyuEK7Y0qFqxlPHhiNHb1Ze8WQJtU31olMObkAMw==", + "dev": true, + "requires": { + "chokidar": ">=2.0.0 <4.0.0" + } + }, + "sass-loader": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.1.tgz", + "integrity": "sha512-b2PSldKVTS3JcFPHSrEXh3BeAfR7XknGiGCAO5aHruR3Pf3kqLP3Gb2ypXLglRrAzgZkloNxLZ7GXEGDX0hBUQ==", + "dev": true, + "requires": { + "klona": "^2.0.3", + "loader-utils": "^2.0.0", + "neo-async": "^2.6.2", + "schema-utils": "^2.7.0", + "semver": "^7.3.2" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selfsigned": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz", + "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==", + "dev": true, + "requires": { + "node-forge": "^0.10.0" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "semver-intersect": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/semver-intersect/-/semver-intersect-1.4.0.tgz", + "integrity": "sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ==", + "dev": true, + "requires": { + "semver": "^5.0.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + } + } + }, + "smart-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sockjs": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", + "dev": true, + "requires": { + "faye-websocket": "^0.10.0", + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "sockjs-client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", + "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", + "dev": true, + "requires": { + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + } + } + }, + "socks": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", + "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "dev": true, + "requires": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "dev": true, + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-loader": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.0.2.tgz", + "integrity": "sha512-oX8d6ndRjN+tVyjj6PlXSyFPhDdVAPsZA30nD3/II8g4uOv8fCz0DMn5sy8KtVbDfKQxOpGwGJnK3xIW3tauDw==", + "dev": true, + "requires": { + "data-urls": "^2.0.0", + "iconv-lite": "^0.6.2", + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.0", + "source-map": "^0.6.1" + } + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", + "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", + "dev": true + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "speed-measure-webpack-plugin": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.3.tgz", + "integrity": "sha512-2ljD4Ch/rz2zG3HsLsnPfp23osuPBS0qPuz9sGpkNXTN1Ic4M+W9xB8l8rS8ob2cO4b1L+WTJw/0AJwWYVgcxQ==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", + "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "style-loader": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.2.1.tgz", + "integrity": "sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^2.6.6" + } + }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "stylus": { + "version": "0.54.8", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", + "integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", + "dev": true, + "requires": { + "css-parse": "~2.0.0", + "debug": "~3.1.0", + "glob": "^7.1.6", + "mkdirp": "~1.0.4", + "safer-buffer": "^2.1.2", + "sax": "~1.2.4", + "semver": "^6.3.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "stylus-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz", + "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==", + "dev": true, + "requires": { + "loader-utils": "^1.0.2", + "lodash.clonedeep": "^4.5.0", + "when": "~3.6.x" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "tar": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", + "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + } + } + }, + "terser": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.0.tgz", + "integrity": "sha512-XTT3D3AwxC54KywJijmY2mxZ8nJiEjBHVYzq8l9OaYuRFWeQNBwvipuzzYEP4e+/AVcd1hqG/CqgsdIRyT45Fg==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + } + }, + "terser-webpack-plugin": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-4.1.0.tgz", + "integrity": "sha512-0ZWDPIP8BtEDZdChbufcXUigOYk6dOX/P/X0hWxqDDcVAQLb8Yy/0FAaemSfax3PAA67+DJR778oz8qVbmy4hA==", + "dev": true, + "requires": { + "cacache": "^15.0.5", + "find-cache-dir": "^3.3.1", + "jest-worker": "^26.3.0", + "p-limit": "^3.0.2", + "schema-utils": "^2.6.6", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^5.0.0", + "webpack-sources": "^1.4.3" + }, + "dependencies": { + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "tiny-secp256k1": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.5.tgz", + "integrity": "sha512-duE2hSLSQIpHGzmK48OgRrGTi+4OTkXLC6aa86uOYQ6LLCYZSarVKIAvEtY7MoXjoL6bOXMSerEGMzrvW4SkDw==", + "requires": { + "bindings": "^1.3.0", + "bn.js": "^4.11.8", + "create-hmac": "^1.1.7", + "elliptic": "^6.4.0", + "nan": "^2.13.2" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, + "ts-node": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", + "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, + "ts-pnp": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", + "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==", + "dev": true + }, + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + }, + "tslint": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.13.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, + "typescript": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", + "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", + "dev": true + }, + "uglify-js": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.3.tgz", + "integrity": "sha512-wDRziHG94mNj2n3R864CvYw/+pc9y/RNImiTyrrf8BzgWn75JgFSwYvXrtZQMnMnOp/4UTrf3iCSQxSStPiByA==", + "optional": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", + "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", + "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universal-analytics": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.4.23.tgz", + "integrity": "sha512-lgMIH7XBI6OgYn1woDEmxhGdj8yDefMKg7GkWdeATAlQZFrMrNyxSkpDzY57iY0/6fdlzTbBV03OawvvzG+q7A==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "request": "^2.88.2", + "uuid": "^3.0.0" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util-promisify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz", + "integrity": "sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "varuint-bitcoin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", + "requires": { + "safe-buffer": "^5.1.1" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "vendors": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "watchpack": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", + "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", + "dev": true, + "requires": { + "chokidar": "^3.4.1", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.0" + } + }, + "watchpack-chokidar2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz", + "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==", + "dev": true, + "optional": true, + "requires": { + "chokidar": "^2.1.8" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "optional": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true, + "optional": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "optional": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "webpack": { + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.1.tgz", + "integrity": "sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.3.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.7.4", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + } + }, + "terser-webpack-plugin": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", + "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", + "dev": true, + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "webpack-dev-server": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.7", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "0.3.20", + "sockjs-client": "1.4.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } + } + }, + "webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "webpack-subresource-integrity": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.4.1.tgz", + "integrity": "sha512-XMLFInbGbB1HV7K4vHWANzc1CN0t/c4bBvnlvGxGwV45yE/S/feAXIm8dJsCkzqWtSKnmaEgTp/meyeThxG4Iw==", + "dev": true, + "requires": { + "webpack-sources": "^1.3.0" + } + }, + "websocket-driver": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", + "dev": true, + "requires": { + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz", + "integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + }, + "when": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz", + "integrity": "sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", + "requires": { + "bs58check": "<3.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "worker-plugin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-5.0.0.tgz", + "integrity": "sha512-AXMUstURCxDD6yGam2r4E34aJg6kW85IiaeX72hi+I1cxyaMUtrvVY6sbfpGKAj5e7f68Acl62BjQF5aOOx2IQ==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "zone.js": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.2.tgz", + "integrity": "sha512-JUpNKmWIovqRZlqkX6pFdBVlOU42n5Mt1n2yEaJdy+msBant/l2L1hTG6BFxCzM+KV3SX4XQOcwbhnwsPAeUTA==", + "requires": { + "tslib": "^2.0.0" + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 000000000..1c88a5088 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,62 @@ +{ + "name": "embassy-ui", + "version": "0.2.5", + "description": "GUI for EmbassyOS", + "author": "Start9 Labs", + "homepage": "https://github.com/Start9Labs/embassy-ui", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "build-prod": "ng build --prod && tsc postprocess.ts && node postprocess.js", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" + }, + "private": true, + "dependencies": { + "@angular/common": "^10.1.6", + "@angular/core": "^10.1.6", + "@angular/forms": "^10.1.6", + "@angular/platform-browser": "^10.1.6", + "@angular/platform-browser-dynamic": "^10.1.6", + "@angular/router": "^10.1.6", + "@ionic/angular": "^5.4.0", + "@ionic/storage": "2.2.0", + "@start9labs/ambassador-sdk": "file:../ambassador-sdk", + "@start9labs/emver": "^0.1.1", + "ajv": "^6.12.6", + "angularx-qrcode": "^10.0.11", + "base32.js": "^0.1.0", + "base64url": "^3.0.1", + "bip39": "^3.0.2", + "bitcoinjs-lib": "^5.2.0", + "compare-versions": "^3.5.0", + "core-js": "^3.4.0", + "handlebars": "^4.7.6", + "json-pointer": "^0.6.1", + "jsonpointerx": "^1.0.30", + "jsontokens": "^3.0.0", + "marked": "^1.2.0", + "rxjs": "^6.6.3", + "uuid": "^8.3.1", + "zone.js": "^0.11.2" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^0.1002.0", + "@angular/cli": "^10.1.7", + "@angular/compiler": "^10.1.6", + "@angular/compiler-cli": "^10.1.6", + "@angular/language-service": "^10.1.6", + "@ionic/angular-toolkit": "^2.3.3", + "@ionic/lab": "^3.2.9", + "@types/json-pointer": "^1.0.30", + "@types/marked": "^1.1.0", + "@types/node": "^14.11.10", + "@types/uuid": "^8.0.0", + "node-html-parser": "^1.3.1", + "ts-node": "^9.0.0", + "tslint": "^6.1.0", + "typescript": "^4.0.3" + } +} diff --git a/ui/postprocess.ts b/ui/postprocess.ts new file mode 100644 index 000000000..2aa4aa897 --- /dev/null +++ b/ui/postprocess.ts @@ -0,0 +1,13 @@ +import { parse } from 'node-html-parser' +import * as fs from 'fs' + +let index = fs.readFileSync('./www/index.html').toString('utf-8') + +const root = parse(index) +for (let elem of root.querySelectorAll('link')) { + if (elem.getAttribute('rel') === 'stylesheet') { + const sheet = fs.readFileSync('./www/' + elem.getAttribute('href')).toString('utf-8') + index = index.replace(elem.toString(), '') + } +} +fs.writeFileSync('./www/index.html', index) \ No newline at end of file diff --git a/ui/src/app/app-config/config-cursor.ts b/ui/src/app/app-config/config-cursor.ts new file mode 100644 index 000000000..b09d46706 --- /dev/null +++ b/ui/src/app/app-config/config-cursor.ts @@ -0,0 +1,446 @@ +import { + ValueSpec, ConfigSpec, UniqueBy, ValueSpecOf, ValueType +} from './config-types' +import * as pointer from 'json-pointer' +import * as handlebars from 'handlebars' +import { Annotations, getDefaultObject, getDefaultUnion, listInnerSpec, mapConfigSpec, Range } from './config-utilities' + +export class ConfigCursor { + private cachedSpec?: ValueSpecOf + + constructor ( + private readonly rootSpec: ConfigSpec, + private readonly rootOldConfig: object, + private readonly rootMappedConfig: object = null, + private readonly rootConfig: object = null, + private readonly ptr: string = '', + ) { + if (!this.rootOldConfig) { + this.rootOldConfig = getDefaultObject(this.rootSpec) + } + if (!this.rootMappedConfig) { + this.rootMappedConfig = JSON.parse(JSON.stringify(this.rootOldConfig)) + mapConfigSpec(this.rootSpec, this.rootMappedConfig) + } + if (!this.rootConfig) { + this.rootConfig = JSON.parse(JSON.stringify(this.rootMappedConfig)) + } + } + + seek (ptr: string): ConfigCursor { + return new ConfigCursor( + this.rootSpec, + this.rootOldConfig, + this.rootMappedConfig, + this.rootConfig, + pointer.compile( + pointer.parse(this.ptr) + .concat(pointer.parse(ptr)), + ), + ) + } + + seekNext (key: string | number): ConfigCursor { + return this.seek(pointer.compile([`${key}`])) + } + + unseek (levels?: number): ConfigCursor { + let ptr: string + if (levels === undefined) { + ptr = '' + } else { + // TODO, delete or make use of, it isn't being used so far + // This is not being used so far + let ptr_arr = pointer.parse(this.ptr) + for (let i = 0; i < levels; i++) { + ptr_arr.pop() + } + ptr = pointer.compile(ptr_arr) + } + return new ConfigCursor( + this.rootSpec, + this.rootOldConfig, + this.rootMappedConfig, + this.rootConfig, + ptr, + ) + } + + key (): string { + return pointer.parse(this.ptr).pop() + } + + oldConfig (): any { + if (pointer.has(this.rootOldConfig, this.ptr)) { + return pointer.get(this.rootOldConfig, this.ptr) + } else { + return undefined + } + } + + mappedConfig (): any { + if (pointer.has(this.rootMappedConfig, this.ptr)) { + return pointer.get(this.rootMappedConfig, this.ptr) + } else { + return undefined + } + } + + toString (): string { + const spec: ValueSpec = this.spec() + const config = this.config() + switch (spec.type) { + case 'string': + return config + case 'number': + return `${config}${spec.units ? ' ' + spec.units : ''}` + case 'object': + return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : '' + case 'union': + return spec.displayAs ? handlebars.compile(spec.displayAs)(config) : config[spec.tag.id] + case 'pointer': + return 'System Defined' + default: + return '' + } + } + + // if (config : T) then (spec : ValueSpecOf) + config (): any { + if (pointer.has(this.rootConfig, this.ptr)) { + return pointer.get(this.rootConfig, this.ptr) + } else { + return undefined + } + } + + // if (config : T) then (spec : ValueSpecOf) + spec (): ValueSpecOf { + if (this.cachedSpec) return this.cachedSpec + const parsed = pointer.parse(this.ptr) + + // We elevate the rootSpec (ConfigSpec) to a dummy ValueSpecObject + let ret: ValueSpec = { + type: 'object', + spec: this.rootSpec, + nullable: false, + nullByDefault: false, + name: 'Config', + displayAs: 'Config', + uniqueBy: null, + } + let ptr = [] + for (let seg of parsed) { + switch (ret.type) { + case 'object': + ret = ret.spec[seg] + break + case 'union': + if (seg === ret.tag.id) { + ret = { + type: 'enum', + default: ret.default, + values: Object.keys(ret.variants), + name: ret.tag.name, + description: ret.tag.description, + valueNames: ret.tag.variantNames, + } + } else { + const cfg = this.unseek().seek(pointer.compile(ptr)) + ret = ret.variants[cfg.config()[ret.tag.id]][seg] + } + break + case 'list': + //in essence, for a list we replace the list typed ValueSpecOf with it's internal ListValueSpec, a ValueSpecOf where config @ ptr is of type T[]. + // we also append default values to it. + // note also that jsonKey is not used. jsonKey in this case is an index of an array, like 0, 1, etc. + // this implies that every index of a list has an identical inner spec + ret = listInnerSpec(ret) + break + default: + return undefined + } + if (ret === undefined) break + ptr.push(seg) + } + this.cachedSpec = ret as ValueSpecOf + return this.cachedSpec + } + + checkInvalid (): string | null { // null if valid + const spec: ValueSpec = this.spec() + const cfg = this.config() + switch (spec.type) { + case 'string': + if (!cfg) { + return spec.nullable ? null : `${spec.name} is missing.` + } else if (typeof cfg === 'string') { + if (!spec.pattern || new RegExp(spec.pattern).test(cfg)) { + return null + } else { + return spec.patternDescription + } + } else { + throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`) + } + case 'number': + if (!cfg) { + return spec.nullable ? null : `${spec.name} is missing.` + } else if (typeof cfg === 'number') { + if (spec.integral && cfg != Math.trunc(cfg)) { + return `${spec.name} must be an integer.` + } + try { + Range.from(spec.range).checkIncludes(cfg) + return null + } catch (e) { + return e.message + } + } else { + throw new TypeError(`${this.ptr}: expected number, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`) + } + case 'boolean': + if (typeof cfg === 'boolean') { + return null + } else { + throw new TypeError(`${this.ptr}: expected boolean, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`) + } + case 'enum': + if (typeof cfg === 'string') { + return spec.values.includes(cfg) ? null : `${cfg} is not a valid selection.` + } else { + throw new TypeError(`${this.ptr}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`) + } + case 'list': + if (Array.isArray(cfg)) { + const range = Range.from(spec.range) + const min = range.integralMin() + const max = range.integralMax() + const length = cfg.length + if (min && length < min) { + return spec.subtype === 'enum' ? 'Not enough options selected.' : 'List is too short.' + } + if (max && length > max) { + return spec.subtype === 'enum' ? 'Too many options selected.' : 'List is too long.' + } + for (let idx in cfg) { + let cursor = this.seekNext(idx) + if (cursor.checkInvalid()) { + return `Item #${idx + 1} is invalid. ${cursor.checkInvalid()}` + } + for (let idx2 in cfg) { + if (idx !== idx2 && cursor.equals(this.seekNext(idx2))) { + return `Item #${idx + 1} is not unique.` + } + } + } + return null + } else { + throw new TypeError(`${this.ptr}: expected array, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`) + } + case 'object': + if (!cfg) { + return spec.nullable ? null : `${spec.name} is missing.` + } else if (typeof cfg === 'object' && !Array.isArray(cfg)) { + for (let idx in spec.spec) { + if (this.seekNext(idx).checkInvalid()) { + return `${spec.spec[idx].name} is invalid.` + } + } + return null + } else { + throw new TypeError(`${this.ptr}: expected object, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`) + } + case 'pointer': + return null + case 'union': + if (typeof cfg === 'object' && !Array.isArray(cfg)) { + if (typeof cfg[spec.tag.id] === 'string') { + for (let idx in spec.variants[cfg[spec.tag.id]]) { + if (this.seekNext(idx).checkInvalid()) { + return `${spec.variants[cfg[spec.tag.id]][idx].name} is invalid.` + } + } + return null + } else { + throw new TypeError(`${this.ptr}/${spec.tag.id}: expected string, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`) + } + } else { + throw new TypeError(`${this.ptr}: expected object, got ${Array.isArray(cfg) ? 'array' : typeof cfg}`) + } + } + } + + isNew (): boolean { + const oldCfg = this.oldConfig() + const mappedCfg = this.mappedConfig() + if (mappedCfg && oldCfg && typeof mappedCfg === 'object' && typeof oldCfg === 'object') { + for (let key in mappedCfg) { + if (this.seekNext(key).isNew()) return true + } + return false + } else { + return mappedCfg !== oldCfg + } + } + + isEdited (): boolean { + const cfg = this.config() + const mappedCfg = this.mappedConfig() + if (cfg && mappedCfg && typeof cfg === 'object' && typeof mappedCfg === 'object') { + const spec = this.spec() + let allKeys + if (spec.type === 'union') { + let unionSpec = spec as ValueSpecOf<'union'> + const labelForSelection = unionSpec.tag.id + allKeys = new Set([...Object.keys(unionSpec.variants[cfg[labelForSelection]])]) + } else { + allKeys = new Set([...Object.keys(cfg), ...Object.keys(mappedCfg)]) + } + + for (let key of allKeys) { + if (this.seekNext(key).isEdited()) return true + } + return false + } else { + return cfg !== mappedCfg + } + } + + equals (cursor: ConfigCursor): boolean { + const lhs = this.config() + const rhs = cursor.config() + const spec: ValueSpec = this.spec() + + switch (spec.type) { + case 'string': + case 'number': + case 'boolean': + case 'enum': + return lhs === rhs + case 'object': + case 'union': + return isEqual(spec.uniqueBy, this as ConfigCursor<'object' | 'union'>, cursor as ConfigCursor<'object' | 'union'>) + case 'list': + if (lhs.length !== rhs.length) { + return false + } + for (let idx = 0; idx < lhs.length; idx++) { + if (!this.seekNext(`${idx}`).equals(cursor.seekNext(`${idx}`))) { + return false + } + } + return true + default: + return false + } + } + + getAnnotations (): Annotations { + const spec: ValueSpec = this.spec() + switch (spec.type) { + case 'object': { + const ret: Annotations<'object'> = { + self: { + invalid: this.checkInvalid(), + edited: this.isEdited(), + added: this.isNew(), + }, + members: { }, + } + for (let key in spec.spec) { + let annotation: any = this.seekNext(key).getAnnotations() + if ('self' in annotation) { + annotation = annotation.self + } + ret.members[key] = annotation + } + return ret as Annotations + } + case 'union': { + const ret: Annotations<'union'> = { + self: { + invalid: this.checkInvalid(), + edited: this.isEdited(), + added: this.isNew(), + }, + members: { + [spec.tag.id]: this.seekNext<'enum'>(spec.tag.id).getAnnotations(), + }, + } + for (let key in spec.variants[this.config()[spec.tag.id]]) { + let annotation: any = this.seekNext(key).getAnnotations() + if ('self' in annotation) { + annotation = annotation.self + } + ret.members[key] = annotation + } + return ret as Annotations + } + case 'list': { + const ret: Annotations<'list'> = { + self: { + invalid: this.checkInvalid(), + edited: this.isEdited(), + added: this.isNew(), + }, + members: [], + } + for (let key in this.config()) { + let annotation: any = this.seekNext(key).getAnnotations() + if ('self' in annotation) { + annotation = annotation.self + } + ret.members[key] = annotation + } + return ret as Annotations + } + default: + return { + invalid: this.checkInvalid(), + edited: this.isEdited(), + added: this.isNew(), + } as Annotations + } + } + + async createFirstEntryForList () { + const spec: ValueSpec = this.spec() + + if (spec.type === 'object' && !this.config()) { + pointer.set(this.rootConfig, this.ptr, getDefaultObject(spec.spec)) + } + + if (spec.type === 'union' && !this.config()) { + pointer.set(this.rootConfig, this.ptr, getDefaultUnion(spec)) + } + } + + injectModalData (res: { data?: any }): void { + if (res.data !== undefined) { + pointer.set(this.rootConfig, this.ptr, res.data) + } + } +} + +function isEqual (uniqueBy: UniqueBy, lhs: ConfigCursor<'object'>, rhs: ConfigCursor<'object'>): boolean { + if (uniqueBy === null) { + return false + } else if (typeof uniqueBy === 'string') { + return lhs.seekNext(uniqueBy).equals(rhs.seekNext(uniqueBy)) + } else if ('any' in uniqueBy) { + for (let subSpec of uniqueBy.any) { + if (isEqual(subSpec, lhs, rhs)) { + return true + } + } + return false + } else if ('all' in uniqueBy) { + for (let subSpec of uniqueBy.all) { + if (!isEqual(subSpec, lhs, rhs)) { + return false + } + } + return true + } +} \ No newline at end of file diff --git a/ui/src/app/app-config/config-types.ts b/ui/src/app/app-config/config-types.ts new file mode 100644 index 000000000..c298b3b0b --- /dev/null +++ b/ui/src/app/app-config/config-types.ts @@ -0,0 +1,133 @@ +export interface ConfigSpec { [key: string]: ValueSpec } + +export type ValueType = 'string' | 'number' | 'boolean' | 'enum' | 'list' | 'object' | 'pointer' | 'union' +export type ValueSpec = ValueSpecOf + +// core spec types. These types provide the metadata for performing validations +export type ValueSpecOf = + T extends 'string' ? ValueSpecString : + T extends 'number' ? ValueSpecNumber : + T extends 'boolean' ? ValueSpecBoolean : + T extends 'enum' ? ValueSpecEnum : + T extends 'list' ? ValueSpecList : + T extends 'object' ? ValueSpecObject : + T extends 'pointer' ? ValueSpecPointer : + T extends 'union' ? ValueSpecUnion : + never + +export interface ValueSpecString extends ListValueSpecString, WithStandalone { + type: 'string' + default?: DefaultString + nullable: boolean + masked: boolean + copyable: boolean +} + +export interface ValueSpecNumber extends ListValueSpecNumber, WithStandalone { + type: 'number' + nullable: boolean + default?: number +} + +export interface ValueSpecEnum extends ListValueSpecEnum, WithStandalone { + type: 'enum' + default: string +} + +export interface ValueSpecBoolean extends WithStandalone { + type: 'boolean' + default: boolean +} + +export interface ValueSpecUnion extends ListValueSpecUnion, WithStandalone { + type: 'union' +} + +export interface ValueSpecPointer extends WithStandalone { + type: 'pointer' + subtype: 'app' | 'system' + target: 'lan-address' | 'tor-address' | 'config' + 'app-id': string +} + +export interface ValueSpecObject extends ListValueSpecObject, WithStandalone { + type: 'object' + nullable: boolean + nullByDefault: boolean +} + +export interface WithStandalone { + name: string + description?: string + changeWarning?: string +} + +// no lists of booleans, lists, pointers +export type ListValueSpecType = 'string' | 'number' | 'enum' | 'object' | 'union' + +// represents a spec for the values of a list +export type ListValueSpecOf = + T extends 'string' ? ListValueSpecString : + T extends 'number' ? ListValueSpecNumber : + T extends 'enum' ? ListValueSpecEnum : + T extends 'object' ? ListValueSpecObject : + T extends 'union' ? ListValueSpecUnion : + never + +// represents a spec for a list +export type ValueSpecList = ValueSpecListOf +export interface ValueSpecListOf extends WithStandalone { + type: 'list' + subtype: T + spec: ListValueSpecOf + range: string // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules + default: string[] | number[] | DefaultString[] | object[] +} + +// sometimes the type checker needs just a little bit of help +export function isValueSpecListOf (t: ValueSpecList, s: S): t is ValueSpecListOf { + return t.subtype === s +} + +export interface ListValueSpecString { + pattern?: string + patternDescription?: string +} + +export interface ListValueSpecNumber { + range: string + integral: boolean + units?: string +} + +export interface ListValueSpecEnum { + values: string[] + valueNames: { [value: string]: string } +} + +export interface ListValueSpecObject { + spec: ConfigSpec //this is a mapped type of the config object at this level, replacing the object's values with specs on those values + uniqueBy: UniqueBy //indicates whether duplicates can be permitted in the list + displayAs?: string //this should be a handlebars template which can make use of the entire config which corresponds to 'spec' +} + +export type UniqueBy = null | string | { any: UniqueBy[] } | { all: UniqueBy[] } + +export interface ListValueSpecUnion { + tag: UnionTagSpec + variants: { [key: string]: ConfigSpec } + displayAs?: string //this may be a handlebars template which can conditionally (on tag.id) make use of each union's entries, or if left blank will display as tag.id + uniqueBy: UniqueBy + default: string //this should be the variantName which one prefers a user to start with by default when creating a new union instance in a list +} + +export interface UnionTagSpec { + id: string //The name of the field containing one of the union variants + name: string + description?: string + variantNames: { //the name of each variant + [variant: string]: string + } +} + +export type DefaultString = string | { charset: string, len: number } diff --git a/ui/src/app/app-config/config-utilities.ts b/ui/src/app/app-config/config-utilities.ts new file mode 100644 index 000000000..4f67ed843 --- /dev/null +++ b/ui/src/app/app-config/config-utilities.ts @@ -0,0 +1,413 @@ +import { + ValueSpec, ValueSpecList, DefaultString, ValueSpecUnion, ConfigSpec, + ValueSpecObject, ValueSpecString, ValueSpecEnum, ValueSpecNumber, + ValueSpecBoolean, ValueSpecPointer, ValueSpecOf, ListValueSpecType +} from './config-types' + +export interface Annotation { + invalid: string | null + edited: boolean + added: boolean +} + +export type Annotations = + T extends 'object' | 'union' ? { self: Annotation, members: { [key: string]: Annotation } } : + T extends 'list' ? { self: Annotation, members: Annotation[] } : + Annotation + +export class Range { + min?: number + max?: number + minInclusive: boolean + maxInclusive: boolean + + + static from (s: string): Range { + const r = new Range() + r.minInclusive = s.startsWith('[') + r.maxInclusive = s.endsWith(']') + const [minStr, maxStr] = s.split(',').map(a => a.trim()) + r.min = minStr === '(*' ? undefined : Number(minStr.slice(1)) + r.max = maxStr === '*)' ? undefined : Number(maxStr.slice(0, -1)) + return r + } + + checkIncludes (n: number) { + if (this.hasMin() !== undefined && ((!this.minInclusive && this.min == n || (this.min > n)))) { + throw new Error(`Value must be ${this.minMessage()}.`) + } + if (this.hasMax() && ((!this.maxInclusive && this.max == n || (this.max < n)))) { + throw new Error(`Value must be ${this.maxMessage()}.`) + } + } + + hasMin (): boolean { + return this.min !== undefined + } + + hasMax (): boolean { + return this.max !== undefined + } + + minMessage (): string { + return `greater than${this.minInclusive ? ' or equal to' : ''} ${this.min}` + } + + maxMessage (): string { + return `less than${this.maxInclusive ? ' or equal to' : ''} ${this.max}` + } + + description (): string { + let message = 'Value can be any number.' + + if (this.hasMin() || this.hasMax()) { + message = 'Value must be' + } + + if (this.hasMin() && this.hasMax()) { + message = `${message} ${this.minMessage()} AND ${this.maxMessage()}.` + } else if (this.hasMin() && !this.hasMax()) { + message = `${message} ${this.minMessage()}.` + } else if (!this.hasMin() && this.hasMax()) { + message = `${message} ${this.maxMessage()}.` + } + + return message + } + + integralMin (): number | undefined { + if (this.min) { + const ceil = Math.ceil(this.min) + if (this.minInclusive) { + return ceil + } else { + if (ceil === this.min) { + return ceil + 1 + } else { + return ceil + } + } + } + } + + integralMax (): number | undefined { + if (this.max) { + const floor = Math.floor(this.max) + if (this.maxInclusive) { + return floor + } else { + if (floor === this.max) { + return floor - 1 + } else { + return floor + } + } + } + } +} + +// converts a ValueSpecList, i.e. a spec for a list, to its inner ListValueSpec, i.e., a spec for the values within the list. +// We then augment it with the defalt values (e.g. nullable: false) to make a +export function listInnerSpec (listSpec: ValueSpecList): ValueSpecOf { + return { + type: listSpec.subtype, + nullable: false, + name: listSpec.name, + description: listSpec.description, + changeWarning: listSpec.changeWarning, + ...listSpec.spec as any, //listSpec.spec is a ListValueSpecOf listSpec.subtype + } +} + +export function mapSpecToConfigValue (spec: ValueSpec, value: any): any { + if (value === undefined) return undefined + switch (spec.type) { + case 'string': return mapStringSpec(value) + case 'number': return mapNumberSpec(value) + case 'boolean': return mapBooleanSpec(spec, value) + case 'enum': return mapEnumSpec(spec, value) + case 'list': return mapListSpec(spec, value) + case 'object': return mapObjectSpec(spec, value) + case 'union': return mapUnionSpec(spec, value) + case 'pointer': return value + } +} + +export function mapConfigSpec (configSpec: ConfigSpec, value: any): object { + if (value && typeof value === 'object' && !Array.isArray(value)) { + Object.entries(configSpec).map(([key, val]) => { + value[key] = mapSpecToConfigValue(val, value[key]) + if (value[key] === undefined) { + value[key] = getDefaultConfigValue(val) + } + }) + return value + } else { + return getDefaultObject(configSpec) + } +} + +export function mapObjectSpec (spec: ValueSpecObject, value: any): object { + if (value && typeof value === 'object' && !Array.isArray(value)) { + return mapConfigSpec(spec.spec, value) + } else { + return null + } +} + +export function mapUnionSpec (spec: ValueSpecUnion, value: any): object { + if (value && typeof value === 'object' && !Array.isArray(value)) { + const variant = mapEnumSpec({ + ...spec.tag, + type: 'enum', + default: spec.default, + values: Object.keys(spec.variants), + valueNames: spec.tag.variantNames, + }, value[spec.tag.id]) + value = mapConfigSpec(spec.variants[variant], value) + value[spec.tag.id] = variant + return value + } else { + return getDefaultUnion(spec) + } +} + +export function mapStringSpec (value: any): string { + if (typeof value === 'string') { + return value + } else { + return null + } +} + +export function mapNumberSpec (value: any): number { + if (typeof value === 'number') { + return value + } else { + return null + } +} + +export function mapEnumSpec (spec: ValueSpecEnum, value: any): string { + if (typeof value === 'string' && spec.values.includes(value)) { + return value + } else { + return spec.default + } +} + +export function mapListSpec (spec: ValueSpecList, value: any): string[] | number[] | object[] { + if (Array.isArray(value)) { + const innerSpec = listInnerSpec(spec) + return value.map(item => mapSpecToConfigValue(innerSpec, item)) + } else { + return getDefaultList(spec) + } +} + +export function mapBooleanSpec (spec: ValueSpecBoolean, value: any): boolean { + if (typeof value === 'boolean') { + return value + } else { + return spec.default + } +} + +export function getDefaultConfigValue (spec: ValueSpec): string | number | object | string[] | number[] | object[] | boolean | null { + switch (spec.type) { + case 'object': + return spec.nullByDefault ? null : getDefaultObject(spec.spec) + case 'union': + return getDefaultUnion(spec) + case 'string': + return spec.default ? getDefaultString(spec.default) : null + case 'number': + return spec.default || null + case 'list': + return getDefaultList(spec) + case 'enum': + case 'boolean': + return spec.default + case 'pointer': + return null + } +} + +export function getDefaultObject (spec: ConfigSpec): object { + const obj = { } + Object.entries(spec).map(([key, val]) => { + obj[key] = getDefaultConfigValue(val) + }) + + return obj +} + +export function getDefaultList (spec: ValueSpecList): string[] | number[] | object[] { + if (spec.subtype === 'object') { + const l = (spec.default as any[]) + const range = Range.from(spec.range) + while (l.length < range.integralMin()) { + l.push(getDefaultConfigValue(listInnerSpec(spec))) + } + return l as string[] | number[] | object[] + } else { + const l = (spec.default as any[]).map(d => getDefaultConfigValue({ ...listInnerSpec(spec), default: d })) + return l as string[] | number[] | object[] + } +} + +export function getDefaultUnion (spec: ValueSpecUnion): object { + return { [spec.tag.id]: spec.default, ...getDefaultObject(spec.variants[spec.default]) } +} + +export function getDefaultMapTagKey (defaultSpec: DefaultString = '', value: object): string { + const keySrc = getDefaultString(defaultSpec) + + const keys = Object.keys(value) + + let key = keySrc + let idx = 1 + while (keys.includes(key)) { + key = `${keySrc}-${idx++}` + } + + return key +} + +export function getDefaultString (defaultSpec: DefaultString): string { + if (typeof defaultSpec === 'string') { + return defaultSpec + } else { + let s = '' + for (let i = 0; i < defaultSpec.len; i++) { + s = s + getRandomCharInSet(defaultSpec.charset) + } + + return s + } +} + +export function getDefaultDescription (spec: ValueSpec): string { + let toReturn: string | undefined + switch (spec.type) { + case 'string': + if (typeof spec.default === 'string') { + toReturn = spec.default + } else if (typeof spec.default === 'object') { + toReturn = 'random' + } + break + case 'number': + if (typeof spec.default === 'number') { + toReturn = String(spec.default) + } + break + case 'boolean': + toReturn = spec.default === true ? 'True' : 'False' + break + case 'enum': + toReturn = spec.valueNames[spec.default] + break + } + + return toReturn || '' +} + +// a,g,h,A-Z,,,,- +export function getRandomCharInSet (charset: string): string { + const set = stringToCharSet(charset) + let charIdx = Math.floor(Math.random() * set.len) + for (let range of set.ranges) { + if (range.len > charIdx) { + return String.fromCharCode(range.start.charCodeAt(0) + charIdx) + } + charIdx -= range.len + } + throw new Error('unreachable') +} + +function stringToCharSet (charset: string): CharSet { + let set: CharSet = { ranges: [], len: 0 } + let start: string | null = null + let end: string | null = null + let in_range = false + for (let char of charset) { + switch (char) { + case ',': + if (start !== null && end !== null) { + if (start!.charCodeAt(0) > end!.charCodeAt(0)) { + throw new Error('start > end of charset') + } + const len = end.charCodeAt(0) - start.charCodeAt(0) + 1 + set.ranges.push({ + start, + end, + len, + }) + set.len += len + start = null + end = null + in_range = false + } else if (start !== null && !in_range) { + set.len += 1 + set.ranges.push({ start, end: start, len: 1 }) + start = null + } else if (start !== null && in_range) { + end = ',' + } else if (start === null && end === null && !in_range) { + start = ',' + } else { + throw new Error('unexpected ","') + } + break + case '-': + if (start === null) { + start = '-' + } else if (!in_range) { + in_range = true + } else if (in_range && end === null) { + end = '-' + } else { + throw new Error('unexpected "-"') + } + break + default: + if (start === null) { + start = char + } else if (in_range && end === null) { + end = char + } else { + throw new Error(`unexpected "${char}"`) + } + } + } + if (start !== null && end !== null) { + if (start!.charCodeAt(0) > end!.charCodeAt(0)) { + throw new Error('start > end of charset') + } + const len = end.charCodeAt(0) - start.charCodeAt(0) + 1 + set.ranges.push({ + start, + end, + len, + }) + set.len += len + } else if (start !== null) { + set.len += 1 + set.ranges.push({ + start, + end: start, + len: 1, + }) + } + return set +} + +interface CharSet { + ranges: { + start: string + end: string + len: number + }[] + len: number +} \ No newline at end of file diff --git a/ui/src/app/app-config/modal-presentable.ts b/ui/src/app/app-config/modal-presentable.ts new file mode 100644 index 000000000..52473b947 --- /dev/null +++ b/ui/src/app/app-config/modal-presentable.ts @@ -0,0 +1,27 @@ +import { ConfigCursor } from './config-cursor' +import { TrackingModalController } from '../services/tracking-modal-controller.service' + +export class ModalPresentable { + constructor (private readonly trackingModalCtrl: TrackingModalController) { } + + async presentModal (cursor: ConfigCursor, callback: () => any) { + const modal = await this.trackingModalCtrl.createConfigModal({ + backdropDismiss: false, + presentingElement: await this.trackingModalCtrl.getTop(), + componentProps: { + cursor, + }, + }, cursor.spec().type) + + modal.onWillDismiss().then(res => { + cursor.injectModalData(res) + callback() + }) + + await modal.present() + } + + dismissModal (a: any) { + return this.trackingModalCtrl.dismiss(a) + } +} diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts new file mode 100644 index 000000000..235803d9b --- /dev/null +++ b/ui/src/app/app-routing.module.ts @@ -0,0 +1,48 @@ +import { NgModule } from '@angular/core' +import { PreloadAllModules, RouterModule, Routes } from '@angular/router' +import { AuthGuard } from './guards/auth.guard' +import { UnauthGuard } from './guards/unauth.guard' + +const routes: Routes = [ + { + redirectTo: 'services', + pathMatch: 'full', + path: '', + }, + { + path: 'authenticate', + canActivate: [UnauthGuard], + pathMatch: 'full', + loadChildren: () => import('./pages/authenticate/authenticate.module').then( m => m.AuthenticatePageModule), + }, + { + path: 'embassy', + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], + loadChildren: () => import('./pages/server-routes/server-routing.module').then(m => m.ServerRoutingModule), + }, + { + path: 'notifications', + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], + loadChildren: () => import('./pages/notifications/notifications.module').then(m => m.NotificationsPageModule), + }, + { + path: 'services', + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], + loadChildren: () => import('./pages/apps-routes/apps-routing.module').then(m => m.AppsRoutingModule), + }, +] + +@NgModule({ + imports: [ + RouterModule.forRoot(routes, { + preloadingStrategy: PreloadAllModules, + initialNavigation: false, + useHash: true, + }), + ], + exports: [RouterModule], +}) +export class AppRoutingModule { } diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html new file mode 100644 index 000000000..62f63caf9 --- /dev/null +++ b/ui/src/app/app.component.html @@ -0,0 +1,159 @@ + + + + + + {{ name }} + + + + + + + + + + {{page.title}} + {{s}} + + + + + + + + Welcome + + +

This is the private website of your Start9 Embassy device.

+
+

Please authenticate yourself to continue.

+ + + + + + + + + Logout + + + + + + + + + + diff --git a/ui/src/app/app.component.scss b/ui/src/app/app.component.scss new file mode 100644 index 000000000..dbacbb664 --- /dev/null +++ b/ui/src/app/app.component.scss @@ -0,0 +1,13 @@ +.selected { + --background: linear-gradient(120deg, #1e1e1e -1%, var(--ion-color-start9) 96%); +} + +.menu-style { + border-style: solid; + border-width: 0px 1px 0px 0px; + border-color: #ff4960; +} + +.selected-badge { + background-color: #1e1e1e; +} \ No newline at end of file diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts new file mode 100644 index 000000000..233c8374c --- /dev/null +++ b/ui/src/app/app.component.ts @@ -0,0 +1,190 @@ +import { Component } from '@angular/core' +import { ServerModel, ServerStatus } from './models/server-model' +import { Storage } from '@ionic/storage' +import { SyncDaemon } from './services/sync.service' +import { AuthService, AuthState } from './services/auth.service' +import { ApiService } from './services/api/api.service' +import { Router } from '@angular/router' +import { BehaviorSubject, Observable } from 'rxjs' +import { AppModel } from './models/app-model' +import { filter, take } from 'rxjs/operators' +import { AlertController } from '@ionic/angular' +import { LoaderService } from './services/loader.service' +import { Emver } from './services/emver.service' +import { SplitPaneTracker } from './services/split-pane.service' +import { LoadingOptions } from '@ionic/core' + +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.scss'], +}) +export class AppComponent { + isUpdating = false + fullPageMenu = true + $showMenuContent$ = new BehaviorSubject(false) + serverName$ : Observable + serverBadge$: Observable + selectedIndex = 0 + appPages = [ + { + title: 'Services', + url: '/services/installed', + icon: 'grid-outline', + }, + { + title: 'Embassy', + url: '/embassy', + icon: 'cube-outline', + }, + { + title: 'Marketplace', + url: '/services/marketplace', + icon: 'cart-outline', + }, + { + title: 'Notifications', + url: '/notifications', + icon: 'notifications-outline', + }, + ] + + constructor ( + private readonly serverModel: ServerModel, + private readonly syncDaemon: SyncDaemon, + private readonly storage: Storage, + private readonly appModel: AppModel, + private readonly authService: AuthService, + private readonly router: Router, + private readonly api: ApiService, + private readonly alertCtrl: AlertController, + private readonly loader: LoaderService, + private readonly emver: Emver, + readonly splitPane: SplitPaneTracker, + ) { + // set dark theme + document.body.classList.toggle('dark', true) + this.serverName$ = this.serverModel.watch().name + this.serverBadge$ = this.serverModel.watch().badge + this.init() + } + + async init () { + let fromFresh = true + await this.storage.ready() + await this.authService.restoreCache() + await this.emver.init() + + this.authService.listen({ + [AuthState.VERIFIED]: async () => { + console.log('verified') + this.api.authenticatedRequestsEnabled = true + await this.serverModel.restoreCache() + await this.appModel.restoreCache() + this.syncDaemon.start() + this.$showMenuContent$.next(true) + if (fromFresh) { + this.router.initialNavigation() + fromFresh = false + } + }, + [AuthState.UNVERIFIED]: () => { + console.log('unverified') + this.api.authenticatedRequestsEnabled = false + this.serverModel.clear() + this.appModel.clear() + this.syncDaemon.stop() + this.storage.clear() + this.router.navigate(['/authenticate'], { replaceUrl: true }) + this.$showMenuContent$.next(false) + if (fromFresh) { + this.router.initialNavigation() + fromFresh = false + } + }, + }) + + this.serverModel.watch().status.subscribe(s => { + this.isUpdating = (s === ServerStatus.UPDATING) + }) + + this.router.events.pipe(filter(e => !!(e as any).urlAfterRedirects)).subscribe((e: any) => { + const appPageIndex = this.appPages.findIndex( + appPage => (e.urlAfterRedirects || e.url || '').startsWith(appPage.url), + ) + if (appPageIndex > -1) this.selectedIndex = appPageIndex + + // TODO: while this works, it is dangerous and impractical. + if (e.urlAfterRedirects !== '/embassy' && e.urlAfterRedirects !== '/authenticate' && this.isUpdating) { + this.router.navigateByUrl('/embassy') + } + }) + this.api.watch401$().subscribe(() => { + this.authService.setAuthStateUnverified() + return this.api.postLogout() + }) + } + + async presentAlertLogout () { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Caution', + message: 'Are you sure you want to logout?', + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Logout', + cssClass: 'alert-danger', + handler: () => { + this.logout() + }, + }, + ], + }) + await alert.present() + } + + private async logout () { + this.serverName$.pipe(take(1)).subscribe(name => { + this.loader.of(LoadingSpinner(`Logging out ${name || ''}...`)) + .displayDuringP(this.api.postLogout()) + .then(() => this.authService.setAuthStateUnverified()) + .catch(e => this.setError(e)) + }) + } + + async setError (e: Error) { + console.error(e) + await this.presentError(e.message) + } + + async presentError (e: string) { + const alert = await this.alertCtrl.create({ + backdropDismiss: true, + message: `Exception on logout: ${e}`, + buttons: [ + { + text: 'Dismiss', + role: 'cancel', + }, + ], + }) + await alert.present() + } + + splitPaneVisible (e) { + this.splitPane.$menuFixedOpenOnLeft$.next(e.detail.visible) + } +} + +const LoadingSpinner: (m?: string) => LoadingOptions = (m) => { + const toMergeIn = m ? { message: m } : { } + return { + spinner: 'lines', + cssClass: 'loader', + ...toMergeIn, + } as LoadingOptions +} diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts new file mode 100644 index 000000000..8d47f2f20 --- /dev/null +++ b/ui/src/app/app.module.ts @@ -0,0 +1,41 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA, Type } from '@angular/core' +import { BrowserModule } from '@angular/platform-browser' +import { RouteReuseStrategy } from '@angular/router' +import { IonicModule, IonicRouteStrategy } from '@ionic/angular' +import { IonicStorageModule } from '@ionic/storage' +import { HttpClientModule } from '@angular/common/http' +import { AppComponent } from './app.component' +import { AppRoutingModule } from './app-routing.module' +import { ApiService } from './services/api/api.service' +import { ApiServiceFactory } from './services/api/api.service.factory' +import { AppModel } from './models/app-model' +import { HttpService } from './services/http.service' +import { ServerModel } from './models/server-model' +import { ConfigService } from './services/config.service' +import { QRCodeModule } from 'angularx-qrcode' +import { APP_CONFIG_COMPONENT_MAPPING } from './modals/app-config-injectable/modal-injectable-token' +import { appConfigComponents } from './modals/app-config-injectable/modal-injectable-value'; + +@NgModule({ + declarations: [AppComponent], + entryComponents: [], + imports: [ + HttpClientModule, + BrowserModule, + IonicModule.forRoot(), + AppRoutingModule, + IonicStorageModule.forRoot(), + QRCodeModule, + ], + providers: [ + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + { provide: ApiService, useFactory: ApiServiceFactory, deps: [ConfigService, HttpService, AppModel, ServerModel] }, + { provide: APP_CONFIG_COMPONENT_MAPPING, useValue: appConfigComponents }, + ], + bootstrap: [AppComponent], + schemas: [ CUSTOM_ELEMENTS_SCHEMA ], +}) +export class AppModule { } + + + diff --git a/ui/src/app/components/badge-menu-button/badge-menu.component.html b/ui/src/app/components/badge-menu-button/badge-menu.component.html new file mode 100644 index 000000000..738173e18 --- /dev/null +++ b/ui/src/app/components/badge-menu-button/badge-menu.component.html @@ -0,0 +1,12 @@ +
+ + + +
diff --git a/ui/src/app/components/badge-menu-button/badge-menu.component.module.ts b/ui/src/app/components/badge-menu-button/badge-menu.component.module.ts new file mode 100644 index 000000000..0a2c247fc --- /dev/null +++ b/ui/src/app/components/badge-menu-button/badge-menu.component.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { BadgeMenuComponent } from './badge-menu.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' + +@NgModule({ + declarations: [ + BadgeMenuComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + ], + exports: [BadgeMenuComponent], +}) +export class BadgeMenuComponentModule { } diff --git a/ui/src/app/components/badge-menu-button/badge-menu.component.scss b/ui/src/app/components/badge-menu-button/badge-menu.component.scss new file mode 100644 index 000000000..26c065665 --- /dev/null +++ b/ui/src/app/components/badge-menu-button/badge-menu.component.scss @@ -0,0 +1,17 @@ +.ios-badge { + background-color: var(--ion-color-start9); + position: absolute; + top: 1px; + left: 62%; + border-radius: 5px; + z-index: 1; +} + +.md-badge { + background-color: var(--ion-color-start9); + position: absolute; + top: -2px; + left: 56%; + border-radius: 5px; + z-index: 1; +} diff --git a/ui/src/app/components/badge-menu-button/badge-menu.component.ts b/ui/src/app/components/badge-menu-button/badge-menu.component.ts new file mode 100644 index 000000000..3b7a0e858 --- /dev/null +++ b/ui/src/app/components/badge-menu-button/badge-menu.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core' +import { ServerModel } from '../../models/server-model' +import { Observable } from 'rxjs' +import { map } from 'rxjs/operators' +import { SplitPaneTracker } from 'src/app/services/split-pane.service' +import { isPlatform } from '@ionic/angular' + +@Component({ + selector: 'badge-menu-button', + templateUrl: './badge-menu.component.html', + styleUrls: ['./badge-menu.component.scss'], +}) + +export class BadgeMenuComponent { + badge$: Observable + menuFixedOpen$: Observable + isIos: boolean + + constructor ( + private readonly serverModel: ServerModel, + private readonly splitPane: SplitPaneTracker, + ) { + this.menuFixedOpen$ = this.splitPane.$menuFixedOpenOnLeft$.asObservable() + this.badge$ = this.serverModel.watch().badge.pipe(map(i => i > 0)) + this.isIos = isPlatform('ios') + } +} diff --git a/ui/src/app/components/config-header/config-header.component.html b/ui/src/app/components/config-header/config-header.component.html new file mode 100644 index 000000000..6a8bb7b6f --- /dev/null +++ b/ui/src/app/components/config-header/config-header.component.html @@ -0,0 +1,24 @@ + + + + + + {{ error }} + + + + + + +

Description

+

{{ spec.description }}

+
+
+ + + +

Warning!

+

{{ spec.changeWarning }}

+
+
+
\ No newline at end of file diff --git a/ui/src/app/components/config-header/config-header.component.module.ts b/ui/src/app/components/config-header/config-header.component.module.ts new file mode 100644 index 000000000..cec8c2822 --- /dev/null +++ b/ui/src/app/components/config-header/config-header.component.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { ConfigHeaderComponent } from './config-header.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' + +@NgModule({ + declarations: [ + ConfigHeaderComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + ], + exports: [ConfigHeaderComponent], +}) +export class ConfigHeaderComponentModule { } diff --git a/ui/src/app/components/config-header/config-header.component.scss b/ui/src/app/components/config-header/config-header.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/components/config-header/config-header.component.ts b/ui/src/app/components/config-header/config-header.component.ts new file mode 100644 index 000000000..b7fddf9e5 --- /dev/null +++ b/ui/src/app/components/config-header/config-header.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from '@angular/core' +import { ValueSpec } from 'src/app/app-config/config-types' + +@Component({ + selector: 'config-header', + templateUrl: './config-header.component.html', + styleUrls: ['./config-header.component.scss'], +}) +export class ConfigHeaderComponent { + @Input() spec: ValueSpec + @Input() error: string +} diff --git a/ui/src/app/components/dependency-list/dependency-list.component.html b/ui/src/app/components/dependency-list/dependency-list.component.html new file mode 100644 index 000000000..b33e5ec20 --- /dev/null +++ b/ui/src/app/components/dependency-list/dependency-list.component.html @@ -0,0 +1,14 @@ +
+ + + + +
\ No newline at end of file diff --git a/ui/src/app/components/dependency-list/dependency-list.component.module.ts b/ui/src/app/components/dependency-list/dependency-list.component.module.ts new file mode 100644 index 000000000..3b848aabf --- /dev/null +++ b/ui/src/app/components/dependency-list/dependency-list.component.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { DependencyListComponent } from './dependency-list.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' +import { InformationPopoverComponentModule } from '../information-popover/information-popover.component.module' +import { StatusComponentModule } from '../status/status.component.module' +import { InstalledDependencyItemComponentModule } from './installed-dependency-item/installed-dependency-item.component.module' +import { MarketplaceDependencyItemComponentModule } from './marketplace-dependency-item/marketplace-dependency-item.component.module' + +@NgModule({ + declarations: [ + DependencyListComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + InformationPopoverComponentModule, + StatusComponentModule, + InstalledDependencyItemComponentModule, + MarketplaceDependencyItemComponentModule, + ], + exports: [DependencyListComponent], +}) +export class DependencyListComponentModule { } diff --git a/ui/src/app/components/dependency-list/dependency-list.component.scss b/ui/src/app/components/dependency-list/dependency-list.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/components/dependency-list/dependency-list.component.ts b/ui/src/app/components/dependency-list/dependency-list.component.ts new file mode 100644 index 000000000..c05883c14 --- /dev/null +++ b/ui/src/app/components/dependency-list/dependency-list.component.ts @@ -0,0 +1,30 @@ +import { Component, Input } from '@angular/core' +import { BehaviorSubject } from 'rxjs' +import { AppDependency, BaseApp, isOptional } from 'src/app/models/app-types' + +@Component({ + selector: 'dependency-list', + templateUrl: './dependency-list.component.html', + styleUrls: ['./dependency-list.component.scss'], +}) +export class DependencyListComponent { + @Input() depType: 'installed' | 'available' = 'available' + @Input() hostApp: BaseApp + @Input() dependencies: AppDependency[] + dependenciesToDisplay: AppDependency[] + @Input() $loading$: BehaviorSubject = new BehaviorSubject(true) + + constructor () { } + + ngOnChanges () { + this.dependenciesToDisplay = this.dependencies.filter(dep => + this.depType === 'available' ? !isOptional(dep) : true, + ) + } + + ngOnInit () { + this.dependenciesToDisplay = this.dependencies.filter(dep => + this.depType === 'available' ? !isOptional(dep) : true, + ) + } +} diff --git a/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.html b/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.html new file mode 100644 index 000000000..524cfc1ef --- /dev/null +++ b/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.html @@ -0,0 +1,33 @@ + + + + +
+ +
+ +

{{ dep.title }}

+

{{ dep.versionSpec }}

+

{{statusText}}

+

Refreshing

+
+ + + {{actionText}} + + +
+
+ +
+ +
+ +
+
+
+
+ diff --git a/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.module.ts b/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.module.ts new file mode 100644 index 000000000..e362b55d0 --- /dev/null +++ b/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { InstalledDependencyItemComponent } from './installed-dependency-item.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' +import { InformationPopoverComponentModule } from '../../information-popover/information-popover.component.module' +import { StatusComponentModule } from '../../status/status.component.module' + +@NgModule({ + declarations: [InstalledDependencyItemComponent], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + InformationPopoverComponentModule, + StatusComponentModule, + ], + exports: [InstalledDependencyItemComponent], +}) +export class InstalledDependencyItemComponentModule { } diff --git a/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.scss b/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.scss new file mode 100644 index 000000000..622ac9acd --- /dev/null +++ b/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.scss @@ -0,0 +1,30 @@ + +.spinner { + background: rgba(0,0,0,0); + border-radius: 100px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 5px; +} + +.badge { + position: absolute; width: 2.5vh; + height: 2.5vh; + border-radius: 50px; + left: -1vh; + top: -1vh; +} + +.xSmallText { + font-size: x-small !important; +} + +.mediumText { + font-size: medium !important; +} + +.opacityUp { + opacity: 0.75; +} \ No newline at end of file diff --git a/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.ts b/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.ts new file mode 100644 index 000000000..ff89f482a --- /dev/null +++ b/ui/src/app/components/dependency-list/installed-dependency-item/installed-dependency-item.component.ts @@ -0,0 +1,113 @@ +import { Component, Input, OnInit } from '@angular/core' +import { NavigationExtras } from '@angular/router' +import { AlertController, NavController } from '@ionic/angular' +import { BehaviorSubject, Observable } from 'rxjs' +import { AppStatus } from 'src/app/models/app-model' +import { AppDependency, BaseApp, DependencyViolationSeverity, getInstalledViolationSeverity, getViolationSeverity, isInstalling, isMisconfigured, isMissing, isNotRunning, isVersionMismatch } from 'src/app/models/app-types' +import { Recommendation } from '../../recommendation-button/recommendation-button.component' + +@Component({ + selector: 'installed-dependency-item', + templateUrl: './installed-dependency-item.component.html', + styleUrls: ['./installed-dependency-item.component.scss'], +}) +export class InstalledDependencyItemComponent implements OnInit { + @Input() dep: AppDependency + @Input() hostApp: BaseApp + @Input() $loading$: BehaviorSubject + + isLoading$: Observable + color: string + installing = false + badgeStyle: string + violationSeverity: DependencyViolationSeverity + statusText: string + actionText: string + action: () => Promise + + constructor (private readonly navCtrl: NavController, private readonly alertCtrl: AlertController) { } + + ngOnInit () { + this.violationSeverity = getInstalledViolationSeverity(this.dep) + + const { color, statusText, installing, actionText, action } = this.getValues() + + this.color = color + this.statusText = statusText + this.installing = installing + this.actionText = actionText + this.action = action + this.badgeStyle = `background: radial-gradient(var(--ion-color-${this.color}) 40%, transparent)` + } + + isDanger () { + // installed dep violations are either REQUIRED or NONE, by getInstalledViolationSeverity above. + return [DependencyViolationSeverity.REQUIRED].includes(this.violationSeverity) + } + + getValues (): { color: string, statusText: string, installing: boolean, actionText: string, action: () => Promise } { + if (isInstalling(this.dep)) return { color: 'primary', statusText: 'Installing', installing: true, actionText: undefined, action: () => this.view() } + if (!this.isDanger()) return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View', action: () => this.view() } + + if (isMissing(this.dep)) return { color: 'warning', statusText: 'Not Installed', installing: false, actionText: 'Install', action: () => this.install() } + if (isVersionMismatch(this.dep)) return { color: 'warning', statusText: 'Incompatible Version Installed', installing: false, actionText: 'Update', action: () => this.install() } + if (isMisconfigured(this.dep)) return { color: 'warning', statusText: 'Incompatible Config', installing: false, actionText: 'Configure', action: () => this.configure() } + if (isNotRunning(this.dep)) return { color: 'warning', statusText: 'Not Running', installing: false, actionText: 'View', action: () => this.view() } + return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View', action: () => this.view() } + } + + async view () { + return this.navCtrl.navigateForward(`/services/installed/${this.dep.id}`) + } + + async install () { + const verb = 'requires' + const description = `${this.hostApp.title} ${verb} an install of ${this.dep.title} satisfying ${this.dep.versionSpec}.` + + const whyDependency = this.dep.description + + const installationRecommendation: Recommendation = { + iconURL: this.hostApp.iconURL, + appId: this.hostApp.id, + description, + title: this.hostApp.title, + versionSpec: this.dep.versionSpec, + whyDependency, + } + const navigationExtras: NavigationExtras = { + state: { installationRecommendation }, + } + + return this.navCtrl.navigateForward(`/services/marketplace/${this.dep.id}`, navigationExtras) + } + + async configure () { + if (this.dep.violation.name !== 'incompatible-config') return + const configViolationDesc = this.dep.violation.ruleViolations + + const configViolationFormatted = + `
    ${configViolationDesc.map(d => `
  • ${d}
  • `).join('\n')}
` + + const configRecommendation: Recommendation = { + iconURL: this.hostApp.iconURL, + appId: this.hostApp.id, + description: configViolationFormatted, + title: this.hostApp.title, + } + const navigationExtras: NavigationExtras = { + state: { configRecommendation }, + } + + return this.navCtrl.navigateForward(`/services/installed/${this.dep.id}/config`, navigationExtras) + } + + async presentAlertDescription() { + const description = `

${this.dep.description}<\p>` + + const alert = await this.alertCtrl.create({ + backdropDismiss: true, + message: description, + }) + await alert.present() + } +} diff --git a/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.html b/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.html new file mode 100644 index 000000000..0f25d3732 --- /dev/null +++ b/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.html @@ -0,0 +1,45 @@ + + + + +

+ + + +

{{ dep.title }} + (recommended) +

+

{{ dep.versionSpec }}

+

{{statusText}}

+

Refreshing

+
+ + + + + + + + {{actionText}} + + +
+
+ +
+ +
+ +
+
+ + +
+
+
+ + diff --git a/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.module.ts b/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.module.ts new file mode 100644 index 000000000..de8204c12 --- /dev/null +++ b/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { MarketplaceDependencyItemComponent } from './marketplace-dependency-item.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' +import { InformationPopoverComponentModule } from '../../information-popover/information-popover.component.module' +import { StatusComponentModule } from '../../status/status.component.module' + +@NgModule({ + declarations: [MarketplaceDependencyItemComponent], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + InformationPopoverComponentModule, + StatusComponentModule, + ], + exports: [MarketplaceDependencyItemComponent], +}) +export class MarketplaceDependencyItemComponentModule { } diff --git a/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.scss b/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.scss new file mode 100644 index 000000000..7021720ab --- /dev/null +++ b/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.scss @@ -0,0 +1,35 @@ + +.spinner { + background: rgba(0,0,0,0); + border-radius: 100px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 14px; +} + +.badge { + position: absolute; width: 2.5vh; + height: 2.5vh; + border-radius: 50px; + left: -1vh; + top: -1vh; +} + +.xSmallText { + font-size: x-small !important; +} + +.mediumText { + font-size: medium !important; +} + +.opacityUp { + opacity: 0.75; +} + +.dependency { + --padding-start: 20px; + --padding-end: 2px; +} \ No newline at end of file diff --git a/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.ts b/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.ts new file mode 100644 index 000000000..66171dbba --- /dev/null +++ b/ui/src/app/components/dependency-list/marketplace-dependency-item/marketplace-dependency-item.component.ts @@ -0,0 +1,88 @@ +import { Component, Input, OnInit } from '@angular/core' +import { NavigationExtras } from '@angular/router' +import { NavController } from '@ionic/angular' +import { BehaviorSubject, Observable } from 'rxjs' +import { AppDependency, BaseApp, DependencyViolationSeverity, getViolationSeverity, isOptional, isMissing, isInstalling, isRecommended, isVersionMismatch } from 'src/app/models/app-types' +import { Recommendation } from '../../recommendation-button/recommendation-button.component' + +@Component({ + selector: 'marketplace-dependency-item', + templateUrl: './marketplace-dependency-item.component.html', + styleUrls: ['./marketplace-dependency-item.component.scss'], +}) +export class MarketplaceDependencyItemComponent implements OnInit { + @Input() dep: AppDependency + @Input() hostApp: BaseApp + @Input() $loading$: BehaviorSubject + + presentAlertDescription = false + + isLoading$: Observable + color: string + installing = false + recommended = false + badgeStyle: string + violationSeverity: DependencyViolationSeverity + statusText: string + actionText: 'View' | 'Get' + + descriptionText: string + + constructor ( + private readonly navCtrl: NavController, + ) { } + + ngOnInit () { + this.violationSeverity = getViolationSeverity(this.dep) + if (isOptional(this.dep)) throw new Error('Do not display optional deps, satisfied or otherwise, on the AAL') + + const { actionText, color, statusText, installing } = this.getValues() + + this.color = color + this.statusText = statusText + this.installing = installing + this.recommended = isRecommended(this.dep) + this.actionText = actionText + this.badgeStyle = `background: radial-gradient(var(--ion-color-${this.color}) 40%, transparent)` + this.descriptionText = `

${this.dep.description}<\p>` + if (this.recommended) { + this.descriptionText = this.descriptionText + `

This service is not required: ${this.dep.optional}<\p>` + } + } + + isDanger (): boolean { + return [DependencyViolationSeverity.REQUIRED, DependencyViolationSeverity.RECOMMENDED].includes(this.violationSeverity) + } + + getValues (): { color: string, statusText: string, installing: boolean, actionText: 'View' | 'Get' } { + if (isInstalling(this.dep)) return { color: 'primary', statusText: 'Installing', installing: true, actionText: undefined } + if (!this.isDanger()) return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View' } + if (isMissing(this.dep)) return { color: 'warning', statusText: 'Not Installed', installing: false, actionText: 'Get' } + if (isVersionMismatch(this.dep)) return { color: 'warning', statusText: 'Incompatible Version Installed', installing: false, actionText: 'Get' } + return { color: 'success', statusText: 'Satisfied', installing: false, actionText: 'View' } + } + + async toInstall () { + if (this.actionText === 'View') return this.navCtrl.navigateForward(`/services/marketplace/${this.dep.id}`) + + const verb = this.violationSeverity === DependencyViolationSeverity.REQUIRED ? 'requires' : 'recommends' + const description = `${this.hostApp.title} ${verb} an install of ${this.dep.title} satisfying ${this.dep.versionSpec}.` + + const whyDependency = this.dep.description + + const installationRecommendation: Recommendation = { + iconURL: this.hostApp.iconURL, + appId: this.hostApp.id, + description, + title: this.hostApp.title, + versionSpec: this.dep.versionSpec, + whyDependency, + } + const navigationExtras: NavigationExtras = { + state: { installationRecommendation }, + } + + return this.navCtrl.navigateForward(`/services/marketplace/${this.dep.id}`, navigationExtras) + } + +} diff --git a/ui/src/app/components/error-message/error-message.component.html b/ui/src/app/components/error-message/error-message.component.html new file mode 100644 index 000000000..83112c02f --- /dev/null +++ b/ui/src/app/components/error-message/error-message.component.html @@ -0,0 +1,6 @@ + +

{{ error }}

+ + + + diff --git a/ui/src/app/components/error-message/error-message.component.module.ts b/ui/src/app/components/error-message/error-message.component.module.ts new file mode 100644 index 000000000..6f445372d --- /dev/null +++ b/ui/src/app/components/error-message/error-message.component.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { ErrorMessageComponent } from './error-message.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' + +@NgModule({ + declarations: [ + ErrorMessageComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + ], + exports: [ErrorMessageComponent], +}) +export class ErrorMessageComponentModule { } diff --git a/ui/src/app/components/error-message/error-message.component.scss b/ui/src/app/components/error-message/error-message.component.scss new file mode 100644 index 000000000..c500626a7 --- /dev/null +++ b/ui/src/app/components/error-message/error-message.component.scss @@ -0,0 +1,10 @@ +.error-message { + --background: var(--ion-color-danger); + margin: 12px; + border-radius: 3px; + font-weight: bold; +} + +.legacy-error-message { + margin: 5px; +} \ No newline at end of file diff --git a/ui/src/app/components/error-message/error-message.component.ts b/ui/src/app/components/error-message/error-message.component.ts new file mode 100644 index 000000000..313dfb3aa --- /dev/null +++ b/ui/src/app/components/error-message/error-message.component.ts @@ -0,0 +1,19 @@ +import { Component, Input, OnInit } from '@angular/core' +import { BehaviorSubject } from 'rxjs' + +@Component({ + selector: 'error-message', + templateUrl: './error-message.component.html', + styleUrls: ['./error-message.component.scss'], +}) +export class ErrorMessageComponent implements OnInit { + @Input() $error$: BehaviorSubject = new BehaviorSubject(undefined) + @Input() dismissable = true + + constructor () { } + ngOnInit () { } + + clear () { + this.$error$.next(undefined) + } +} diff --git a/ui/src/app/components/information-popover/information-popover.component.html b/ui/src/app/components/information-popover/information-popover.component.html new file mode 100644 index 000000000..66a3b6b84 --- /dev/null +++ b/ui/src/app/components/information-popover/information-popover.component.html @@ -0,0 +1,11 @@ +
+
diff --git a/ui/src/app/components/information-popover/information-popover.component.module.ts b/ui/src/app/components/information-popover/information-popover.component.module.ts new file mode 100644 index 000000000..7aaa53cfa --- /dev/null +++ b/ui/src/app/components/information-popover/information-popover.component.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { InformationPopoverComponent } from './information-popover.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' + +@NgModule({ + declarations: [ + InformationPopoverComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + ], + exports: [InformationPopoverComponent], +}) +export class InformationPopoverComponentModule { } diff --git a/ui/src/app/components/information-popover/information-popover.component.scss b/ui/src/app/components/information-popover/information-popover.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/components/information-popover/information-popover.component.ts b/ui/src/app/components/information-popover/information-popover.component.ts new file mode 100644 index 000000000..80f60f636 --- /dev/null +++ b/ui/src/app/components/information-popover/information-popover.component.ts @@ -0,0 +1,18 @@ +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core' +import { DomSanitizer, SafeHtml } from '@angular/platform-browser' + +@Component({ + selector: 'app-information-popover', + templateUrl: './information-popover.component.html', + styleUrls: ['./information-popover.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class InformationPopoverComponent implements OnInit { + @Input() title: string + @Input() information: string + unsafeInformation: SafeHtml + constructor (private sanitizer: DomSanitizer) { } + ngOnInit () { + this.unsafeInformation = this.sanitizer.bypassSecurityTrustHtml(this.information) + } +} diff --git a/ui/src/app/components/install-wizard/complete/complete.component.html b/ui/src/app/components/install-wizard/complete/complete.component.html new file mode 100644 index 000000000..226a014b3 --- /dev/null +++ b/ui/src/app/components/install-wizard/complete/complete.component.html @@ -0,0 +1,17 @@ +
+
+
+ + {{successText}} + +
+
+ {{summary}} +
+
+
+ +
+ + {{label}} +
\ No newline at end of file diff --git a/ui/src/app/components/install-wizard/complete/complete.component.module.ts b/ui/src/app/components/install-wizard/complete/complete.component.module.ts new file mode 100644 index 000000000..cd509591a --- /dev/null +++ b/ui/src/app/components/install-wizard/complete/complete.component.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { CompleteComponent } from './complete.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' + +@NgModule({ + declarations: [ + CompleteComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + ], + exports: [CompleteComponent], +}) +export class CompleteComponentModule { } diff --git a/ui/src/app/components/install-wizard/complete/complete.component.ts b/ui/src/app/components/install-wizard/complete/complete.component.ts new file mode 100644 index 000000000..def647c31 --- /dev/null +++ b/ui/src/app/components/install-wizard/complete/complete.component.ts @@ -0,0 +1,82 @@ +import { Component, Input, OnInit } from '@angular/core' +import { BehaviorSubject, from, Subject } from 'rxjs' +import { takeUntil } from 'rxjs/operators' +import { markAsLoadingDuring$ } from 'src/app/services/loader.service' +import { capitalizeFirstLetter } from 'src/app/util/misc.util' +import { Colorable, Loadable } from '../loadable' +import { WizardAction } from '../wizard-types' + +@Component({ + selector: 'complete', + templateUrl: './complete.component.html', + styleUrls: ['../install-wizard.component.scss'], +}) +export class CompleteComponent implements OnInit, Loadable, Colorable { + @Input() params: { + action: WizardAction + verb: string //loader verb: '*stopping* ...' + title: string + executeAction: () => Promise + skipCompletionDialogue?: boolean + } + + @Input() finished: (info: { error?: Error, cancelled?: true, final?: true }) => Promise + + $loading$ = new BehaviorSubject(false) + $color$ = new BehaviorSubject('medium') + $cancel$ = new Subject() + + label: string + summary: string + successText: string + + load () { + markAsLoadingDuring$(this.$loading$, from(this.params.executeAction())).pipe(takeUntil(this.$cancel$)).subscribe( + { error: e => this.finished({ error: new Error(`${this.params.action} failed: ${e.message || e}`) }), + complete: () => this.params.skipCompletionDialogue && this.finished( { final: true} ), + }, + ) + } + + constructor () { } + ngOnInit () { + switch (this.params.action) { + case 'install': + this.summary = `Installation of ${this.params.title} is now in progress. You will receive a notification when the installation has completed.` + this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...` + this.$color$.next('primary') + this.successText = 'In Progress' + break + case 'downgrade': + this.summary = `Downgrade for ${this.params.title} is now in progress. You will receive a notification when the downgrade has completed.` + this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...` + this.$color$.next('primary') + this.successText = 'In Progress' + break + case 'update': + this.summary = `Update for ${this.params.title} is now in progress. You will receive a notification when the update has completed.` + this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...` + this.$color$.next('primary') + this.successText = 'In Progress' + break + case 'uninstall': + this.summary = `${capitalizeFirstLetter(this.params.title)} has been successfully uninstalled.` + this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...` + this.$color$.next('success') + this.successText = 'Success' + break + case 'stop': + this.summary = `${capitalizeFirstLetter(this.params.title)} has been successfully stopped.` + this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...` + this.$color$.next('success') + this.successText = 'Success' + break + case 'configure': + this.summary = `New config for ${this.params.title} has been successfully saved.` + this.label = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title}...` + this.$color$.next('success') + this.successText = 'Success' + break + } + } +} diff --git a/ui/src/app/components/install-wizard/dependencies/dependencies.component.html b/ui/src/app/components/install-wizard/dependencies/dependencies.component.html new file mode 100644 index 000000000..3363ddfcf --- /dev/null +++ b/ui/src/app/components/install-wizard/dependencies/dependencies.component.html @@ -0,0 +1,31 @@ +
+
+
+ + {{label}} + +
+ +
+ {{longMessage}} +
+ +
+ + +
+ +
+ +
{{dep.title}}
+ {{dep.versionSpec}} +
+ {{dep.violation}} + +
+
+
+
diff --git a/ui/src/app/components/install-wizard/dependencies/dependencies.component.module.ts b/ui/src/app/components/install-wizard/dependencies/dependencies.component.module.ts new file mode 100644 index 000000000..d41c07e08 --- /dev/null +++ b/ui/src/app/components/install-wizard/dependencies/dependencies.component.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { DependenciesComponent } from './dependencies.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' +import { StatusComponentModule } from '../../status/status.component.module' + +@NgModule({ + declarations: [ + DependenciesComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + StatusComponentModule, + ], + exports: [DependenciesComponent], +}) +export class DependenciesComponentModule { } diff --git a/ui/src/app/components/install-wizard/dependencies/dependencies.component.ts b/ui/src/app/components/install-wizard/dependencies/dependencies.component.ts new file mode 100644 index 000000000..6341f155f --- /dev/null +++ b/ui/src/app/components/install-wizard/dependencies/dependencies.component.ts @@ -0,0 +1,127 @@ +import { Component, Input, OnInit } from '@angular/core' +import { PopoverController } from '@ionic/angular' +import { BehaviorSubject, Subject } from 'rxjs' +import { AppStatus } from 'src/app/models/app-model' +import { AppDependency, DependencyViolationSeverity, getViolationSeverity } from 'src/app/models/app-types' +import { displayEmver } from 'src/app/pipes/emver.pipe' +import { InformationPopoverComponent } from '../../information-popover/information-popover.component' +import { Colorable, Loadable } from '../loadable' +import { WizardAction } from '../wizard-types' + +@Component({ + selector: 'dependencies', + templateUrl: './dependencies.component.html', + styleUrls: ['../install-wizard.component.scss'], +}) +export class DependenciesComponent implements OnInit, Loadable, Colorable { + @Input() params: { + action: WizardAction, + title: string, + version: string, + serviceRequirements: AppDependency[] + } + + filteredServiceRequirements: AppDependency[] + + $loading$ = new BehaviorSubject(false) + $cancel$ = new Subject() + + longMessage: string + dependencyViolations: { + iconURL: string + title: string, + versionSpec: string, + violation: string, + color: string, + badgeStyle: string + }[] + label: string + $color$ = new BehaviorSubject('medium') + + constructor (private readonly popoverController: PopoverController) { } + + load () { + this.$color$.next(this.$color$.getValue()) + } + + ngOnInit () { + this.filteredServiceRequirements = this.params.serviceRequirements.filter(dep => { + return [DependencyViolationSeverity.REQUIRED, DependencyViolationSeverity.RECOMMENDED].includes(getViolationSeverity(dep)) + }) + .filter(dep => ['incompatible-version', 'missing'].includes(dep.violation.name)) + + this.dependencyViolations = this.filteredServiceRequirements + .map(dep => ({ + iconURL: dep.iconURL, + title: dep.title, + versionSpec: (dep.violation && dep.violation.name === 'incompatible-config' && 'reconfigure') || dep.versionSpec, + isInstalling: dep.violation && dep.violation.name === 'incompatible-status' && dep.violation.status === AppStatus.INSTALLING, + violation: renderViolation(dep), + color: 'medium', + badgeStyle: `background: radial-gradient(var(--ion-color-warning) 40%, transparent)`, + })) + + this.setSeverityAttributes() + } + + setSeverityAttributes () { + switch (getWorstViolationSeverity(this.filteredServiceRequirements)){ + case DependencyViolationSeverity.REQUIRED: + this.longMessage = `${this.params.title} requires the installation of other services. Don't worry, you'll be able to install these requirements later.` + this.label = 'Notice' + this.$color$.next('dark') + break + case DependencyViolationSeverity.RECOMMENDED: + this.longMessage = `${this.params.title} recommends the installation of other services. Don't worry, you'll be able to install these requirements later.` + this.label = 'Notice' + this.$color$.next('dark') + break + default: + this.longMessage = `All installation requirements for ${this.params.title} version ${displayEmver(this.params.version)} are met.` + this.$color$.next('success') + this.label = `Ready` + } + } + + async presentPopover (ev: any, information: string) { + const popover = await this.popoverController.create({ + component: InformationPopoverComponent, + event: ev, + translucent: false, + showBackdrop: true, + backdropDismiss: true, + componentProps: { + information, + }, + }) + return popover.present() + } +} + +function renderViolation1 (dep: AppDependency): string { + const severity = getViolationSeverity(dep) + switch (severity){ + case DependencyViolationSeverity.REQUIRED: return 'mandatory' + case DependencyViolationSeverity.RECOMMENDED: return 'recommended' + case DependencyViolationSeverity.OPTIONAL: return 'optional' + case DependencyViolationSeverity.NONE: return 'none' + } +} + +function renderViolation (dep: AppDependency): string { + const severity = renderViolation1(dep) + if (severity === 'none') return '' + + switch (dep.violation.name){ + case 'missing': return `${severity}` + case 'incompatible-version': return `${severity}` + case 'incompatible-config': return `` + case 'incompatible-status': return '' + default: return '' + } +} + +function getWorstViolationSeverity (rs: AppDependency[]) : DependencyViolationSeverity { + if (!rs) return DependencyViolationSeverity.NONE + return rs.map(getViolationSeverity).sort( (a, b) => b - a )[0] || DependencyViolationSeverity.NONE +} diff --git a/ui/src/app/components/install-wizard/dependents/dependents.component.html b/ui/src/app/components/install-wizard/dependents/dependents.component.html new file mode 100644 index 000000000..5d2f6e30f --- /dev/null +++ b/ui/src/app/components/install-wizard/dependents/dependents.component.html @@ -0,0 +1,42 @@ +
+
+
+
+ + WARNING + + + READY + +
+ +
+ {{longMessage}} +
+
+
+ Will Stop +
+ + + + + +
{{dep.title}}
+
+
+
+
+
+
+ + Checking for installed services which depend on {{params.title}}... +
+
\ No newline at end of file diff --git a/ui/src/app/components/install-wizard/dependents/dependents.component.module.ts b/ui/src/app/components/install-wizard/dependents/dependents.component.module.ts new file mode 100644 index 000000000..cbe95d71f --- /dev/null +++ b/ui/src/app/components/install-wizard/dependents/dependents.component.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { DependentsComponent } from './dependents.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' +import { InformationPopoverComponentModule } from '../../information-popover/information-popover.component.module' + +@NgModule({ + declarations: [ + DependentsComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + InformationPopoverComponentModule, + ], + exports: [DependentsComponent], +}) +export class DependentsComponentModule { } diff --git a/ui/src/app/components/install-wizard/dependents/dependents.component.ts b/ui/src/app/components/install-wizard/dependents/dependents.component.ts new file mode 100644 index 000000000..14fac4be8 --- /dev/null +++ b/ui/src/app/components/install-wizard/dependents/dependents.component.ts @@ -0,0 +1,59 @@ +import { Component, Input, OnInit } from '@angular/core' +import { BehaviorSubject, from, Subject } from 'rxjs' +import { takeUntil, tap } from 'rxjs/operators' +import { DependentBreakage } from 'src/app/models/app-types' +import { markAsLoadingDuring$ } from 'src/app/services/loader.service' +import { capitalizeFirstLetter } from 'src/app/util/misc.util' +import { Colorable, Loadable } from '../loadable' +import { WizardAction } from '../wizard-types' + +@Component({ + selector: 'dependents', + templateUrl: './dependents.component.html', + styleUrls: ['../install-wizard.component.scss'], +}) +export class DependentsComponent implements OnInit, Loadable, Colorable { + @Input() params: { + title: string, + action: WizardAction, //Are you sure you want to *uninstall*..., + verb: string, // *Uninstalling* will cause problems... + fetchBreakages: () => Promise, + skipConfirmationDialogue?: boolean + } + @Input() finished: (info: { error?: Error, cancelled?: true, final?: true }) => Promise + + + dependentBreakages: DependentBreakage[] + hasDependentViolation: boolean + longMessage: string | null = null + $color$ = new BehaviorSubject('medium') // this will display disabled while loading + $loading$ = new BehaviorSubject(false) + $cancel$ = new Subject() + + constructor () { } + ngOnInit () { } + + load () { + this.$color$.next('medium') + markAsLoadingDuring$(this.$loading$, from(this.params.fetchBreakages())).pipe( + takeUntil(this.$cancel$), + tap(breakages => this.dependentBreakages = breakages || []), + ).subscribe( + { + complete: () => { + this.hasDependentViolation = this.dependentBreakages && this.dependentBreakages.length > 0 + if (this.hasDependentViolation) { + this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will cause the following services to STOP running. Starting them again will require additional actions.` + this.$color$.next('warning') + } else if (this.params.skipConfirmationDialogue) { + this.finished({ }) + } else { + this.longMessage = `No other services installed on your Embassy will be affected by this action.` + this.$color$.next('success') + } + }, + error: (e: Error) => this.finished({ error: new Error(`Fetching dependent service information failed: ${e.message || e}`) }), + }, + ) + } +} diff --git a/ui/src/app/components/install-wizard/install-wizard.component.html b/ui/src/app/components/install-wizard/install-wizard.component.html new file mode 100644 index 000000000..3194bedd9 --- /dev/null +++ b/ui/src/app/components/install-wizard/install-wizard.component.html @@ -0,0 +1,52 @@ + + + +

{{ params.toolbar.title }}

+

{{params.toolbar.action}} {{ params.toolbar.version | displayEmver }}

+
+
+
+ + + + + + + + + + + +
+
+
+ + Error + +
+
+ {{error}} +
+
+
+
+ + + + + + {{t}} + + + + {{t}} + + + {{nextButton}} + {{finishButton}} + + + Dismiss + + + diff --git a/ui/src/app/components/install-wizard/install-wizard.component.module.ts b/ui/src/app/components/install-wizard/install-wizard.component.module.ts new file mode 100644 index 000000000..dea976a81 --- /dev/null +++ b/ui/src/app/components/install-wizard/install-wizard.component.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { InstallWizardComponent } from './install-wizard.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' +import { DependenciesComponentModule } from './dependencies/dependencies.component.module' +import { DependentsComponentModule } from './dependents/dependents.component.module' +import { CompleteComponentModule } from './complete/complete.component.module' + +@NgModule({ + declarations: [ + InstallWizardComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + DependenciesComponentModule, + DependentsComponentModule, + CompleteComponentModule, + ], + exports: [InstallWizardComponent], +}) +export class InstallWizardComponentModule { } diff --git a/ui/src/app/components/install-wizard/install-wizard.component.scss b/ui/src/app/components/install-wizard/install-wizard.component.scss new file mode 100644 index 000000000..03a59a661 --- /dev/null +++ b/ui/src/app/components/install-wizard/install-wizard.component.scss @@ -0,0 +1,79 @@ +.toolbar-label { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + color: white; + padding: 8px 0px 8px 15px; +} + +.toolbar-title { + font-size: x-large; + text-transform: capitalize; + border-style: solid; + border-width: 0px 0px 1px 0px; + border-color: #404040; + font-family: 'Montserrat'; +} + +.center-spinner { + min-height: 40vh; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + color:white; +} + +.slide-content { + display: flex; + flex-direction: column; + justify-content: space-between; + width: 100%; + color:white; + min-height: 40vh +} + +.status-label { + font-size: xx-large; + font-weight: bold; +} + +.long-message { + margin-left: 5%; + margin-right: 5%; + padding: 10px; + font-size: small; + border-width: 0px 0px 1px 0px; + border-color: #393b40; +} + +@media (min-width:500px) { + .long-message { + margin-left: 5%; + margin-right: 5%; + padding: 10px; + font-size: medium; + border-width: 0px 0px 1px 0px; + border-color: #393b40; + } +} + +.toolbar-button { + text-transform: capitalize; + font-weight: bolder; +} + +.smaller-text { + font-size: 14px; +} + +.badge { + position: absolute; + width: 2vh; + height: 2vh; + border-radius: 50px; + left: -0.75vh; + top: -0.75vh; +} diff --git a/ui/src/app/components/install-wizard/install-wizard.component.ts b/ui/src/app/components/install-wizard/install-wizard.component.ts new file mode 100644 index 000000000..a02ad2aac --- /dev/null +++ b/ui/src/app/components/install-wizard/install-wizard.component.ts @@ -0,0 +1,128 @@ +import { Component, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core' +import { IonContent, IonSlides, ModalController } from '@ionic/angular' +import { BehaviorSubject, combineLatest, Subscription } from 'rxjs' +import { map } from 'rxjs/operators' +import { Cleanup } from 'src/app/util/cleanup' +import { capitalizeFirstLetter } from 'src/app/util/misc.util' +import { CompleteComponent } from './complete/complete.component' +import { DependenciesComponent } from './dependencies/dependencies.component' +import { DependentsComponent } from './dependents/dependents.component' +import { Colorable, Loadable } from './loadable' +import { WizardAction } from './wizard-types' + +@Component({ + selector: 'install-wizard', + templateUrl: './install-wizard.component.html', + styleUrls: ['./install-wizard.component.scss'], +}) +export class InstallWizardComponent extends Cleanup implements OnInit { + @Input() params: { + // defines the slideshow in the html + slideDefinitions: SlideDefinition[] + toolbar: TopbarParams + } + + // containers + @ViewChild(IonContent) contentContainer: IonContent + @ViewChild(IonSlides) slideContainer: IonSlides + + //don't use this, use slideComponents instead. + @ViewChildren('components') + public slideComponentsQL: QueryList + + //don't use this, use currentSlide instead. + slideIndex = 0 + + get slideComponents (): (Loadable & Colorable)[] { + return this.slideComponentsQL.toArray() + } + + get currentSlide (): (Loadable & Colorable) { + return this.slideComponents[this.slideIndex] + } + + get currentSlideDef (): SlideDefinition { + return this.params.slideDefinitions[this.slideIndex] + } + + $anythingLoading$: BehaviorSubject = new BehaviorSubject(true) + $currentColor$: BehaviorSubject = new BehaviorSubject('medium') + $error$ = new BehaviorSubject(undefined) + + constructor (private readonly modalController: ModalController) { super() } + ngOnInit () { } + + ngAfterViewInit () { + this.currentSlide.load() + this.slideContainer.update() + this.slideContainer.lockSwipes(true) + } + + ionViewDidEnter () { + this.cleanup( + combineLatest(this.slideComponents.map(component => component.$loading$)).pipe( + map(loadings => !loadings.every(p => !p)), + ).subscribe(this.$anythingLoading$), + combineLatest(this.slideComponents.map(component => component.$color$)).pipe( + map(colors => colors[this.slideIndex]), + ).subscribe(this.$currentColor$), + ) + } + + finished = (info: { error?: Error, cancelled?: true, final?: true }) => { + if (info.cancelled) this.currentSlide.$cancel$.next() + if (info.final || info.cancelled) return this.modalController.dismiss(info) + if (info.error) return this.$error$.next(capitalizeFirstLetter(info.error.message)) + + this.slide() + } + + private async slide () { + if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.finished({ final: true }) } + this.slideIndex += 1 + await this.slideContainer.lockSwipes(false) + await Promise.all([this.contentContainer.scrollToTop(), this.slideContainer.slideNext()]) + await this.slideContainer.lockSwipes(true) + this.currentSlide.load() + } +} + +export interface SlideCommon { + selector: string + cancelButton: { + // indicates the existence of a cancel button, and whether to have text or an icon 'x' by default. + afterLoading?: { text?: string }, + whileLoading?: { text?: string } + } + nextButton?: string, + finishButton?: string +} + +export type SlideDefinition = SlideCommon & ( + { + selector: 'dependencies', + params: DependenciesComponent['params'] + } | { + selector: 'dependents', + params: DependentsComponent['params'] + } | { + selector: 'complete', + params: CompleteComponent['params'] + } +) + +export type TopbarParams = { action: WizardAction, title: string, version: string } + +export async function wizardModal ( + modalController: ModalController, params: InstallWizardComponent['params'], +): Promise<{ cancelled?: true, final?: true, modal: HTMLIonModalElement }> { + const modal = await modalController.create({ + backdropDismiss: false, + cssClass: 'wizard-modal', + component: InstallWizardComponent, + componentProps: { params }, + }) + + await modal.present() + return modal.onWillDismiss().then(({ data }) => ({ ...data, modal })) +} diff --git a/ui/src/app/components/install-wizard/loadable.ts b/ui/src/app/components/install-wizard/loadable.ts new file mode 100644 index 000000000..f2f69d6b6 --- /dev/null +++ b/ui/src/app/components/install-wizard/loadable.ts @@ -0,0 +1,11 @@ +import { BehaviorSubject, Subject } from 'rxjs' + +export interface Loadable { + load: () => void + $loading$: BehaviorSubject //will be true during load function + $cancel$: Subject //will cancel load function +} + +export interface Colorable { + $color$: BehaviorSubject +} diff --git a/ui/src/app/components/install-wizard/prebaked-wizards.ts b/ui/src/app/components/install-wizard/prebaked-wizards.ts new file mode 100644 index 000000000..548dba910 --- /dev/null +++ b/ui/src/app/components/install-wizard/prebaked-wizards.ts @@ -0,0 +1,161 @@ +import { Injectable } from '@angular/core' +import { AppModel, AppStatus } from 'src/app/models/app-model' +import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types' +import { ApiService } from '../../services/api/api.service' +import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install-wizard.component' + +@Injectable({ providedIn: 'root' }) +export class WizardBaker { + constructor (private readonly apiService: ApiService, private readonly appModel: AppModel) { } + + install (values: { + id: string, title: string, version: string, serviceRequirements: AppDependency[] + }): InstallWizardComponent['params'] { + const { id, title, version, serviceRequirements } = values + + validate(id, exists, 'missing id') + validate(title, exists, 'missing title') + validate(version, exists, 'missing version') + validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements') + + const action = 'install' + const toolbar: TopbarParams = { action, title, version } + + const slideDefinitions: SlideDefinition[] = [ + { selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Install', params: { + action, title, version, serviceRequirements, + }}, + { selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: { + action, verb: 'beginning installation for', title, executeAction: () => this.apiService.installApp(id, version).then(app => { + this.appModel.add({ ...app, status: AppStatus.INSTALLING }) + }), + }}, + ] + return { toolbar, slideDefinitions } + } + + update (values: { + id: string, title: string, version: string, serviceRequirements: AppDependency[] + }): InstallWizardComponent['params'] { + const { id, title, version, serviceRequirements } = values + validate(id, exists, 'missing id') + validate(title, exists, 'missing title') + validate(version, exists, 'missing version') + validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements') + + const action = 'update' + const toolbar: TopbarParams = { action, title, version } + + const slideDefinitions: SlideDefinition[] = [ + { selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update', params: { + action, title, version, serviceRequirements, + }}, + { selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Update Anyways', params: { + skipConfirmationDialogue: true, action, verb: 'updating', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ), + }}, + { selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: { + action, verb: 'beginning update for', title, executeAction: () => this.apiService.installApp(id, version).then(app => { + this.appModel.update({ id: app.id, status: AppStatus.INSTALLING }) + }), + }}, + ] + return { toolbar, slideDefinitions } + } + + downgrade (values: { + id: string, title: string, version: string, serviceRequirements: AppDependency[] + }): InstallWizardComponent['params'] { + const { id, title, version, serviceRequirements } = values + + validate(id, exists, 'missing id') + validate(title, exists, 'missing title') + validate(version, exists, 'missing version') + validate(serviceRequirements, t => !!t && Array.isArray(t), 'missing serviceRequirements') + + const action = 'downgrade' + const toolbar: TopbarParams = { action, title, version } + + const slideDefinitions: SlideDefinition[] = [ + { selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade', params: { + action, title, version, serviceRequirements, + }}, + { selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade Anyways', params: { + skipConfirmationDialogue: true, action, verb: 'downgrading', title, fetchBreakages: () => this.apiService.installApp(id, version, true).then( ({ breakages }) => breakages ), + }}, + { selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: { + action, verb: 'beginning downgrade for', title, executeAction: () => this.apiService.installApp(id, version).then(app => { + this.appModel.update({ id: app.id, status: AppStatus.INSTALLING }) + }), + }}, + ] + return { toolbar, slideDefinitions } + } + + uninstall (values: { + id: string, title: string, version: string + }): InstallWizardComponent['params'] { + const { id, title, version } = values + + validate(id, exists, 'missing id') + validate(title, exists, 'missing title') + validate(version, exists, 'missing version') + + const action = 'uninstall' + const toolbar: TopbarParams = { action, title, version } + + const slideDefinitions: SlideDefinition[] = [ + { selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Uninstall', params: { + action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ), + }}, + { selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: { + action, verb: 'uninstalling', title, executeAction: () => this.apiService.uninstallApp(id).then(() => this.appModel.delete(id)), + }}, + ] + return { toolbar, slideDefinitions } + } + + stop (values: { + breakages: DependentBreakage[], id: string, title: string, version: string + }): InstallWizardComponent['params'] { + const { breakages, title, version } = values + + validate(breakages, t => !!t && Array.isArray(t), 'missing breakages') + validate(title, exists, 'missing title') + validate(version, exists, 'missing version') + + const action = 'stop' + const toolbar: TopbarParams = { action, title, version } + + const slideDefinitions: SlideDefinition[] = [ + { selector: 'dependents', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Stop Anyways', params: { + action, verb: 'stopping', title, fetchBreakages: () => Promise.resolve(breakages), + }}, + ] + return { toolbar, slideDefinitions } + } + + configure (values: { + breakages: DependentBreakage[], app: AppInstalledPreview + }): InstallWizardComponent['params'] { + const { breakages, app } = values + const { title, versionInstalled: version } = app + const action = 'configure' + const toolbar: TopbarParams = { action, title, version } + + const slideDefinitions: SlideDefinition[] = [ + { selector: 'dependents', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Save Config Anyways', params: { + action, verb: 'saving config for', title, fetchBreakages: () => Promise.resolve(breakages), + }}, + ] + return { toolbar, slideDefinitions } + } +} + +function validate (t: T, test: (t: T) => Boolean, desc: string) { + if (!test(t)) { + console.error('failed validation', desc, t) + throw new Error(desc) + } +} + +const exists = t => !!t \ No newline at end of file diff --git a/ui/src/app/components/install-wizard/wizard-types.ts b/ui/src/app/components/install-wizard/wizard-types.ts new file mode 100644 index 000000000..f7e31c953 --- /dev/null +++ b/ui/src/app/components/install-wizard/wizard-types.ts @@ -0,0 +1,7 @@ +export type WizardAction = + 'install' + | 'update' + | 'downgrade' + | 'uninstall' + | 'stop' + | 'configure' \ No newline at end of file diff --git a/ui/src/app/components/object-config/object-config-item.component.html b/ui/src/app/components/object-config/object-config-item.component.html new file mode 100644 index 000000000..7ed4bd2c7 --- /dev/null +++ b/ui/src/app/components/object-config/object-config-item.component.html @@ -0,0 +1,16 @@ + + + + + + +
+ + {{ spec.name }} + (new) + +
+ + {{ displayValue }} +
+ diff --git a/ui/src/app/components/object-config/object-config.component.html b/ui/src/app/components/object-config/object-config.component.html new file mode 100644 index 000000000..ed4a46da3 --- /dev/null +++ b/ui/src/app/components/object-config/object-config.component.html @@ -0,0 +1,10 @@ +
+ +
\ No newline at end of file diff --git a/ui/src/app/components/object-config/object-config.component.module.ts b/ui/src/app/components/object-config/object-config.component.module.ts new file mode 100644 index 000000000..3938c0614 --- /dev/null +++ b/ui/src/app/components/object-config/object-config.component.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { FormsModule } from '@angular/forms' +import { ObjectConfigComponent, ObjectConfigItemComponent } from './object-config.component' +import { IonicModule } from '@ionic/angular' +import { SharingModule } from 'src/app/modules/sharing.module' + +@NgModule({ + declarations: [ + ObjectConfigComponent, + ObjectConfigItemComponent, + ], + imports: [ + CommonModule, + FormsModule, + IonicModule, + SharingModule, + ], + exports: [ + ObjectConfigComponent, + ObjectConfigItemComponent, + ], +}) +export class ObjectConfigComponentModule { } diff --git a/ui/src/app/components/object-config/object-config.component.scss b/ui/src/app/components/object-config/object-config.component.scss new file mode 100644 index 000000000..97beee453 --- /dev/null +++ b/ui/src/app/components/object-config/object-config.component.scss @@ -0,0 +1,43 @@ +.add-margin { + margin: 0 16px; +} + +.help-button { + position: relative; + bottom: 7px; + right: 9px; +} + +.new-tag { + padding: 0px 5px; + font-weight: bold; + font-size: smaller; + color: #cecece; + font-style: italic; +} + +.status-icon{ + // width: 2%; + margin-right: 12px; +} + +.bright { + color: white !important; +} + +.bold { + font-weight: bold; +} + +.invalid { + color: var(--ion-color-danger) !important; +} + +.organizer { + display: flex; + align-items: center; +} + +.name { + text-decoration: underline; +} \ No newline at end of file diff --git a/ui/src/app/components/object-config/object-config.component.ts b/ui/src/app/components/object-config/object-config.component.ts new file mode 100644 index 000000000..6f4f9f482 --- /dev/null +++ b/ui/src/app/components/object-config/object-config.component.ts @@ -0,0 +1,101 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core' +import { Annotation, Annotations } from '../../app-config/config-utilities' +import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service' +import { ConfigCursor } from 'src/app/app-config/config-cursor' +import { ModalPresentable } from 'src/app/app-config/modal-presentable' +import { ValueSpecOf, ValueSpec } from 'src/app/app-config/config-types' +import { MaskPipe } from 'src/app/pipes/mask.pipe' + +@Component({ + selector: 'object-config', + templateUrl: './object-config.component.html', + styleUrls: ['./object-config.component.scss'], +}) +export class ObjectConfigComponent extends ModalPresentable { + @Input() cursor: ConfigCursor<'object' | 'union'> + @Output() onEdit = new EventEmitter() + spec: ValueSpecOf<'object' | 'union'> + value: object + annotations: Annotations<'object' | 'union'> + + constructor ( + trackingModalCtrl: TrackingModalController, + ) { + super(trackingModalCtrl) + } + + ngOnInit () { + this.spec = this.cursor.spec() + this.value = this.cursor.config() + this.annotations = this.cursor.getAnnotations() + } + + async handleClick (key: string) { + const nextCursor = this.cursor.seekNext(key) + nextCursor.createFirstEntryForList() + + await this.presentModal(nextCursor, () => { + this.onEdit.emit(true) + this.annotations = this.cursor.getAnnotations() + }) + } + + asIsOrder () { + return 0 + } +} + +@Component({ + selector: 'object-config-item', + templateUrl: './object-config-item.component.html', + styleUrls: ['./object-config.component.scss'], +}) + +export class ObjectConfigItemComponent { + @Input() key: string + @Input() spec: ValueSpec + @Input() value: string | number + @Input() anno: Annotation + @Output() onClick = new EventEmitter() + maskPipe: MaskPipe = new MaskPipe() + + displayValue?: string | number | boolean + + ngOnChanges () { + switch (this.spec.type) { + case 'string': + if (this.value) { + if (this.spec.masked) { + this.displayValue = this.maskPipe.transform(this.value as string, 4) + } else { + this.displayValue = this.value + } + } else { + this.displayValue = '-' + } + break + case 'boolean': + this.displayValue = String(this.value) + break + case 'number': + this.displayValue = this.value || '-' + if (this.displayValue && this.spec.units) { + this.displayValue = `${this.displayValue} ${this.spec.units}` + } + break + case 'enum': + this.displayValue = this.spec.valueNames[this.value] + break + case 'pointer': + this.displayValue = 'System Defined' + break + default: + return + } + } + + async handleClick (): Promise { + if (this.spec.type === 'pointer') return + this.onClick.emit(true) + } +} \ No newline at end of file diff --git a/ui/src/app/components/pwa-back-button/pwa-back.component.html b/ui/src/app/components/pwa-back-button/pwa-back.component.html new file mode 100644 index 000000000..a68d114c6 --- /dev/null +++ b/ui/src/app/components/pwa-back-button/pwa-back.component.html @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/app/components/pwa-back-button/pwa-back.component.module.ts b/ui/src/app/components/pwa-back-button/pwa-back.component.module.ts new file mode 100644 index 000000000..94443f554 --- /dev/null +++ b/ui/src/app/components/pwa-back-button/pwa-back.component.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { PwaBackComponent } from './pwa-back.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' + +@NgModule({ + declarations: [ + PwaBackComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + ], + exports: [PwaBackComponent], +}) +export class PwaBackComponentModule { } diff --git a/ui/src/app/components/pwa-back-button/pwa-back.component.scss b/ui/src/app/components/pwa-back-button/pwa-back.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/components/pwa-back-button/pwa-back.component.ts b/ui/src/app/components/pwa-back-button/pwa-back.component.ts new file mode 100644 index 000000000..cc8bb0379 --- /dev/null +++ b/ui/src/app/components/pwa-back-button/pwa-back.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core' +import { PwaBackService } from 'src/app/services/pwa-back.service' +@Component({ + selector: 'pwa-back-button', + templateUrl: './pwa-back.component.html', + styleUrls: ['./pwa-back.component.scss'], +}) +export class PwaBackComponent { + constructor ( + private readonly pwaBack: PwaBackService, + ) { } + + navigateBack () { + return this.pwaBack.back() + } +} diff --git a/ui/src/app/components/qr/qr.component.html b/ui/src/app/components/qr/qr.component.html new file mode 100644 index 000000000..24010028f --- /dev/null +++ b/ui/src/app/components/qr/qr.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/app/components/qr/qr.component.module.ts b/ui/src/app/components/qr/qr.component.module.ts new file mode 100644 index 000000000..bc52b9c5a --- /dev/null +++ b/ui/src/app/components/qr/qr.component.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { QRComponent } from './qr.component' +import { IonicModule } from '@ionic/angular' +import { QRCodeModule } from 'angularx-qrcode' + +@NgModule({ + declarations: [ + QRComponent, + ], + imports: [ + CommonModule, + IonicModule, + QRCodeModule, + ], + exports: [QRComponent], +}) +export class QRComponentModule { } diff --git a/ui/src/app/components/qr/qr.component.scss b/ui/src/app/components/qr/qr.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/components/qr/qr.component.ts b/ui/src/app/components/qr/qr.component.ts new file mode 100644 index 000000000..6a37bedf7 --- /dev/null +++ b/ui/src/app/components/qr/qr.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core' +import { isPlatform } from '@ionic/angular' + +@Component({ + selector: 'qr', + templateUrl: './qr.component.html', + styleUrls: ['./qr.component.scss'], +}) +export class QRComponent { + @Input() text: string + width: number + + ngOnInit () { + this.width = isPlatform('ios') || isPlatform('android') ? 320 : 420 + } +} diff --git a/ui/src/app/components/recommendation-button/recommendation-button.component.html b/ui/src/app/components/recommendation-button/recommendation-button.component.html new file mode 100644 index 000000000..785c811b2 --- /dev/null +++ b/ui/src/app/components/recommendation-button/recommendation-button.component.html @@ -0,0 +1,9 @@ + + + + + diff --git a/ui/src/app/components/recommendation-button/recommendation-button.component.module.ts b/ui/src/app/components/recommendation-button/recommendation-button.component.module.ts new file mode 100644 index 000000000..4bceb29c8 --- /dev/null +++ b/ui/src/app/components/recommendation-button/recommendation-button.component.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { RecommendationButtonComponent } from './recommendation-button.component' +import { IonicModule } from '@ionic/angular' +import { RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' + +@NgModule({ + declarations: [ + RecommendationButtonComponent, + ], + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild([]), + SharingModule, + ], + exports: [RecommendationButtonComponent], +}) +export class RecommendationButtonComponentModule { } diff --git a/ui/src/app/components/recommendation-button/recommendation-button.component.scss b/ui/src/app/components/recommendation-button/recommendation-button.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/components/recommendation-button/recommendation-button.component.ts b/ui/src/app/components/recommendation-button/recommendation-button.component.ts new file mode 100644 index 000000000..32d9bf03b --- /dev/null +++ b/ui/src/app/components/recommendation-button/recommendation-button.component.ts @@ -0,0 +1,66 @@ +import { Component, Input, OnInit } from '@angular/core' +import { Router } from '@angular/router' +import { PopoverController } from '@ionic/angular' +import { filter, take } from 'rxjs/operators' +import { Cleanup } from 'src/app/util/cleanup' +import { capitalizeFirstLetter } from 'src/app/util/misc.util' +import { InformationPopoverComponent } from '../information-popover/information-popover.component' + +@Component({ + selector: 'recommendation-button', + templateUrl: './recommendation-button.component.html', + styleUrls: ['./recommendation-button.component.scss'], +}) +export class RecommendationButtonComponent extends Cleanup implements OnInit { + @Input() rec: Recommendation + @Input() raise?: { id: string } + constructor (private readonly router: Router, private readonly popoverController: PopoverController) { + super() + } + + ngOnInit () { + if (!this.raise) return + const mainContent = document.getElementsByTagName('ion-app')[0] + const recButton = document.getElementById(this.raise.id) + mainContent.appendChild(recButton) + + this.router.events.pipe(filter(e => !!(e as any).urlAfterRedirects, take(1))).subscribe((e: any) => { + recButton.remove() + }) + } + + disabled = false + + async presentPopover (ev: any) { + const popover = await this.popoverController.create({ + component: InformationPopoverComponent, + event: ev, + translucent: false, + showBackdrop: true, + backdropDismiss: true, + componentProps: { + information: ` +
+ ${capitalizeFirstLetter(this.rec.title)} Installation Recommendations +
+
+ ${this.rec.description} +
`, + }, + }) + popover.onWillDismiss().then(() => { + this.disabled = false + }) + this.disabled = true + return await popover.present() + } +} + +export type Recommendation = { + title: string + appId: string + iconURL: string, + description: string, + versionSpec?: string + whyDependency?: string +} diff --git a/ui/src/app/components/status/status.component.html b/ui/src/app/components/status/status.component.html new file mode 100644 index 000000000..9509a9ae5 --- /dev/null +++ b/ui/src/app/components/status/status.component.html @@ -0,0 +1,24 @@ +

+ {{ display }} + +

+ +

+ {{ display }} + +

+ +

+ {{ display }} + +

+ +

+ {{ display }} + +

+ +

+ {{ display }} + +

diff --git a/ui/src/app/components/status/status.component.module.ts b/ui/src/app/components/status/status.component.module.ts new file mode 100644 index 000000000..73d9bab53 --- /dev/null +++ b/ui/src/app/components/status/status.component.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { StatusComponent } from './status.component' +import { IonicModule } from '@ionic/angular' + +@NgModule({ + declarations: [ + StatusComponent, + ], + imports: [ + CommonModule, + IonicModule, + ], + exports: [StatusComponent], +}) +export class StatusComponentModule { } diff --git a/ui/src/app/components/status/status.component.scss b/ui/src/app/components/status/status.component.scss new file mode 100644 index 000000000..9c61e745d --- /dev/null +++ b/ui/src/app/components/status/status.component.scss @@ -0,0 +1,32 @@ +.icon-small { + width: auto; + height: 14px; + padding-left: 6px; +} + +.icon-medium { + width: auto; + height: 18px; + padding-left: 8px; +} + +.icon-large { + width: auto; + height: 24px; + padding-left: 12px; +} + +.dots { + vertical-align: middle; + margin-left: 8px; +} + +.dots-small { + width: 12px !important; + height: 12px !important; +} + +.dots-medium { + width: 16px !important; + height: 16px !important; +} \ No newline at end of file diff --git a/ui/src/app/components/status/status.component.ts b/ui/src/app/components/status/status.component.ts new file mode 100644 index 000000000..d6cb837b6 --- /dev/null +++ b/ui/src/app/components/status/status.component.ts @@ -0,0 +1,56 @@ +import { Component, Input } from '@angular/core' +import { AppStatus } from 'src/app/models/app-model' +import { ServerStatus } from 'src/app/models/server-model' +import { ServerStatusRendering, AppStatusRendering } from '../../util/status-rendering' + +@Component({ + selector: 'status', + templateUrl: './status.component.html', + styleUrls: ['./status.component.scss'], +}) +export class StatusComponent { + @Input() appStatus?: AppStatus + @Input() serverStatus?: ServerStatus + @Input() size: 'small' | 'medium' | 'large' | 'italics-small' | 'bold-large' = 'large' + @Input() text: string = '' + color: string + display: string + showDots: boolean + style = '' + + ngOnChanges () { + if (this.serverStatus) { + this.handleServerStatus() + } else if (this.appStatus) { + this.handleAppStatus() + } + } + + handleServerStatus () { + let res = ServerStatusRendering[this.serverStatus] + if (!res) { + console.warn(`Received invalid server status from the server: `, this.serverStatus) + res = ServerStatusRendering[ServerStatus.UNKNOWN] + } + + const { display, color, showDots } = res + this.display = display + this.color = color + this.showDots = showDots + } + + handleAppStatus () { + let res = AppStatusRendering[this.appStatus] + if (!res) { + console.warn(`Received invalid app status from the server: `, this.appStatus) + res = AppStatusRendering[AppStatus.UNKNOWN] + } + + const { display, color, showDots, style } = res + this.display = display + this.text + this.color = color + this.showDots = showDots + this.style = style + } +} + diff --git a/ui/src/app/guards/auth.guard.ts b/ui/src/app/guards/auth.guard.ts new file mode 100644 index 000000000..98fd1a6b5 --- /dev/null +++ b/ui/src/app/guards/auth.guard.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core' +import { CanActivate, Router, CanActivateChild } from '@angular/router' +import { AuthState, AuthService } from '../services/auth.service' + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate, CanActivateChild { + constructor ( + private readonly authService: AuthService, + private readonly router: Router, + ) { } + + canActivate (): boolean { + return this.runCheck() + } + + canActivateChild (): boolean { + return this.runCheck() + } + + private runCheck (): boolean { + const state = this.authService.peek() + + switch (state){ + case AuthState.VERIFIED: return true + case AuthState.UNVERIFIED: return this.toAuthenticate() + case AuthState.INITIALIZING: return this.toAuthenticate() + } + } + + private toAuthenticate () { + this.router.navigate(['/authenticate'], { replaceUrl: true }) + return false + } +} diff --git a/ui/src/app/guards/deactivate.guard.ts b/ui/src/app/guards/deactivate.guard.ts new file mode 100644 index 000000000..43beacecc --- /dev/null +++ b/ui/src/app/guards/deactivate.guard.ts @@ -0,0 +1,26 @@ +import { Injectable, Directive } from '@angular/core' +import { CanDeactivate } from '@angular/router' +import { HostListener } from '@angular/core' + +@Directive() +export abstract class PageCanDeactivate { + abstract canDeactivate (): boolean + + @HostListener('window:beforeunload', ['$event']) + unloadNotification (e: any) { + console.log(e) + if (!this.canDeactivate()) { + e.returnValue = true + } + } +} + +@Injectable({ + providedIn: 'root', +}) +export class CanDeactivateGuard implements CanDeactivate { + + canDeactivate (page: PageCanDeactivate): boolean { + return page.canDeactivate() || confirm('You have unsaved changes. Are you sure you want to leave the page?') + } +} \ No newline at end of file diff --git a/ui/src/app/guards/unauth.guard.ts b/ui/src/app/guards/unauth.guard.ts new file mode 100644 index 000000000..e3312cbbb --- /dev/null +++ b/ui/src/app/guards/unauth.guard.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core' +import { CanActivate, Router } from '@angular/router' +import { AuthService, AuthState } from '../services/auth.service' + +@Injectable({ + providedIn: 'root', +}) +export class UnauthGuard implements CanActivate { + constructor ( + private readonly authService: AuthService, + private readonly router: Router, + ) { } + + canActivate (): boolean { + const state = this.authService.peek() + switch (state){ + case AuthState.VERIFIED: { + this.router.navigateByUrl('') + return false + } + case AuthState.UNVERIFIED: return true + case AuthState.INITIALIZING: return true + } + } +} + diff --git a/ui/src/app/modals/app-backup/app-backup.module.ts b/ui/src/app/modals/app-backup/app-backup.module.ts new file mode 100644 index 000000000..079f043c1 --- /dev/null +++ b/ui/src/app/modals/app-backup/app-backup.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { AppBackupPage } from './app-backup.page' + +@NgModule({ + declarations: [AppBackupPage], + imports: [ + CommonModule, + IonicModule, + ], + entryComponents: [AppBackupPage], + exports: [AppBackupPage], +}) +export class AppBackupPageModule { } \ No newline at end of file diff --git a/ui/src/app/modals/app-backup/app-backup.page.html b/ui/src/app/modals/app-backup/app-backup.page.html new file mode 100644 index 000000000..a1d8be267 --- /dev/null +++ b/ui/src/app/modals/app-backup/app-backup.page.html @@ -0,0 +1,50 @@ + + + + + + + + {{ type === 'create' ? 'Create Backup' : 'Restore Backup' }} + + + + + + + + + + + + + + + + {{ error }} + + + + + + + No partitions available. To begin a backup, insert a storage device into your Embassy. + No partitions available. Insert the storage device containing the backup you wish to restore. + + + {{ d.logicalname }} ({{ d.size }}) + + + + +

{{ p.label || p.logicalname }}

+

{{ p.size || 'unknown size' }}

+

Available

+

Unvailable

+
+
+
+
+
+ +
diff --git a/ui/src/app/modals/app-backup/app-backup.page.scss b/ui/src/app/modals/app-backup/app-backup.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/modals/app-backup/app-backup.page.ts b/ui/src/app/modals/app-backup/app-backup.page.ts new file mode 100644 index 000000000..fbcf6ae57 --- /dev/null +++ b/ui/src/app/modals/app-backup/app-backup.page.ts @@ -0,0 +1,206 @@ +import { Component, Input } from '@angular/core' +import { ModalController, AlertController, LoadingController, IonicSafeString } from '@ionic/angular' +import { AppModel, AppStatus } from 'src/app/models/app-model' +import { AppInstalledFull } from 'src/app/models/app-types' +import { ApiService } from 'src/app/services/api/api.service' +import { DiskInfo, DiskPartition } from 'src/app/models/server-model' +import { pauseFor } from 'src/app/util/misc.util' + +@Component({ + selector: 'app-backup', + templateUrl: './app-backup.page.html', + styleUrls: ['./app-backup.page.scss'], +}) +export class AppBackupPage { + @Input() app: AppInstalledFull + @Input() type: 'create' | 'restore' + disks: DiskInfo[] + loading = true + error: string + allPartitionsMounted: boolean + + constructor ( + private readonly modalCtrl: ModalController, + private readonly alertCtrl: AlertController, + private readonly loadingCtrl: LoadingController, + private readonly apiService: ApiService, + private readonly appModel: AppModel, + ) { } + + ngOnInit () { + return this.getExternalDisks().then(() => this.loading = false) + } + + async getExternalDisks (): Promise { + try { + this.disks = await this.apiService.getExternalDisks() + this.allPartitionsMounted = this.disks.every(d => d.partitions.every(p => p.isMounted)) + } catch (e) { + console.error(e) + this.error = e.message + } + } + + async doRefresh (event: any) { + await Promise.all([ + this.getExternalDisks(), + pauseFor(600), + ]) + event.target.complete() + } + + async dismiss () { + await this.modalCtrl.dismiss() + } + + async presentAlertHelp (): Promise { + let alert: HTMLIonAlertElement + if (this.type === 'create') { + alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: `Backups`, + message: `Select a location to back up ${this.app.title}.

Internal drives and drives currently backing up other services will not be available.

Depending on the amount of data in ${this.app.title}, your first backup may take a while. Since backups are diff-based, the speed of future backups to the same disk will likely be much faster.`, + buttons: ['Dismiss'], + }) + } else if (this.type === 'restore') { + alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: `Backups`, + message: `Select a location containing the backup you wish to restore for ${this.app.title}.

Restoring ${this.app.title} will re-sync your service with your previous backup. The speed of the restore process depends on the backup size.`, + buttons: ['Dismiss'], + }) + } + await alert.present() + } + + async presentAlert (partition: DiskPartition): Promise { + if (this.type === 'create') { + this.presentAlertCreateEncrypted(partition) + } else { + this.presentAlertWarn(partition) + } + } + + private async presentAlertCreateEncrypted (partition: DiskPartition): Promise { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: `Encrypt Backup`, + message: `Enter your master password to create an encrypted backup of ${this.app.title} to "${partition.label || partition.logicalname}".`, + inputs: [ + { + name: 'password', + label: 'Password', + type: 'password', + placeholder: 'Master Password', + }, + ], + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Create Backup', + handler: (data) => { + if (!data.password || data.password.length < 12) { + alert.message = new IonicSafeString(alert.message + '

Password must be at least 12 characters in length.') + return false + } else { + this.create(partition, data.password) + } + }, + }, + ], + }) + await alert.present() + } + + private async presentAlertWarn (partition: DiskPartition): Promise { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: `Warning`, + message: `Restoring ${this.app.title} from "${partition.label || partition.logicalname}" will overwrite its current data.

Are you sure you want to continue?`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, { + text: 'Continue', + handler: () => { + this.presentAlertRestore(partition) + }, + }, + ], + }) + await alert.present() + } + + private async presentAlertRestore (partition: DiskPartition): Promise { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: `Decrypt Backup`, + message: `Enter your master password`, + inputs: [ + { + name: 'password', + type: 'password', + placeholder: 'Password', + }, + ], + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, { + text: 'Restore', + handler: (data) => { + this.restore(partition, data.password) + }, + }, + ], + }) + await alert.present() + } + + private async restore (partition: DiskPartition, password?: string): Promise { + this.error = '' + + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + cssClass: 'loader-ontop-of-all', + }) + await loader.present() + + try { + await this.apiService.restoreAppBackup(this.app.id, partition.logicalname, password) + this.appModel.update({ id: this.app.id, status: AppStatus.RESTORING_BACKUP }) + await this.dismiss() + } catch (e) { + console.error(e) + this.error = e.message + } finally { + await loader.dismiss() + } + } + + private async create (partition: DiskPartition, password?: string): Promise { + this.error = '' + + const loader = await this.loadingCtrl.create({ + spinner: 'lines', + cssClass: 'loader-ontop-of-all', + }) + await loader.present() + + try { + await this.apiService.createAppBackup(this.app.id, partition.logicalname, password) + this.appModel.update({ id: this.app.id, status: AppStatus.CREATING_BACKUP }) + await this.dismiss() + } catch (e) { + console.error(e) + this.error = e.message + } finally { + await loader.dismiss() + } + } +} diff --git a/ui/src/app/modals/app-config-injectable/modal-injectable-token.ts b/ui/src/app/modals/app-config-injectable/modal-injectable-token.ts new file mode 100644 index 000000000..158df9672 --- /dev/null +++ b/ui/src/app/modals/app-config-injectable/modal-injectable-token.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core' + +export const APP_CONFIG_COMPONENT_MAPPING = new InjectionToken('APP_CONFIG_COMPONENTS') \ No newline at end of file diff --git a/ui/src/app/modals/app-config-injectable/modal-injectable-type.ts b/ui/src/app/modals/app-config-injectable/modal-injectable-type.ts new file mode 100644 index 000000000..03c08bcca --- /dev/null +++ b/ui/src/app/modals/app-config-injectable/modal-injectable-type.ts @@ -0,0 +1,4 @@ +import { Type } from '@angular/core' +import { ValueType } from 'src/app/app-config/config-types' + +export type AppConfigComponentMapping = { [k in ValueType]: Type } diff --git a/ui/src/app/modals/app-config-injectable/modal-injectable-value.ts b/ui/src/app/modals/app-config-injectable/modal-injectable-value.ts new file mode 100644 index 000000000..727cd8f8f --- /dev/null +++ b/ui/src/app/modals/app-config-injectable/modal-injectable-value.ts @@ -0,0 +1,16 @@ +import { AppConfigObjectPage } from '../app-config-object/app-config-object.page' +import { AppConfigListPage } from '../app-config-list/app-config-list.page' +import { AppConfigUnionPage } from '../app-config-union/app-config-union.page' +import { AppConfigValuePage } from '../app-config-value/app-config-value.page' +import { AppConfigComponentMapping } from './modal-injectable-type' + +export const appConfigComponents: AppConfigComponentMapping = { + 'string': AppConfigValuePage, + 'number': AppConfigValuePage, + 'enum': AppConfigValuePage, + 'boolean': AppConfigValuePage, + 'list': AppConfigListPage, + 'object': AppConfigObjectPage, + 'union': AppConfigUnionPage, + 'pointer': undefined, +} diff --git a/ui/src/app/modals/app-config-list/app-config-list.module.ts b/ui/src/app/modals/app-config-list/app-config-list.module.ts new file mode 100644 index 000000000..a8ceacaec --- /dev/null +++ b/ui/src/app/modals/app-config-list/app-config-list.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { AppConfigListPage } from './app-config-list.page' +import { SharingModule } from 'src/app/modules/sharing.module' +import { ConfigHeaderComponentModule } from 'src/app/components/config-header/config-header.component.module' +import { FormsModule } from '@angular/forms' + +@NgModule({ + declarations: [AppConfigListPage], + imports: [ + CommonModule, + IonicModule, + SharingModule, + FormsModule, + ConfigHeaderComponentModule, + ], + entryComponents: [AppConfigListPage], + exports: [AppConfigListPage], +}) +export class AppConfigListPageModule { } \ No newline at end of file diff --git a/ui/src/app/modals/app-config-list/app-config-list.page.html b/ui/src/app/modals/app-config-list/app-config-list.page.html new file mode 100644 index 000000000..02c1d67b8 --- /dev/null +++ b/ui/src/app/modals/app-config-list/app-config-list.page.html @@ -0,0 +1,68 @@ + + + + + + + + {{ spec.name }} + + + + + + + + + + + + + + + + + {{ value.length }} selected +  (min: {{ min }}) +  (max: {{ max }}) + {{ selectAll ? 'All' : 'None' }} + + + {{ option.value }} + + + + + +
+ + + + {{ value.length }}  + Entry + Entries +  (min: {{ min }}) +  (max: {{ max }}) + + +
+ + + + + + + + {{ valueString[i] }} + + + + + + + +
+
+
+ +
diff --git a/ui/src/app/modals/app-config-list/app-config-list.page.scss b/ui/src/app/modals/app-config-list/app-config-list.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/modals/app-config-list/app-config-list.page.ts b/ui/src/app/modals/app-config-list/app-config-list.page.ts new file mode 100644 index 000000000..7074e6ca1 --- /dev/null +++ b/ui/src/app/modals/app-config-list/app-config-list.page.ts @@ -0,0 +1,146 @@ +import { Component, Input } from '@angular/core' +import { AlertController } from '@ionic/angular' +import { Annotations, Range } from '../../app-config/config-utilities' +import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service' +import { ConfigCursor } from 'src/app/app-config/config-cursor' +import { ValueSpecList, isValueSpecListOf } from 'src/app/app-config/config-types' +import { ModalPresentable } from 'src/app/app-config/modal-presentable' + +@Component({ + selector: 'app-config-list', + templateUrl: './app-config-list.page.html', + styleUrls: ['./app-config-list.page.scss'], +}) +export class AppConfigListPage extends ModalPresentable { + @Input() cursor: ConfigCursor<'list'> + + spec: ValueSpecList + value: string[] | number[] | object[] + valueString: string[] + annotations: Annotations<'list'> + + // enum only + options: { value: string, checked: boolean }[] = [] + selectAll = true + // + + min: number | undefined + max: number | undefined + + minMessage: string + maxMessage: string + + error: string + + constructor ( + private readonly alertCtrl: AlertController, + trackingModalCtrl: TrackingModalController, + ) { + super(trackingModalCtrl) + } + + ngOnInit () { + this.spec = this.cursor.spec() + this.value = this.cursor.config() + const range = Range.from(this.spec.range) + this.min = range.integralMin() + this.max = range.integralMax() + this.minMessage = `The minimum number of ${this.cursor.key()} is ${this.min}.` + this.maxMessage = `The maximum number of ${this.cursor.key()} is ${this.max}.` + // enum list only + if (isValueSpecListOf(this.spec, 'enum')) { + for (let val of this.spec.spec.values) { + this.options.push({ + value: val, + checked: (this.value as string[]).includes(val), + }) + } + } + this.updateCaches() + } + + async dismiss () { + return this.dismissModal(this.value) + } + + // enum only + toggleSelectAll () { + if (!isValueSpecListOf(this.spec, 'enum')) { throw new Error('unreachable') } + + this.value.length = 0 + if (this.selectAll) { + for (let v of this.spec.spec.values) { + (this.value as string[]).push(v) + } + for (let option of this.options) { + option.checked = true + } + } else { + for (let option of this.options) { + option.checked = false + } + } + this.updateCaches() + } + + // enum only + async toggleSelected (value: string) { + const index = (this.value as string[]).indexOf(value) + + // if present, delete + if (index > -1) { + (this.value as string[]).splice(index, 1) + // if not present, add + } else { + (this.value as string[]).push(value) + } + + this.updateCaches() + } + + async presentModalValueEdit (index?: number) { + const nextCursor = this.cursor.seekNext(index === undefined ? this.value.length : index) + nextCursor.createFirstEntryForList() + return this.presentModal(nextCursor, () => this.updateCaches()) + } + + async presentAlertDeleteEntry (key: number) { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Caution', + message: `Are you sure you want to delete this entry?`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Delete', + cssClass: 'alert-danger', + handler: () => { + if (typeof key === 'number') { + (this.value as any[]).splice(key, 1) + } else { + delete this.value[key] + } + this.updateCaches() + }, + }, + ], + }) + await alert.present() + } + + asIsOrder () { + return 0 + } + + private updateCaches () { + if (isValueSpecListOf(this.spec, 'enum')) { + this.selectAll = this.value.length !== this.spec.spec.values.length + } + this.error = this.cursor.checkInvalid() + this.annotations = this.cursor.getAnnotations() + this.valueString = (this.value as any[]).map((_, idx) => this.cursor.seekNext(idx).toString()) + } +} diff --git a/ui/src/app/modals/app-config-object/app-config-object.module.ts b/ui/src/app/modals/app-config-object/app-config-object.module.ts new file mode 100644 index 000000000..2cc678819 --- /dev/null +++ b/ui/src/app/modals/app-config-object/app-config-object.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { AppConfigObjectPage } from './app-config-object.page' +import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module' +import { ConfigHeaderComponentModule } from 'src/app/components/config-header/config-header.component.module' + +@NgModule({ + declarations: [AppConfigObjectPage], + imports: [ + CommonModule, + IonicModule, + ObjectConfigComponentModule, + ConfigHeaderComponentModule, + ], + entryComponents: [AppConfigObjectPage], + exports: [AppConfigObjectPage], +}) +export class AppConfigObjectPageModule { } \ No newline at end of file diff --git a/ui/src/app/modals/app-config-object/app-config-object.page.html b/ui/src/app/modals/app-config-object/app-config-object.page.html new file mode 100644 index 000000000..c1c8e18f5 --- /dev/null +++ b/ui/src/app/modals/app-config-object/app-config-object.page.html @@ -0,0 +1,27 @@ + + + + + + + + {{ spec.name }} + + + Delete + + + + + + + + + + + + + + + + diff --git a/ui/src/app/modals/app-config-object/app-config-object.page.scss b/ui/src/app/modals/app-config-object/app-config-object.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/modals/app-config-object/app-config-object.page.ts b/ui/src/app/modals/app-config-object/app-config-object.page.ts new file mode 100644 index 000000000..23909fb14 --- /dev/null +++ b/ui/src/app/modals/app-config-object/app-config-object.page.ts @@ -0,0 +1,57 @@ +import { Component, Input } from '@angular/core' +import { ModalController, AlertController } from '@ionic/angular' +import { ConfigCursor } from 'src/app/app-config/config-cursor' +import { ValueSpecObject } from 'src/app/app-config/config-types' + +@Component({ + selector: 'app-config-object', + templateUrl: './app-config-object.page.html', + styleUrls: ['./app-config-object.page.scss'], +}) +export class AppConfigObjectPage { + @Input() cursor: ConfigCursor<'object'> + spec: ValueSpecObject + value: object + error: string + + constructor ( + private readonly modalCtrl: ModalController, + private readonly alertCtrl: AlertController, + ) { } + + ngOnInit () { + this.spec = this.cursor.spec() + this.value = this.cursor.config() + this.error = this.cursor.checkInvalid() + } + + async dismiss (nullify = false) { + this.modalCtrl.dismiss(nullify ? null : this.value) + } + + async presentAlertDestroy () { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Caution', + message: 'Are you sure you want to delete this record?', + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Delete', + cssClass: 'alert-danger', + handler: () => { + this.dismiss(true) + }, + }, + ], + }) + await alert.present() + } + + handleObjectEdit () { + this.error = this.cursor.checkInvalid() + } +} diff --git a/ui/src/app/modals/app-config-union/app-config-union.module.ts b/ui/src/app/modals/app-config-union/app-config-union.module.ts new file mode 100644 index 000000000..14880d693 --- /dev/null +++ b/ui/src/app/modals/app-config-union/app-config-union.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { AppConfigUnionPage } from './app-config-union.page' +import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module' +import { FormsModule } from '@angular/forms' +import { ConfigHeaderComponentModule } from 'src/app/components/config-header/config-header.component.module' + + +@NgModule({ + declarations: [AppConfigUnionPage], + imports: [ + CommonModule, + IonicModule, + FormsModule, + ObjectConfigComponentModule, + ConfigHeaderComponentModule, + ], + entryComponents: [AppConfigUnionPage], + exports: [AppConfigUnionPage], +}) +export class AppConfigUnionPageModule { } \ No newline at end of file diff --git a/ui/src/app/modals/app-config-union/app-config-union.page.html b/ui/src/app/modals/app-config-union/app-config-union.page.html new file mode 100644 index 000000000..ba1de39f8 --- /dev/null +++ b/ui/src/app/modals/app-config-union/app-config-union.page.html @@ -0,0 +1,31 @@ + + + + + + + + {{ spec.name }} + + + + + + + + + + + + {{ spec.tag.name }} + + + {{ spec.tag.variantNames[option.key] }} + (default) + + + + + + + diff --git a/ui/src/app/modals/app-config-union/app-config-union.page.scss b/ui/src/app/modals/app-config-union/app-config-union.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/modals/app-config-union/app-config-union.page.ts b/ui/src/app/modals/app-config-union/app-config-union.page.ts new file mode 100644 index 000000000..2331ecaa2 --- /dev/null +++ b/ui/src/app/modals/app-config-union/app-config-union.page.ts @@ -0,0 +1,58 @@ +import { Component, Input, ViewChild } from '@angular/core' +import { ModalController } from '@ionic/angular' +import { ConfigCursor } from 'src/app/app-config/config-cursor' +import { ValueSpecUnion } from 'src/app/app-config/config-types' +import { ObjectConfigComponent } from 'src/app/components/object-config/object-config.component' +import { mapUnionSpec } from '../../app-config/config-utilities' + +@Component({ + selector: 'app-config-union', + templateUrl: './app-config-union.page.html', + styleUrls: ['./app-config-union.page.scss'], +}) +export class AppConfigUnionPage { + @Input() cursor: ConfigCursor<'union'> + + @ViewChild(ObjectConfigComponent) + objectConfig: ObjectConfigComponent + + spec: ValueSpecUnion + value: object + error: string + + constructor ( + private readonly modalCtrl: ModalController, + ) { } + + ngOnInit () { + this.spec = this.cursor.spec() + this.value = this.cursor.config() + this.error = this.cursor.checkInvalid() + } + + async dismiss () { + this.modalCtrl.dismiss(this.value) + } + + async handleUnionChange () { + this.value = mapUnionSpec(this.spec, this.value) + this.objectConfig.annotations = this.objectConfig.cursor.getAnnotations() + } + + setSelectOptions () { + return { + header: this.spec.tag.name, + subHeader: this.spec.changeWarning ? 'Warning!' : undefined, + message: this.spec.changeWarning ? `${this.spec.changeWarning}` : undefined, + cssClass: 'select-change-warning', + } + } + + handleObjectEdit () { + this.error = this.cursor.checkInvalid() + } + + asIsOrder () { + return 0 + } +} diff --git a/ui/src/app/modals/app-config-value/app-config-value.module.ts b/ui/src/app/modals/app-config-value/app-config-value.module.ts new file mode 100644 index 000000000..5d08de325 --- /dev/null +++ b/ui/src/app/modals/app-config-value/app-config-value.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { FormsModule } from '@angular/forms' +import { IonicModule } from '@ionic/angular' +import { AppConfigValuePage } from './app-config-value.page' +import { ConfigHeaderComponentModule } from 'src/app/components/config-header/config-header.component.module' + +@NgModule({ + declarations: [AppConfigValuePage], + imports: [ + CommonModule, + FormsModule, + IonicModule, + ConfigHeaderComponentModule, + ], + entryComponents: [AppConfigValuePage], + exports: [AppConfigValuePage], +}) +export class AppConfigValuePageModule { } diff --git a/ui/src/app/modals/app-config-value/app-config-value.page.html b/ui/src/app/modals/app-config-value/app-config-value.page.html new file mode 100644 index 000000000..b63517006 --- /dev/null +++ b/ui/src/app/modals/app-config-value/app-config-value.page.html @@ -0,0 +1,83 @@ + + + + + + + + + {{ spec.name }} + + + + {{ saveFn ? 'Save' : 'Done' }} + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+
+ + + + {{ spec.units }} + + + + + + + {{ spec.name }} + + + + + + + {{ spec.valueNames[option] }} + + + + + +
+

+ {{ spec.patternDescription }} +

+

+ {{ integralDescription }} +

+

+ {{ rangeDescription }} +

+

+ +

Default: {{ defaultDescription }}

+

Units: {{ spec.units }}

+ +

+
+
+ +
\ No newline at end of file diff --git a/ui/src/app/modals/app-config-value/app-config-value.page.scss b/ui/src/app/modals/app-config-value/app-config-value.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/modals/app-config-value/app-config-value.page.ts b/ui/src/app/modals/app-config-value/app-config-value.page.ts new file mode 100644 index 000000000..b6eacd782 --- /dev/null +++ b/ui/src/app/modals/app-config-value/app-config-value.page.ts @@ -0,0 +1,173 @@ +import { Component, Input } from '@angular/core' +import { getDefaultConfigValue, getDefaultDescription, Range } from 'src/app/app-config/config-utilities' +import { AlertController, ToastController } from '@ionic/angular' +import { LoaderService } from 'src/app/services/loader.service' +import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service' +import { ConfigCursor } from 'src/app/app-config/config-cursor' +import { ValueSpecOf } from 'src/app/app-config/config-types' +import { copyToClipboard } from 'src/app/util/web.util' + +@Component({ + selector: 'app-config-value', + templateUrl: 'app-config-value.page.html', + styleUrls: ['app-config-value.page.scss'], +}) +export class AppConfigValuePage { + @Input() cursor: ConfigCursor<'string' | 'number' | 'boolean' | 'enum'> + @Input() saveFn?: (value: string | number | boolean) => Promise + + spec: ValueSpecOf<'string' | 'number' | 'boolean' | 'enum'> + value: string | number | boolean | null + + edited: boolean + error: string + unmasked = false + + defaultDescription: string + integralDescription = 'Value must be a whole number.' + + range: Range + rangeDescription: string + + constructor ( + private readonly loader: LoaderService, + private readonly trackingModalCtrl: TrackingModalController, + private readonly alertCtrl: AlertController, + private readonly toastCtrl: ToastController, + ) { } + + ngOnInit () { + this.spec = this.cursor.spec() + this.value = this.cursor.config() + this.error = this.cursor.checkInvalid() + + this.defaultDescription = getDefaultDescription(this.spec) + if (this.spec.type === 'number') { + this.range = Range.from(this.spec.range) + this.rangeDescription = this.range.description() + } + } + + async dismiss () { + if (this.edited) { + await this.presentAlertUnsaved() + } else { + await this.trackingModalCtrl.dismiss() + } + } + + async done () { + if (!this.validate()) { return } + + if (this.spec.type !== 'boolean') { + this.value = this.value || null + } + if (this.spec.type === 'number' && this.value) { + this.value = Number(this.value) + } + + if (this.saveFn) { + this.loader.displayDuringP( + this.saveFn(this.value).catch(e => { + console.error(e) + this.error = e.message + }), + ) + } + + await this.trackingModalCtrl.dismiss(this.value) + } + + refreshDefault () { + this.value = getDefaultConfigValue(this.spec) as any + this.handleInput() + } + + handleInput () { + this.error = '' + this.edited = true + } + + clear () { + this.value = null + this.edited = true + } + + toggleMask () { + this.unmasked = !this.unmasked + } + + async copy (): Promise { + let message = '' + await copyToClipboard(String(this.value)).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'}) + + const toast = await this.toastCtrl.create({ + header: message, + position: 'bottom', + duration: 1000, + cssClass: 'notification-toast', + }) + await toast.present() + } + + private validate (): boolean { + if (this.spec.type === 'boolean') return true + + // test blank + if (!this.value && !(this.spec as any).nullable) { + this.error = 'Value cannot be blank' + return false + } + // test pattern if string + if (this.spec.type === 'string' && this.value) { + const { pattern, patternDescription } = this.spec + if (pattern && !RegExp(pattern).test(this.value as string)) { + this.error = patternDescription || `Must match ${pattern}` + return false + } + } + // test range if number + if (this.spec.type === 'number' && this.value) { + if (this.spec.integral && !RegExp(/^[-+]?[0-9]+$/).test(String(this.value))) { + this.error = this.integralDescription + return false + } else if (!this.spec.integral && !RegExp(/^[0-9]*\.?[0-9]+$/).test(String(this.value))) { + this.error = 'Value must be a number.' + return false + } else { + try { + this.range.checkIncludes(Number(this.value)) + } catch (e) { + console.warn(e) //an invalid spec is not an error + this.error = e.message + return false + } + } + } + + return true + } + + private async presentAlertUnsaved () { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Unsaved Changes', + message: 'You have unsaved changes. Are you sure you want to leave?', + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: `Leave`, + cssClass: 'alert-danger', + handler: () => { + this.trackingModalCtrl.dismiss() + }, + }, + ], + }) + await alert.present() + } +} + diff --git a/ui/src/app/modals/app-release-notes/app-release-notes.module.ts b/ui/src/app/modals/app-release-notes/app-release-notes.module.ts new file mode 100644 index 000000000..f6613d5b5 --- /dev/null +++ b/ui/src/app/modals/app-release-notes/app-release-notes.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { AppReleaseNotesPage } from './app-release-notes.page' +import { SharingModule } from 'src/app/modules/sharing.module' + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + SharingModule, + ], + declarations: [AppReleaseNotesPage], +}) +export class AppReleaseNotesPageModule { } diff --git a/ui/src/app/modals/app-release-notes/app-release-notes.page.html b/ui/src/app/modals/app-release-notes/app-release-notes.page.html new file mode 100644 index 000000000..58f75a2e0 --- /dev/null +++ b/ui/src/app/modals/app-release-notes/app-release-notes.page.html @@ -0,0 +1,14 @@ + + + + + + + + {{ version }} Release Notes + + + + +
+
\ No newline at end of file diff --git a/ui/src/app/modals/app-release-notes/app-release-notes.page.scss b/ui/src/app/modals/app-release-notes/app-release-notes.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/modals/app-release-notes/app-release-notes.page.ts b/ui/src/app/modals/app-release-notes/app-release-notes.page.ts new file mode 100644 index 000000000..1c4c47dfa --- /dev/null +++ b/ui/src/app/modals/app-release-notes/app-release-notes.page.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core' +import { ModalController } from '@ionic/angular' + +@Component({ + selector: 'app-release-notes', + templateUrl: './app-release-notes.page.html', + styleUrls: ['./app-release-notes.page.scss'], +}) +export class AppReleaseNotesPage { + @Input() releaseNotes: string + @Input() version: string + + constructor ( + private readonly modalCtrl: ModalController, + ) { } + + dismiss () { + this.modalCtrl.dismiss() + } +} diff --git a/ui/src/app/models/app-model.ts b/ui/src/app/models/app-model.ts new file mode 100644 index 000000000..5d17e2356 --- /dev/null +++ b/ui/src/app/models/app-model.ts @@ -0,0 +1,161 @@ +import { MapSubject, Delta, Update } from '../util/map-subject.util' +import { diff, partitionArray } from '../util/misc.util' +import { PropertySubject, complete } from '../util/property-subject.util' +import { Injectable } from '@angular/core' +import { merge, Observable, of } from 'rxjs' +import { filter, throttleTime, delay, pairwise, mapTo, take } from 'rxjs/operators' +import { Storage } from '@ionic/storage' +import { StorageKeys } from './storage-keys' +import { AppInstalledFull, AppInstalledPreview } from './app-types' + +@Injectable({ + providedIn: 'root', +}) +export class AppModel extends MapSubject { + // hasLoaded tells us if we've successfully queried apps from api or storage, even if there are none. + hasLoaded = false + lastUpdatedAt: { [id: string]: Date } = { } + constructor (private readonly storage: Storage) { + super() + // 500ms after first delta, will save to db. Subsequent deltas are ignored for those 500ms. + // Process continues as long as deltas fire. + this.watchDelta().pipe(throttleTime(200), delay(200)).subscribe(() => { + this.commitCache() + }) + } + + update (newValues: Update, timestamp: Date = new Date()): void { + this.lastUpdatedAt[newValues.id] = this.lastUpdatedAt[newValues.id] || timestamp + if (this.lastUpdatedAt[newValues.id] > timestamp) { + return + } else { + super.update(newValues) + this.lastUpdatedAt[newValues.id] = timestamp + } + } + + // client fxns + watchDelta (filterFor?: Delta['action']): Observable> { + return filterFor + ? this.$delta$.pipe(filter(d => d.action === filterFor)) + : this.$delta$.asObservable() + } + + watch (appId: string) : PropertySubject { + const toReturn = super.watch(appId) + if (!toReturn) throw new Error(`Expected Service ${appId} but not found.`) + return toReturn + } + + // when an app is installing + watchForInstallation (appId: string): Observable { + const toWatch = super.watch(appId) + if (!toWatch) return of(undefined) + + return toWatch.status.pipe( + pairwise(), + filter( ([old, _]) => old === AppStatus.INSTALLING ), + take(1), + mapTo(appId), + ) + } + + watchForInstallations (appIds: { id: string }[]): Observable { + return merge(...appIds.map(({ id }) => this.watchForInstallation(id))).pipe( + filter(t => !!t), + ) + } + + // cache mgmt + clear (): void { + this.ids.forEach(id => { + complete(this.contents[id] || { } as PropertySubject) + delete this.contents[id] + }) + this.hasLoaded = false + this.contents = { } + this.lastUpdatedAt = { } + } + + private commitCache (): Promise { + return this.storage.set(StorageKeys.APPS_CACHE_KEY, this.all || []) + } + + async restoreCache (): Promise { + const stored = await this.storage.get(StorageKeys.APPS_CACHE_KEY) + console.log(`restored app cache`, stored) + if (stored) this.hasLoaded = true + return (stored || []).map(c => this.add({ ...emptyAppInstalledFull(), ...c, status: AppStatus.UNKNOWN })) + } + + upsertAppFull (app: AppInstalledFull): void { + this.update(app) + } + + // synchronizers + upsertApps (apps: AppInstalledPreview[], timestamp: Date): void { + const [updates, creates] = partitionArray(apps, a => !!this.contents[a.id]) + updates.map(u => this.update(u, timestamp)) + creates.map(c => this.add({ ...emptyAppInstalledFull(), ...c })) + } + + syncCache (upToDateApps : AppInstalledPreview[], timestamp: Date) { + this.hasLoaded = true + this.deleteNonexistentApps(upToDateApps) + this.upsertApps(upToDateApps, timestamp) + } + + private deleteNonexistentApps (apps: AppInstalledPreview[]): void { + const currentAppIds = apps.map(a => a.id) + const previousAppIds = Object.keys(this.contents) + const appsToDelete = diff(previousAppIds, currentAppIds) + appsToDelete.map(appId => this.delete(appId)) + } + + // server state change + markAppsUnreachable (): void { + this.updateAllApps({ status: AppStatus.UNREACHABLE }) + } + + markAppsUnknown (): void { + this.updateAllApps({ status: AppStatus.UNKNOWN }) + } + + private updateAllApps (uniformUpdate: Partial) { + this.ids.map(id => { + this.update(Object.assign(uniformUpdate, { id })) + }) + } +} + +function emptyAppInstalledFull (): Omit { + return { + instructions: null, + lastBackup: null, + configuredRequirements: null, + hasFetchedFull: false, + } +} + +export interface Rules { + rule: string + description: string +} + +export enum AppStatus { + // shared + UNKNOWN = 'UNKNOWN', + UNREACHABLE = 'UNREACHABLE', + INSTALLING = 'INSTALLING', + NEEDS_CONFIG = 'NEEDS_CONFIG', + RUNNING = 'RUNNING', + STOPPED = 'STOPPED', + CREATING_BACKUP = 'CREATING_BACKUP', + RESTORING_BACKUP = 'RESTORING_BACKUP', + CRASHED = 'CRASHED', + REMOVING = 'REMOVING', + DEAD = 'DEAD', + BROKEN_DEPENDENCIES = 'BROKEN_DEPENDENCIES', + STOPPING = 'STOPPING', + RESTARTING = 'RESTARTING', +} diff --git a/ui/src/app/models/app-types.ts b/ui/src/app/models/app-types.ts new file mode 100644 index 000000000..7042d34d3 --- /dev/null +++ b/ui/src/app/models/app-types.ts @@ -0,0 +1,122 @@ +import { AppStatus } from './app-model' + +/** APPS **/ + +export interface BaseApp { + id: string + title: string + status: AppStatus | null + versionInstalled: string | null + iconURL: string +} + +// available +export interface AppAvailablePreview extends BaseApp { + versionLatest: string + descriptionShort: string +} + +export type AppAvailableFull = + AppAvailablePreview & + { descriptionLong: string + versions: string[] + } & + AppAvailableVersionSpecificInfo + + +export interface AppAvailableVersionSpecificInfo { + releaseNotes: string + serviceRequirements: AppDependency[] + versionViewing: string +} +// installed + +export interface AppInstalledPreview extends BaseApp { + torAddress: string + versionInstalled: string +} + +export interface AppInstalledFull extends AppInstalledPreview { + instructions: string | null + lastBackup: string | null + configuredRequirements: AppDependency[] | null // null if not yet configured + hasFetchedFull: boolean +} +// dependencies + +export interface AppDependency extends InstalledAppDependency { + // explanation of why it *is* optional. null represents it is required. + optional: string | null + // whether it comes as defualt in the config. This will not be present on an installed app, as we only care + default: boolean +} + +export interface InstalledAppDependency extends Omit { + // semver specification + versionSpec: string + + // an optional description of how this dependency is utlitized by the host app + description: string | null + + // how the requirement is failed, null means satisfied. If the dependency is optional, this should still be set as though it were required. + // This way I can say "it's optional, but also you would need to upgrade it to versionSpec" or "it's optional, but you don't even have it" + // Said another way, if violaion === null, then this thing as a requirement is straight up satisfied. + violation: DependencyViolation | null +} + +export enum DependencyViolationSeverity { + NONE = 0, + OPTIONAL = 1, + RECOMMENDED = 2, + REQUIRED = 3, +} +export function getViolationSeverity (r: AppDependency): DependencyViolationSeverity { + if (!r.optional && r.violation) return DependencyViolationSeverity.REQUIRED + if (r.optional && r.default && r.violation) return DependencyViolationSeverity.RECOMMENDED + if (isOptional(r) && r.violation) return DependencyViolationSeverity.OPTIONAL + return DependencyViolationSeverity.NONE +} + +// optional not recommended +export function isOptional (r: AppDependency): boolean { + return r.optional && !r.default +} + +export function isRecommended (r: AppDependency): boolean { + return r.optional && r.default +} + +export function isMissing (r: AppDependency) { + return r.violation && r.violation.name === 'missing' +} + +export function isMisconfigured (r: AppDependency) { + return r.violation && r.violation.name === 'incompatible-config' +} + +export function isNotRunning (r: AppDependency) { + return r.violation && r.violation.name === 'incompatible-status' +} + +export function isVersionMismatch (r: AppDependency) { + return r.violation && r.violation.name === 'incompatible-version' +} + +export function isInstalling (r: AppDependency) { + return r.violation && r.violation.name === 'incompatible-status' && r.violation.status === AppStatus.INSTALLING +} + + +// both or none +export function getInstalledViolationSeverity (r: InstalledAppDependency): DependencyViolationSeverity { + if (r.violation) return DependencyViolationSeverity.REQUIRED + return DependencyViolationSeverity.NONE +} +// e.g. of I try to uninstall a thing, and some installed apps break, those apps will be returned as instances of this type. +export type DependentBreakage = Omit + +export type DependencyViolation = + { name: 'missing' } | + { name: 'incompatible-version' } | + { name: 'incompatible-config'; ruleViolations: string[]; } | + { name: 'incompatible-status'; status: AppStatus; } diff --git a/ui/src/app/models/model-preload.ts b/ui/src/app/models/model-preload.ts new file mode 100644 index 000000000..ac1775933 --- /dev/null +++ b/ui/src/app/models/model-preload.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@angular/core' +import { AppModel } from './app-model' +import { AppInstalledFull, AppInstalledPreview } from './app-types' +import { ApiService } from '../services/api/api.service' +import { PropertySubject, PropertySubjectId } from '../util/property-subject.util' +import { S9Server, ServerModel } from './server-model' +import { Observable, of, from } from 'rxjs' +import { map, concatMap } from 'rxjs/operators' +import { fromSync$ } from '../util/rxjs.util' + +@Injectable({ + providedIn: 'root', +}) +export class ModelPreload { + constructor ( + private readonly appModel: AppModel, + private readonly api: ApiService, + private readonly serverModel: ServerModel, + ) { } + + apps (): Observable[]> { + return fromSync$(() => this.appModel.getContents()).pipe(concatMap(apps => { + const now = new Date() + if (this.appModel.hasLoaded) { + return of(apps) + } else { + return from(this.api.getInstalledApps()).pipe( + map(appsRes => { + this.appModel.upsertApps(appsRes, now) + return this.appModel.getContents() + }), + ) + }}), + ) + } + + appFull (appId: string): Observable > { + return fromSync$(() => this.appModel.watch(appId)).pipe( + concatMap(app => { + // if we haven't fetched full, don't return till we do + // if we have fetched full, go ahead and return now, but fetch full again in the background + if (!app.hasFetchedFull.getValue()) { + return from(this.loadInstalledApp(appId)) + } else { + this.loadInstalledApp(appId) + return of(app) + } + }), + ) + } + + loadInstalledApp (appId: string): Promise> { + return this.api.getInstalledApp(appId).then(res => { + this.appModel.update({ id: appId, ...res, hasFetchedFull: true }) + return this.appModel.watch(appId) + }) + } + + server (): Observable> { + return fromSync$(() => this.serverModel.watch()).pipe(concatMap(sw => { + if (sw.versionInstalled.getValue()) { + return of(sw) + } else { + console.warn(`server not present, preloading`) + return from(this.api.getServer()).pipe( + map(res => { + this.serverModel.update(res) + return this.serverModel.watch() + })) + } + })) + } +} diff --git a/ui/src/app/models/server-model.ts b/ui/src/app/models/server-model.ts new file mode 100644 index 000000000..377389169 --- /dev/null +++ b/ui/src/app/models/server-model.ts @@ -0,0 +1,175 @@ +import { Injectable } from '@angular/core' +import { Subject, BehaviorSubject } from 'rxjs' +import { PropertySubject, peekProperties, initPropertySubject } from '../util/property-subject.util' +import { AppModel } from './app-model' +import { ConfigService } from 'src/app/services/config.service' +import { Storage } from '@ionic/storage' +import { throttleTime, delay } from 'rxjs/operators' +import { StorageKeys } from './storage-keys' + +@Injectable({ + providedIn: 'root', +}) +export class ServerModel { + lastUpdateTimestamp: Date + $delta$ = new Subject() + private embassy: PropertySubject + + constructor ( + private readonly storage: Storage, + private readonly appModel: AppModel, + private readonly config: ConfigService, + ) { + this.embassy = this.defaultEmbassy() + this.$delta$.pipe( + throttleTime(500), delay(500), + ).subscribe(() => { + this.commitCache() + }) + } + + // client fxns + watch (): PropertySubject { + return this.embassy + } + + peek (): S9Server { + return peekProperties(this.embassy) + } + + update (update: Partial, timestamp: Date = new Date()): void { + if (this.lastUpdateTimestamp > timestamp) return + + if (update.versionInstalled && (update.versionInstalled !== this.config.version) && this.embassy.status.getValue() === ServerStatus.RUNNING) { + console.log('update detected, force reload page') + this.clear() + this.nukeCache().then( + () => location.replace('?upd=' + new Date()), + ) + } + + Object.entries(update).forEach( + ([key, value]) => { + if (!this.embassy[key]) { + console.warn('Received an unexpected key: ', key) + this.embassy[key] = new BehaviorSubject(value) + } else if (JSON.stringify(this.embassy[key].getValue()) !== JSON.stringify(value)) { + this.embassy[key].next(value) + } + }, + ) + this.$delta$.next() + this.lastUpdateTimestamp = timestamp + } + + // cache mgmt + clear () { + this.update(peekProperties(this.defaultEmbassy())) + } + + private commitCache (): Promise { + return this.storage.set(StorageKeys.SERVER_CACHE_KEY, peekProperties(this.embassy)) + } + + private nukeCache (): Promise { + return this.storage.remove(StorageKeys.SERVER_CACHE_KEY) + } + + async restoreCache (): Promise { + const emb = await this.storage.get(StorageKeys.SERVER_CACHE_KEY) + if (emb && emb.versionInstalled === this.config.version) this.update(emb) + } + + // server state change + markUnreachable (): void { + this.update({ status: ServerStatus.UNREACHABLE }) + this.appModel.markAppsUnreachable() + } + + markUnknown (): void { + this.update({ status: ServerStatus.UNKNOWN }) + this.appModel.markAppsUnknown() + } + + defaultEmbassy (): PropertySubject { + return initPropertySubject({ + serverId: undefined, + name: undefined, + origin: this.config.origin, + versionInstalled: undefined, + versionLatest: undefined, + status: ServerStatus.UNKNOWN, + badge: 0, + alternativeRegistryUrl: undefined, + specs: { }, + wifi: { ssids: [], current: undefined }, + ssh: [], + notifications: [], + }) + } +} + +export interface S9Server { + serverId: string + name: string + origin: string + versionInstalled: string + versionLatest: string | undefined + status: ServerStatus + badge: number + alternativeRegistryUrl: string | null + specs: ServerSpecs + wifi: { ssids: string[], current: string } + ssh: SSHFingerprint[] + notifications: S9Notification[] +} + +export interface S9Notification { + id: string + appId: string + createdAt: string + code: string + title: string + message: string +} + +export interface ServerSpecs { + [key: string]: string | number +} + +export interface ServerMetrics { + [key: string]: { + [key: string]: { + value: string | number | null + unit?: string + } + } +} + +export interface SSHFingerprint { + alg: string + hash: string + hostname: string +} + +export interface DiskInfo { + logicalname: string, + size: string, + description: string | null, + partitions: DiskPartition[] +} + +export interface DiskPartition { + logicalname: string, + isMounted: boolean, // Do not let them back up to this if true + size: string | null, + label: string | null, +} + +export enum ServerStatus { + UNKNOWN = 'UNKNOWN', + UNREACHABLE = 'UNREACHABLE', + UPDATING = 'UPDATING', + NEEDS_CONFIG = 'NEEDS_CONFIG', + RUNNING = 'RUNNING', +} diff --git a/ui/src/app/models/storage-keys.ts b/ui/src/app/models/storage-keys.ts new file mode 100644 index 000000000..353f8b322 --- /dev/null +++ b/ui/src/app/models/storage-keys.ts @@ -0,0 +1,6 @@ +export class StorageKeys { + static APPS_CACHE_KEY = 'apps' + static SERVER_CACHE_KEY = 'embassy' + static LOGGED_IN_KEY = 'loggedInKey' + static VIEWED_INSTRUCTIONS_KEY = 'viewedInstructions' +} \ No newline at end of file diff --git a/ui/src/app/modules/sharing.module.ts b/ui/src/app/modules/sharing.module.ts new file mode 100644 index 000000000..d2bb82f24 --- /dev/null +++ b/ui/src/app/modules/sharing.module.ts @@ -0,0 +1,52 @@ +import { NgModule } from '@angular/core' +import { EmverComparesPipe, EmverSatisfiesPipe, EmverDisplayPipe, EmverIsValidPipe } from '../pipes/emver.pipe' +import { IncludesPipe } from '../pipes/includes.pipe' +import { IconPipe } from '../pipes/icon.pipe' +import { TypeofPipe } from '../pipes/typeof.pipe' +import { MarkdownPipe } from '../pipes/markdown.pipe' +import { PeekPropertiesPipe } from '../pipes/peek-properties.pipe' +import { InstalledLatestComparisonPipe, InstalledViewingComparisonPipe } from '../pipes/installed-latest-comparison.pipe' +import { AnnotationStatusPipe } from '../pipes/annotation-status.pipe' +import { TruncateCenterPipe, TruncateEndPipe } from '../pipes/truncate.pipe' +import { MaskPipe } from '../pipes/mask.pipe' +import { DisplayBulbPipe } from '../pipes/display-bulb.pipe' + +@NgModule({ + declarations: [ + EmverComparesPipe, + EmverSatisfiesPipe, + TypeofPipe, + IconPipe, + IncludesPipe, + MarkdownPipe, + PeekPropertiesPipe, + InstalledLatestComparisonPipe, + InstalledViewingComparisonPipe, + AnnotationStatusPipe, + TruncateCenterPipe, + TruncateEndPipe, + MaskPipe, + DisplayBulbPipe, + EmverDisplayPipe, + EmverIsValidPipe, + ], + exports: [ + EmverComparesPipe, + EmverSatisfiesPipe, + TypeofPipe, + IconPipe, + IncludesPipe, + MarkdownPipe, + PeekPropertiesPipe, + InstalledLatestComparisonPipe, + AnnotationStatusPipe, + InstalledViewingComparisonPipe, + TruncateEndPipe, + TruncateCenterPipe, + MaskPipe, + DisplayBulbPipe, + EmverDisplayPipe, + EmverIsValidPipe, + ], +}) +export class SharingModule { } \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-available-list/app-available-list.module.ts b/ui/src/app/pages/apps-routes/app-available-list/app-available-list.module.ts new file mode 100644 index 000000000..abe1fa401 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-available-list/app-available-list.module.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { AppAvailableListPage } from './app-available-list.page' +import { SharingModule } from '../../../modules/sharing.module' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { StatusComponentModule } from 'src/app/components/status/status.component.module' + + +const routes: Routes = [ + { + path: '', + component: AppAvailableListPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + StatusComponentModule, + SharingModule, + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [AppAvailableListPage], +}) +export class AppAvailableListPageModule { } diff --git a/ui/src/app/pages/apps-routes/app-available-list/app-available-list.page.html b/ui/src/app/pages/apps-routes/app-available-list/app-available-list.page.html new file mode 100644 index 000000000..ddc829024 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-available-list/app-available-list.page.html @@ -0,0 +1,54 @@ + + + Service Marketplace + + + + + + + + + + + + + + + + + + {{ error }} + + + + + + + + +

+ {{app.subject.title | async}} +

+
+ Installed +
+
+ Update Available +
+
+ Installing + +
+
+
+ + {{ app.subject.descriptionShort | async }} + +
+
+
diff --git a/ui/src/app/pages/apps-routes/app-available-list/app-available-list.page.scss b/ui/src/app/pages/apps-routes/app-available-list/app-available-list.page.scss new file mode 100644 index 000000000..c8043e139 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-available-list/app-available-list.page.scss @@ -0,0 +1,6 @@ +.beneath-title { + font-size: 12px; + font-style: italic; + font-family: 'Open Sans'; + padding: 1px 0px 1.5px 0px; +} \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-available-list/app-available-list.page.ts b/ui/src/app/pages/apps-routes/app-available-list/app-available-list.page.ts new file mode 100644 index 000000000..03f74a3ce --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-available-list/app-available-list.page.ts @@ -0,0 +1,82 @@ +import { Component, NgZone } from '@angular/core' +import { ApiService } from 'src/app/services/api/api.service' +import { AppModel } from 'src/app/models/app-model' +import { AppAvailablePreview, AppInstalledPreview } from 'src/app/models/app-types' +import { pauseFor } from 'src/app/util/misc.util' +import { PropertySubjectId, initPropertySubject } from 'src/app/util/property-subject.util' +import { Subscription, BehaviorSubject, combineLatest } from 'rxjs' +import { take } from 'rxjs/operators' +import { markAsLoadingDuringP } from 'src/app/services/loader.service' + +@Component({ + selector: 'app-available-list', + templateUrl: './app-available-list.page.html', + styleUrls: ['./app-available-list.page.scss'], +}) +export class AppAvailableListPage { + $loading$ = new BehaviorSubject(true) + error = '' + installedAppDeltaSubscription: Subscription + apps: PropertySubjectId[] = [] + appsInstalled: PropertySubjectId[] = [] + + constructor ( + private readonly apiService: ApiService, + private readonly appModel: AppModel, + private readonly zone: NgZone, + ) { } + + async ngOnInit () { + this.installedAppDeltaSubscription = this.appModel + .watchDelta('update') + .subscribe(({ id }) => this.mergeInstalledProps(id)) + + markAsLoadingDuringP(this.$loading$, Promise.all([ + this.getApps(), + pauseFor(600), + ])) + } + + ionViewDidEnter () { + this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id)) + } + + mergeInstalledProps (appInstalledId: string) { + const appAvailable = this.apps.find(app => app.id === appInstalledId) + if (!appAvailable) return + + const app = this.appModel.watch(appInstalledId) + combineLatest([app.status, app.versionInstalled]) + .pipe(take(1)) + .subscribe(([status, versionInstalled]) => { + this.zone.run(() => { + appAvailable.subject.status.next(status) + appAvailable.subject.versionInstalled.next(versionInstalled) + }) + }) + } + + ngOnDestroy () { + this.installedAppDeltaSubscription.unsubscribe() + } + + async doRefresh (e: any) { + await Promise.all([ + this.getApps(), + pauseFor(600), + ]) + e.target.complete() + } + + async getApps (): Promise { + try { + this.apps = await this.apiService.getAvailableApps().then(apps => + apps.map(a => ({ id: a.id, subject: initPropertySubject(a) })), + ) + this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id)) + } catch (e) { + console.error(e) + this.error = e.message + } + } +} diff --git a/ui/src/app/pages/apps-routes/app-available-show/app-available-show.module.ts b/ui/src/app/pages/apps-routes/app-available-show/app-available-show.module.ts new file mode 100644 index 000000000..3bede4774 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-available-show/app-available-show.module.ts @@ -0,0 +1,42 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { DependencyListComponentModule } from '../../../components/dependency-list/dependency-list.component.module' +import { AppAvailableShowPage } from './app-available-show.page' +import { SharingModule } from 'src/app/modules/sharing.module' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { StatusComponentModule } from 'src/app/components/status/status.component.module' +import { RecommendationButtonComponentModule } from 'src/app/components/recommendation-button/recommendation-button.component.module' +import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module' +import { ErrorMessageComponentModule } from 'src/app/components/error-message/error-message.component.module' +import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module' +import { AppReleaseNotesPageModule } from 'src/app/modals/app-release-notes/app-release-notes.module' + +const routes: Routes = [ + { + path: '', + component: AppAvailableShowPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + StatusComponentModule, + DependencyListComponentModule, + RouterModule.forChild(routes), + SharingModule, + PwaBackComponentModule, + RecommendationButtonComponentModule, + BadgeMenuComponentModule, + InstallWizardComponentModule, + ErrorMessageComponentModule, + InformationPopoverComponentModule, + AppReleaseNotesPageModule, + ], + declarations: [AppAvailableShowPage], +}) +export class AppAvailableShowPageModule { } diff --git a/ui/src/app/pages/apps-routes/app-available-show/app-available-show.page.html b/ui/src/app/pages/apps-routes/app-available-show/app-available-show.page.html new file mode 100644 index 000000000..b4fd34aa8 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-available-show/app-available-show.page.html @@ -0,0 +1,115 @@ + + + + + + Marketplace Details + + + + + + + + + + + + + + + + + + +

{{ vars.title }}

+

{{ vars.versionViewing | displayEmver }}

+ +

Installed

+

Installed at {{vars.versionInstalled | displayEmver}}

+
+ +

+ +

+
+
+ +
+
+ + + Install + + +
+ + Update to {{ vars.versionViewing | displayEmver }} + + + Downgrade to {{ vars.versionViewing | displayEmver }} + +
+ + + + + +

+ + + + {{recommendation.title}} +

+
+

{{recommendation.description}}

+

{{vars.title}} version {{vars.versionViewing | displayEmver}} is compatible.

+

{{vars.title}} version {{vars.versionViewing | displayEmver}} is NOT compatible.

+
+
+
+
+ + Description + + + +
{{ vars.descriptionLong }}
+
+
+
+ + Release Notes + + + New in {{ vars.versionViewing | displayEmver }} + + + + + Service Dependencies + + + + + + + + + + Other versions + +
+
+
\ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-available-show/app-available-show.page.scss b/ui/src/app/pages/apps-routes/app-available-show/app-available-show.page.scss new file mode 100644 index 000000000..fa9c4aab5 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-available-show/app-available-show.page.scss @@ -0,0 +1,34 @@ +// .recommendation-container { +// margin-top: 3px; +// display: grid; +// grid-template-columns: auto auto; +// justify-content: start; +// grid-column-gap: 5px; +// align-items: center; +// } + +.recommendation-text { + font-style: italic; +} + +// @media (min-width:1000px) { +// .recommendation-text { +// font-size: small; +// } +// } + +.recommendation-error { + color: var(--ion-color-danger); +} + +.main-action-button { + margin: 20px 5px 20px 5px; +} + +.divider { + margin-top: 15px; + color: var(--ion-color-medium); + font-size: medium; + padding-left: 10px; + font-weight: unset; +} \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-available-show/app-available-show.page.ts b/ui/src/app/pages/apps-routes/app-available-show/app-available-show.page.ts new file mode 100644 index 000000000..0fc5f9147 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-available-show/app-available-show.page.ts @@ -0,0 +1,249 @@ +import { Component, HostListener, NgZone } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { AppAvailableFull, AppAvailableVersionSpecificInfo, AppDependency } from 'src/app/models/app-types' +import { ApiService } from 'src/app/services/api/api.service' +import { AlertController, ModalController, NavController, PopoverController } from '@ionic/angular' +import { markAsLoadingDuring$ } from 'src/app/services/loader.service' +import { BehaviorSubject, from, merge, Observable, of, Subscription } from 'rxjs' +import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators' +import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component' +import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' +import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' +import { AppModel } from 'src/app/models/app-model' +import { initPropertySubject, peekProperties, PropertySubject } from 'src/app/util/property-subject.util' +import { Cleanup } from 'src/app/util/cleanup' +import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component' +import { pauseFor } from 'src/app/util/misc.util' +import { AppReleaseNotesPage } from 'src/app/modals/app-release-notes/app-release-notes.page' +import { Emver } from 'src/app/services/emver.service' +import { displayEmver } from 'src/app/pipes/emver.pipe' + +@Component({ + selector: 'app-available-show', + templateUrl: './app-available-show.page.html', + styleUrls: ['./app-available-show.page.scss'], +}) +export class AppAvailableShowPage extends Cleanup { + $loading$ = new BehaviorSubject(true) + $versionSpecificLoading$ = new BehaviorSubject(false) + $error$ = new BehaviorSubject(undefined) + app$: PropertySubject = { } as any + appId: string + + openRecommendation = false + recommendation: Recommendation | null = null + + serviceDependencyDefintion = 'Service Dependencies are other services that this service recommends or requires in order to run.' + + showMoreReleaseNotes = false + + constructor ( + private readonly route: ActivatedRoute, + private readonly apiService: ApiService, + private readonly alertCtrl: AlertController, + private readonly zone: NgZone, + private readonly modalCtrl: ModalController, + private readonly wizardBaker: WizardBaker, + private readonly navCtrl: NavController, + private readonly appModel: AppModel, + private readonly popoverController: PopoverController, + private readonly emver: Emver, + ) { + super() + } + + async ngOnInit () { + this.appId = this.route.snapshot.paramMap.get('appId') as string + + this.cleanup( + markAsLoadingDuring$(this.$loading$, + from(this.apiService.getAvailableApp(this.appId)).pipe( + tap(app => this.app$ = initPropertySubject(app)), + concatMap(() => this.fetchRecommendation()), + ), + ).pipe( + concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack + catchError(e => of(this.setError(e))), + ).subscribe(), + merge(this.$loading$, this.$versionSpecificLoading$).pipe(concatMap(l => { + if (l) { + this.showMoreReleaseNotes = false + } + return pauseFor(125) + })).subscribe( + () => this.setMoreReleaseNotes(), + ), + ) + } + + ionViewDidEnter () { + markAsLoadingDuring$(this.$versionSpecificLoading$, this.fetchAppVersionInfo()).subscribe({ + error: e => this.setError(e), + }) + } + + async presentPopover (information: string, ev: any) { + const popover = await this.popoverController.create({ + component: InformationPopoverComponent, + event: ev, + translucent: false, + showBackdrop: true, + backdropDismiss: true, + componentProps: { + information, + }, + }) + return await popover.present() + } + + fetchAppVersionInfo (versionSpec?: string): Observable { + if (!this.app$.versionViewing) return of({ }) + const specToFetch = versionSpec || `=${this.app$.versionViewing.getValue()}` + return from(this.apiService.getAvailableAppVersionSpecificInfo(this.appId, specToFetch)).pipe( + tap(versionInfo => this.syncVersionSpecificInfo(versionInfo)), + ) + } + + private syncVersionSpecificInfo (versionSpecificInfo: AppAvailableVersionSpecificInfo) { + this.zone.run(() => { + Object.entries(versionSpecificInfo).forEach( ([k, v]) => { + if (!this.app$[k]) this.app$[k] = new BehaviorSubject(undefined) + if (v !== this.app$[k].getValue()) this.app$[k].next(v) + }) + }) + } + + async presentAlertVersions () { + const app = peekProperties(this.app$) + const alert = await this.alertCtrl.create({ + header: 'Versions', + backdropDismiss: false, + inputs: app.versions.sort((a, b) => -1 * this.emver.compare(a, b)).map(v => { + return { name: v, // for CSS + type: 'radio', + label: displayEmver(v), // appearance on screen + value: v, // literal SEM version value + checked: app.versionViewing === v, + } + }), + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, { + text: 'Ok', + handler: (version: string) => { + const previousVersion = this.app$.versionViewing.getValue() + this.app$.versionViewing.next(version) + markAsLoadingDuring$(this.$versionSpecificLoading$, this.fetchAppVersionInfo(`=${version}`)) + .subscribe({ + error: e => { + this.setError(e) + this.app$.versionViewing.next(previousVersion) + }, + }) + }, + }, + ], + }) + + await alert.present() + } + + async install () { + const app = peekProperties(this.app$) + const { cancelled } = await wizardModal( + this.modalCtrl, + this.wizardBaker.install({ + id: app.id, + title: app.title, + version: app.versionViewing, + serviceRequirements: app.serviceRequirements, + }), + ) + if (cancelled) return + this.navCtrl.back() + } + + async update (action: 'update' | 'downgrade') { + const app = peekProperties(this.app$) + + const value = { + id: app.id, + title: app.title, + version: app.versionViewing, + serviceRequirements: app.serviceRequirements, + } + + switch (action) { + case 'update': + return wizardModal( + this.modalCtrl, + this.wizardBaker.update(value), + ).then(({ cancelled }) => cancelled || this.navCtrl.back()) + case 'downgrade': + return wizardModal( + this.modalCtrl, + this.wizardBaker.downgrade(value), + ).then(({ cancelled }) => cancelled || this.navCtrl.back()) + } + } + + async presentModalReleaseNotes () { + const { releaseNotes, versionViewing } = peekProperties(this.app$) + + const modal = await this.modalCtrl.create({ + component: AppReleaseNotesPage, + componentProps: { + releaseNotes, + version: versionViewing, + }, + }) + + await modal.present() + } + + private fetchRecommendation (): Observable { + this.recommendation = history.state && history.state.installationRecommendation + + if (this.recommendation) { + return from(this.fetchAppVersionInfo(this.recommendation.versionSpec)) + } else { + return of({ }) + } + } + + private syncWhenDependencyInstalls (): Observable { + return this.app$.serviceRequirements.pipe( + filter(deps => !!deps), + switchMap(deps => this.appModel.watchForInstallations(deps)), + concatMap(() => markAsLoadingDuring$(this.$versionSpecificLoading$, this.fetchAppVersionInfo())), + catchError(e => of(console.error(e))), + ) + } + + private setError (e: Error) { + console.error(e) + this.$error$.next(e.message) + } + + private setMoreReleaseNotes () { + const releaseNotes = document.getElementById(`release-notes-${this.appId}`) + if (releaseNotes) { + this.showMoreReleaseNotes = isTextOverflow(releaseNotes) + } + } + + @HostListener('window:resize', ['$event']) + onResize () { + this.setMoreReleaseNotes() + } +} + +function isTextOverflow (elem: any): boolean { + if (elem) { + return (elem.offsetWidth < elem.scrollWidth) + } + + return false +} \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-config/app-config.module.ts b/ui/src/app/pages/apps-routes/app-config/app-config.module.ts new file mode 100644 index 000000000..9252d3203 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-config/app-config.module.ts @@ -0,0 +1,45 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { FormsModule } from '@angular/forms' +import { Routes, RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { AppConfigPage } from './app-config.page' +import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module' +import { AppConfigListPageModule } from 'src/app/modals/app-config-list/app-config-list.module' +import { AppConfigObjectPageModule } from 'src/app/modals/app-config-object/app-config-object.module' +import { AppConfigUnionPageModule } from 'src/app/modals/app-config-union/app-config-union.module' +import { AppConfigValuePageModule } from 'src/app/modals/app-config-value/app-config-value.module' +import { SharingModule } from 'src/app/modules/sharing.module' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { RecommendationButtonComponentModule } from 'src/app/components/recommendation-button/recommendation-button.component.module' +import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module' + +const routes: Routes = [ + { + path: '', + component: AppConfigPage, + // canDeactivate: [CanDeactivateGuard], + }, +] + +@NgModule({ + imports: [ + ObjectConfigComponentModule, + AppConfigListPageModule, + AppConfigObjectPageModule, + AppConfigUnionPageModule, + AppConfigValuePageModule, + SharingModule, + CommonModule, + FormsModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + RecommendationButtonComponentModule, + InformationPopoverComponentModule, + ], + declarations: [AppConfigPage], +}) +export class AppConfigPageModule { } diff --git a/ui/src/app/pages/apps-routes/app-config/app-config.page.html b/ui/src/app/pages/apps-routes/app-config/app-config.page.html new file mode 100644 index 000000000..a2168d8e4 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-config/app-config.page.html @@ -0,0 +1,119 @@ + + + + + + + + {{ app['title'] | async }} + + + + + + +
+ + + {{$loadingText$ | async}} + +
+ + + + + +

{{error.text}}.

+
+ + + +

+

+ Hide +
+ + + + + + + + + + + +

+ + Initial Config +

+

To use the default config for {{ app.title | async }}, click "Save" below.

+
+
+
+ + + + +

+ + + + + {{recommendation.title}} +

+
+

{{app.title | async}} config has been modified to satisfy {{recommendation.title}}. + To accept the changes, click “Save” below. +

+ More Info + +

+ hide +
+ + + +
+
+
+ +
+ + + + +

{{invalid}}

+
+
+ + + + +

No config options for {{ app.title | async }} {{ app.versionInstalled | async }}.

+
+
+ + + + + + Save + + + + + Config Options + + + +
+ + diff --git a/ui/src/app/pages/apps-routes/app-config/app-config.page.scss b/ui/src/app/pages/apps-routes/app-config/app-config.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/apps-routes/app-config/app-config.page.ts b/ui/src/app/pages/apps-routes/app-config/app-config.page.ts new file mode 100644 index 000000000..2b43fb273 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-config/app-config.page.ts @@ -0,0 +1,244 @@ +import { Component } from '@angular/core' +import { NavController, AlertController, ModalController, PopoverController } from '@ionic/angular' +import { ActivatedRoute } from '@angular/router' +import { AppStatus } from 'src/app/models/app-model' +import { AppInstalledFull } from 'src/app/models/app-types' +import { ApiService } from 'src/app/services/api/api.service' +import { pauseFor, isEmptyObject } from 'src/app/util/misc.util' +import { PropertySubject, peekProperties } from 'src/app/util/property-subject.util' +import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service' +import { TrackingModalController } from 'src/app/services/tracking-modal-controller.service' +import { ModelPreload } from 'src/app/models/model-preload' +import { BehaviorSubject, forkJoin, from, fromEvent, of } from 'rxjs' +import { catchError, concatMap, map, take, tap } from 'rxjs/operators' +import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component' +import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' +import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' +import { Cleanup } from 'src/app/util/cleanup' +import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component' +import { ConfigSpec } from 'src/app/app-config/config-types' +import { ConfigCursor } from 'src/app/app-config/config-cursor' + +@Component({ + selector: 'app-config', + templateUrl: './app-config.page.html', + styleUrls: ['./app-config.page.scss'], +}) +export class AppConfigPage extends Cleanup { + error: { text: string, moreInfo?: + { title: string, description: string, buttonText: string } + } + + invalid: string + $loading$ = new BehaviorSubject(true) + $loadingText$ = new BehaviorSubject(undefined) + + app: PropertySubject = { } as any + appId: string + hasConfig = false + + recommendation: Recommendation | null = null + showRecommendation = true + openRecommendation = false + + edited: boolean + added: boolean + rootCursor: ConfigCursor<'object'> + spec: ConfigSpec + config: object + + AppStatus = AppStatus + + constructor ( + private readonly navCtrl: NavController, + private readonly route: ActivatedRoute, + private readonly wizardBaker: WizardBaker, + private readonly preload: ModelPreload, + private readonly apiService: ApiService, + private readonly loader: LoaderService, + private readonly alertCtrl: AlertController, + private readonly modalController: ModalController, + private readonly trackingModalCtrl: TrackingModalController, + private readonly popoverController: PopoverController, + ) { super() } + + backButtonDefense = false + + async ngOnInit () { + this.appId = this.route.snapshot.paramMap.get('appId') as string + + this.route.params.pipe(take(1)).subscribe(params => { + if (params.edit) { + window.history.back() + } + }) + + this.cleanup( + fromEvent(window, 'popstate').subscribe(() => { + this.backButtonDefense = false + this.trackingModalCtrl.dismissAll() + }), + this.trackingModalCtrl.onCreateAny$().subscribe(() => { + if (!this.backButtonDefense) { + window.history.pushState(null, null, window.location.href + '/edit') + this.backButtonDefense = true + } + }), + this.trackingModalCtrl.onDismissAny$().subscribe(() => { + if (!this.trackingModalCtrl.anyModals && this.backButtonDefense === true) { + this.navCtrl.back() + } + }), + ) + + markAsLoadingDuring$(this.$loading$, + from(this.preload.appFull(this.appId)) + .pipe( + tap(app => this.app = app), + tap(() => this.$loadingText$.next(`Fetching config spec...`)), + concatMap(() => forkJoin([this.apiService.getAppConfig(this.appId), pauseFor(600)])), + concatMap(([{ spec, config }]) => { + const rec = history.state && history.state.configRecommendation as Recommendation + if (rec) { + this.$loadingText$.next(`Setting properties to accomodate ${rec.title}...`) + return from(this.apiService.postConfigureDependency(this.appId, rec.appId, true)) + .pipe( + map(res => ({ + spec, + config, + dependencyConfig: res.config, + })), + tap(() => this.recommendation = rec), + catchError(e => { + this.error = { text: `Could not set properties to accomodate ${rec.title}: ${e.message}`, moreInfo: { + title: `${rec.title} requires the following:`, + description: rec.description, + buttonText: 'Configure Manually', + } } + return of({ spec, config, dependencyConfig: null }) + }), + ) + } else { + return of({ spec, config, dependencyConfig: null }) + } + }), + map(({ spec, config, dependencyConfig }) => this.setConfig(spec, config, dependencyConfig)), + tap(() => this.$loadingText$.next(undefined)), + ), + ).subscribe({ + error: e => { + console.error(e) + this.error = { text: e.message } + }, + }, + ) + } + + async presentPopover (title: string, description: string, ev: any) { + const information = ` +
+ ${title} +
+
+ ${description} +
+ ` + const popover = await this.popoverController.create({ + component: InformationPopoverComponent, + event: ev, + translucent: false, + showBackdrop: true, + backdropDismiss: true, + componentProps: { + information, + }, + }) + return await popover.present() + } + + setConfig (spec: ConfigSpec, config: object, dependencyConfig?: object) { + this.rootCursor = dependencyConfig ? new ConfigCursor(spec, config, null, dependencyConfig) : new ConfigCursor(spec, config) + this.spec = this.rootCursor.spec().spec + this.config = this.rootCursor.config() + this.handleObjectEdit() + this.hasConfig = !isEmptyObject(this.spec) + } + + dismissRecommendation () { + this.showRecommendation = false + } + + dismissError () { + this.error = undefined + } + + async cancel () { + if (this.edited) { + await this.presentAlertUnsaved() + } else { + this.navCtrl.back() + } + } + + async save () { + const app = peekProperties(this.app) + + return this.loader.of({ + message: `Saving config...`, + spinner: 'lines', + cssClass: 'loader', + }).displayDuringAsync(async () => { + const config = this.config + const { breakages } = await this.apiService.patchAppConfig(app, config, true) + + if (breakages.length) { + const { cancelled } = await wizardModal( + this.modalController, + this.wizardBaker.configure({ + app, + breakages, + }), + ) + if (cancelled) return { skip: true } + } + + return this.apiService.patchAppConfig(app, config).then( + () => this.preload.loadInstalledApp(this.appId).then(() => ({ skip: false})), + ) + }) + .then(({ skip }) => { + if (skip) return + this.navCtrl.back() + }) + .catch(e => this.error = { text: e.message }) + } + + handleObjectEdit () { + this.edited = this.rootCursor.isEdited() + this.added = this.rootCursor.isNew() + this.invalid = this.rootCursor.checkInvalid() + } + + private async presentAlertUnsaved () { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Unsaved Changes', + message: 'You have unsaved changes. Are you sure you want to leave?', + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: `Leave`, + cssClass: 'alert-danger', + handler: () => { + this.navCtrl.back() + }, + }, + ], + }) + await alert.present() + } +} + diff --git a/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.module.ts b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.module.ts new file mode 100644 index 000000000..5d881d79d --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' + +import { IonicModule } from '@ionic/angular' + +import { DependencyListComponentModule } from 'src/app/components/dependency-list/dependency-list.component.module' +import { AppInstalledListPage } from './app-installed-list.page' +import { StatusComponentModule } from 'src/app/components/status/status.component.module' +import { SharingModule } from 'src/app/modules/sharing.module' +import { AppBackupPageModule } from 'src/app/modals/app-backup/app-backup.module' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' + +const routes: Routes = [ + { + path: '', + component: AppInstalledListPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + StatusComponentModule, + DependencyListComponentModule, + AppBackupPageModule, + SharingModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [AppInstalledListPage], +}) +export class AppInstalledListPageModule { } diff --git a/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.html b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.html new file mode 100644 index 000000000..8a26bbf91 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.html @@ -0,0 +1,53 @@ + + + Installed Services + + + + + + + + + + + + + + + + {{ error }} + + + + + + + + + + + + + + +

{{ app.subject.title | async }}

+
+
+
+
+
+ +
+
+

Welcome to your Embassy

+

Get started by installing your first service.

+
+ + + Marketplace + +
+ +
+
diff --git a/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.scss b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.scss new file mode 100644 index 000000000..2fc83eaed --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.scss @@ -0,0 +1,53 @@ +.installed-card { + margin: 0; + background: linear-gradient(37deg, #333333, #131313); + border-radius: 10px; + text-align: center; + + ion-card-header { + padding: 0; + + status { + font-size: 9px; + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + p { + font-family: 'Montserrat'; + font-size: 11px; + color: white; + margin: 0px 12px 8px 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +.main-img { + width: 50%; + margin: 9px; + color: white; + border-radius: var(--icon-border-radius); +} + +.bulb-on { + position: absolute !important; + left: -7px !important; + top: -7px !important; + height: 25px !important; + width: 25px !important; + margin: 9px; +} + +.bulb-off { + position: absolute !important; + left: -1px !important; + top: -1px !important; + height: 13px !important; + width: 13px !important; + margin: 9px; +} diff --git a/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.ts b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.ts new file mode 100644 index 000000000..d44870ffd --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-installed-list/app-installed-list.page.ts @@ -0,0 +1,106 @@ +import { Component } from '@angular/core' +import { AppModel } from 'src/app/models/app-model' +import { AppInstalledPreview } from 'src/app/models/app-types' +import { ModelPreload } from 'src/app/models/model-preload' +import { doForAtLeast } from 'src/app/util/misc.util' +import { PropertySubject, PropertySubjectId, toObservable } from 'src/app/util/property-subject.util' +import { markAsLoadingDuring$ } from 'src/app/services/loader.service' +import { BehaviorSubject, Observable, Subscription } from 'rxjs' +import { S9Server, ServerModel, ServerStatus } from 'src/app/models/server-model' +import { SyncDaemon } from 'src/app/services/sync.service' +import { Cleanup } from 'src/app/util/cleanup' + +@Component({ + selector: 'app-installed-list', + templateUrl: './app-installed-list.page.html', + styleUrls: ['./app-installed-list.page.scss'], +}) +export class AppInstalledListPage extends Cleanup { + error = '' + initError = '' + $loading$ = new BehaviorSubject(true) + s9Host$: Observable + + server: PropertySubject + currentServer: S9Server + apps: PropertySubjectId[] = [] + + subsToTearDown: Subscription[] = [] + + updatingFreeze = false + updating = false + segmentValue: 'services' | 'embassy' = 'services' + + showCertDownload : boolean + + constructor ( + private readonly serverModel: ServerModel, + private readonly appModel: AppModel, + private readonly preload: ModelPreload, + private readonly syncDaemon: SyncDaemon, + ) { + super() + } + + ngOnDestroy () { + this.subsToTearDown.forEach(s => s.unsubscribe()) + } + + async ngOnInit () { + this.server = this.serverModel.watch() + this.apps = [] + this.cleanup( + // serverUpdateSubscription + this.server.status.subscribe(status => { + if (status === ServerStatus.UPDATING) { + this.updating = true + } else { + if (!this.updatingFreeze) { this.updating = false } + } + }), + + // newAppsSubscription + this.appModel.watchDelta('add').subscribe(({ id }) => { + if (this.apps.find(a => a.id === id)) return + this.apps.push({ id, subject: this.appModel.watch(id) }) + }, + ), + + // appsDeletedSubscription + this.appModel.watchDelta('delete').subscribe(({ id }) => { + const i = this.apps.findIndex(a => a.id === id) + this.apps.splice(i, 1) + }), + + // currentServerSubscription + toObservable(this.server).subscribe(currentServerProperties => { + this.currentServer = currentServerProperties + }), + ) + + markAsLoadingDuring$(this.$loading$, this.preload.apps()).subscribe({ + next: apps => { + this.apps = apps + }, + error: e => { + console.error(e) + this.error = e.message + }, + }) + } + + async doRefresh (event: any) { + await doForAtLeast([this.getServerAndApps()], 600) + event.target.complete() + } + + async getServerAndApps (): Promise { + try { + await this.syncDaemon.sync() + this.error = '' + } catch (e) { + console.error(e) + this.error = e.message + } + } +} diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.module.ts b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.module.ts new file mode 100644 index 000000000..c49ec7883 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.module.ts @@ -0,0 +1,42 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' + +import { IonicModule } from '@ionic/angular' + +import { DependencyListComponentModule } from 'src/app/components/dependency-list/dependency-list.component.module' +import { AppInstalledShowPage } from './app-installed-show.page' +import { StatusComponentModule } from 'src/app/components/status/status.component.module' +import { SharingModule } from 'src/app/modules/sharing.module' +import { AppBackupPageModule } from 'src/app/modals/app-backup/app-backup.module' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module' +import { ErrorMessageComponentModule } from 'src/app/components/error-message/error-message.component.module' +import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module' + +const routes: Routes = [ + { + path: '', + component: AppInstalledShowPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + StatusComponentModule, + DependencyListComponentModule, + AppBackupPageModule, + SharingModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + InstallWizardComponentModule, + ErrorMessageComponentModule, + InformationPopoverComponentModule, + ], + declarations: [AppInstalledShowPage], +}) +export class AppInstalledShowPageModule { } diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html new file mode 100644 index 000000000..a8d317229 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.html @@ -0,0 +1,163 @@ + + + + + + Service Details + + + + + + + + + + + + + + + + +
+ + + + + +
+ + {{ vars.title }} + + + {{ vars.versionInstalled | displayEmver }} + +
+
+
+ + + + + + Configure + + + Stop + + + Stop Backup + + + Force Uninstall + + + Fix + + + Start + + + +
+ + + + Tor Address + + +

{{ vars.torAddress | truncateCenter:18:18:true }}

+ + + +
+
+ + Backups + + + + + Create new Backup + + Last Backup: {{vars.lastBackup ? (vars.lastBackup | date: 'short') : 'never'}} + + + + + + + Restore from Backup + + + General + + + + Check for Updates + + + + + Instructions + + + + + Config + + + + + Properties + + + + + Logs + + + + + Marketplace Details + + + + + Dependencies + + + + + + + + + + + + + + Uninstall + + +
+
+
+
diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss new file mode 100644 index 000000000..ff5e97c0d --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.scss @@ -0,0 +1,41 @@ +.full-width { + margin: 10px; +} + +.about-attribute { + font-size: small; +} + +.about-attribute-value { + font-size: small; +} + +.less-large { + font-size: 20px !important; +} + +.top-plate { + // margin-top: 20px; + background: var(--ion-item-background); + margin: 20px 10px; + border-radius: 10px; + border-style: solid; + border-color: #373737; +} + +.status-readout { + display: flex; + justify-content: space-between; + padding: 4px 10px; + border-radius: 10px; + align-items: center; + background: var(--ion-background-color); + margin: 10px 10px 15px 10px; + border-style: solid; + border-width: 1px; + border-color: #404040; +} + +.no-cushion-item { + --background: transparent; --padding-start: 0px; --inner-padding-end: 0px; --padding-end: 0px; +} diff --git a/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts new file mode 100644 index 000000000..596447f53 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-installed-show/app-installed-show.page.ts @@ -0,0 +1,300 @@ +import { Component, ViewChild } from '@angular/core' +import { AlertController, NavController, ToastController, ModalController, IonContent, PopoverController } from '@ionic/angular' +import { ApiService } from 'src/app/services/api/api.service' +import { ActivatedRoute } from '@angular/router' +import { copyToClipboard } from 'src/app/util/web.util' +import { AppModel, AppStatus } from 'src/app/models/app-model' +import { AppInstalledFull } from 'src/app/models/app-types' +import { ModelPreload } from 'src/app/models/model-preload' +import { chill, pauseFor } from 'src/app/util/misc.util' +import { PropertySubject, peekProperties } from 'src/app/util/property-subject.util' +import { AppBackupPage } from 'src/app/modals/app-backup/app-backup.page' +import { LoaderService, markAsLoadingDuring$, markAsLoadingDuringP } from 'src/app/services/loader.service' +import { BehaviorSubject, Observable, of } from 'rxjs' +import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component' +import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards' +import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators' +import { Cleanup } from 'src/app/util/cleanup' +import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component' +import { Emver } from 'src/app/services/emver.service' + +@Component({ + selector: 'app-installed-show', + templateUrl: './app-installed-show.page.html', + styleUrls: ['./app-installed-show.page.scss'], +}) +export class AppInstalledShowPage extends Cleanup { + $loading$ = new BehaviorSubject(true) + $loadingDependencies$ = new BehaviorSubject(false) // when true, dependencies will render with spinners. + + $error$ = new BehaviorSubject('') + app: PropertySubject = { } as any + appId: string + AppStatus = AppStatus + showInstructions = false + + dependencyDefintion = () => `Dependencies are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}` + + @ViewChild(IonContent) content: IonContent + + constructor ( + private readonly alertCtrl: AlertController, + private readonly route: ActivatedRoute, + private readonly navCtrl: NavController, + private readonly loader: LoaderService, + private readonly toastCtrl: ToastController, + private readonly modalCtrl: ModalController, + private readonly apiService: ApiService, + private readonly preload: ModelPreload, + private readonly wizardBaker: WizardBaker, + private readonly appModel: AppModel, + private readonly popoverController: PopoverController, + private readonly emver: Emver, + ) { + super() + } + + async ngOnInit () { + this.appId = this.route.snapshot.paramMap.get('appId') as string + + this.cleanup( + markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId)) + .pipe( + tap(app => this.app = app), + concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack + catchError(e => of(this.setError(e))), + ).subscribe(), + ) + } + + ionViewDidEnter () { + markAsLoadingDuringP(this.$loadingDependencies$, this.getApp()) + } + + async doRefresh (event: any) { + await Promise.all([ + this.getApp(), + pauseFor(600), + ]) + event.target.complete() + } + + async scrollToRequirements () { + return this.scrollToElement('service-requirements-' + this.appId) + } + + async getApp (): Promise { + try { + await this.preload.loadInstalledApp(this.appId) + this.clearError() + } catch (e) { + this.setError(e) + } + } + + async checkForUpdates () { + const app = peekProperties(this.app) + + this.loader.of({ + message: `Checking for updates...`, + spinner: 'lines', + cssClass: 'loader', + }).displayDuringAsync( + async () => { + const { versionLatest } = await this.apiService.getAvailableApp(this.appId) + if (this.emver.compare(versionLatest, app.versionInstalled) === 1) { + this.presentAlertUpdate(app, versionLatest) + } else { + this.presentAlertUpToDate() + } + }, + ).catch(e => this.setError(e)) + } + + async presentAlertUpdate (app: AppInstalledFull, versionLatest: string) { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Update Available', + message: `New version ${versionLatest} found for ${app.title}.`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'View in Store', + cssClass: 'alert-success', + handler: () => { + this.navCtrl.navigateForward(['/services', 'marketplace', this.appId]) + }, + }, + ], + }) + await alert.present() + } + + async presentAlertUpToDate () { + const alert = await this.alertCtrl.create({ + header: 'Up To Date', + message: `You are running the latest version of ${this.app.title.getValue()}!`, + buttons: ['OK'], + }) + await alert.present() + } + + async copyTor () { + const app = peekProperties(this.app) + let message = '' + await copyToClipboard(app.torAddress || '').then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'}) + + const toast = await this.toastCtrl.create({ + header: message, + position: 'bottom', + duration: 1000, + cssClass: 'notification-toast', + }) + await toast.present() + } + + async stop (): Promise { + const app = peekProperties(this.app) + + await this.loader.of({ + message: `Stopping ${app.title}...`, + spinner: 'lines', + cssClass: 'loader', + }).displayDuringAsync(async () => { + const { breakages } = await this.apiService.stopApp(this.appId, true) + + if (breakages.length) { + const { cancelled } = await wizardModal( + this.modalCtrl, + this.wizardBaker.stop({ + id: app.id, + title: app.title, + version: app.versionInstalled, + breakages, + }), + ) + + if (cancelled) return { } + } + + return this.apiService.stopApp(this.appId).then(chill) + }).catch(e => this.setError(e)) + } + + async start (): Promise { + const app = peekProperties(this.app) + this.loader.of({ + message: `Starting ${app.title}...`, + spinner: 'lines', + cssClass: 'loader', + }).displayDuringP( + this.apiService.startApp(this.appId), + ).catch(e => this.setError(e)) + } + + async presentModalBackup (type: 'create' | 'restore') { + const modal = await this.modalCtrl.create({ + backdropDismiss: false, + component: AppBackupPage, + presentingElement: await this.modalCtrl.getTop(), + componentProps: { + app: peekProperties(this.app), + type, + }, + }) + + await modal.present() + } + + async presentAlertStopBackup (): Promise { + const app = peekProperties(this.app) + + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Warning', + message: `${app.title} is not finished backing up. Are you sure you want stop the process?`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Stop', + cssClass: 'alert-danger', + handler: () => { + this.stopBackup() + }, + }, + ], + }) + await alert.present() + } + + async stopBackup (): Promise { + await this.loader.of({ + message: `Stopping backup...`, + spinner: 'lines', + cssClass: 'loader', + }).displayDuringP(this.apiService.stopAppBackup(this.appId)) + .catch (e => this.setError(e)) + } + + async uninstall () { + const app = peekProperties(this.app) + + const data = await wizardModal( + this.modalCtrl, + this.wizardBaker.uninstall({ + id: app.id, + title: app.title, + version: app.versionInstalled, + }), + ) + + if (data.cancelled) return + return this.navCtrl.navigateRoot('/services/installed') + } + + async presentPopover (information: string, ev: any) { + const popover = await this.popoverController.create({ + component: InformationPopoverComponent, + event: ev, + translucent: false, + showBackdrop: true, + backdropDismiss: true, + componentProps: { + information, + }, + }) + return await popover.present() + } + + private setError (e: Error) { + this.$error$.next(e.message) + } + + private clearError () { + this.$error$.next('') + } + + private async scrollToElement (elementId: string) { + const el = document.getElementById(elementId) + + if (!el) return + + let y = el.offsetTop + return this.content.scrollToPoint(0, y, 1000) + } + + private syncWhenDependencyInstalls (): Observable { + return this.app.configuredRequirements.pipe( + filter(deps => !!deps), + switchMap(reqs => this.appModel.watchForInstallations(reqs)), + concatMap(() => markAsLoadingDuringP(this.$loadingDependencies$, this.getApp())), + catchError(e => of(console.error(e))), + ) + } +} diff --git a/ui/src/app/pages/apps-routes/app-instructions/app-instructions.module.ts b/ui/src/app/pages/apps-routes/app-instructions/app-instructions.module.ts new file mode 100644 index 000000000..c6e0276fa --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-instructions/app-instructions.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' + +import { AppInstructionsPage } from './app-instructions.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { SharingModule } from 'src/app/modules/sharing.module' + +const routes: Routes = [ + { + path: '', + component: AppInstructionsPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + SharingModule, + ], + declarations: [AppInstructionsPage], +}) +export class AppInstructionsPageModule { } diff --git a/ui/src/app/pages/apps-routes/app-instructions/app-instructions.page.html b/ui/src/app/pages/apps-routes/app-instructions/app-instructions.page.html new file mode 100644 index 000000000..4929bd390 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-instructions/app-instructions.page.html @@ -0,0 +1,31 @@ + + + + + + Instructions + + + + + + + + + + + + + + +

No instructions for {{ app.title }} {{ app.versionInstalled }}.

+
+
+ +
+
+
\ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-instructions/app-instructions.page.scss b/ui/src/app/pages/apps-routes/app-instructions/app-instructions.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/apps-routes/app-instructions/app-instructions.page.ts b/ui/src/app/pages/apps-routes/app-instructions/app-instructions.page.ts new file mode 100644 index 000000000..b1049a649 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-instructions/app-instructions.page.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { BehaviorSubject } from 'rxjs' +import { AppInstalledFull } from 'src/app/models/app-types' +import { ModelPreload } from 'src/app/models/model-preload' +import { markAsLoadingDuring$ } from 'src/app/services/loader.service' +import { peekProperties } from 'src/app/util/property-subject.util' + +@Component({ + selector: 'app-instructions', + templateUrl: './app-instructions.page.html', + styleUrls: ['./app-instructions.page.scss'], +}) +export class AppInstructionsPage { + $loading$ = new BehaviorSubject(true) + error = '' + app: AppInstalledFull = { } as any + appId: string + instructions: any + + constructor ( + private readonly route: ActivatedRoute, + private readonly preload: ModelPreload, + ) { } + + async ngOnInit () { + this.appId = this.route.snapshot.paramMap.get('appId') as string + + markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId)).subscribe({ + next: app => this.app = peekProperties(app), + error: e => { + console.error(e) + this.error = e.message + }, + }) + } +} diff --git a/ui/src/app/pages/apps-routes/app-logs/app-logs.module.ts b/ui/src/app/pages/apps-routes/app-logs/app-logs.module.ts new file mode 100644 index 000000000..0dcc71571 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-logs/app-logs.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' + +import { IonicModule } from '@ionic/angular' + +import { AppLogsPage } from './app-logs.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' + +const routes: Routes = [ + { + path: '', + component: AppLogsPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [AppLogsPage], +}) +export class AppLogsPageModule { } diff --git a/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html new file mode 100644 index 000000000..629d8c2ec --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.html @@ -0,0 +1,22 @@ + + + + + + Logs + + + + + + + + + + + + {{ error }} + + +

{{ logs }}

+
\ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-logs/app-logs.page.scss b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts new file mode 100644 index 000000000..883ee65ee --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-logs/app-logs.page.ts @@ -0,0 +1,50 @@ +import { Component, ViewChild } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { ApiService } from 'src/app/services/api/api.service' +import { IonContent } from '@ionic/angular' +import { pauseFor } from 'src/app/util/misc.util' +import { markAsLoadingDuringP } from 'src/app/services/loader.service' +import { BehaviorSubject } from 'rxjs' + +@Component({ + selector: 'app-logs', + templateUrl: './app-logs.page.html', + styleUrls: ['./app-logs.page.scss'], +}) +export class AppLogsPage { + @ViewChild(IonContent, { static: false }) private content: IonContent + $loading$ = new BehaviorSubject(true) + error = '' + appId: string + logs: string + + constructor ( + private readonly route: ActivatedRoute, + private readonly apiService: ApiService, + ) { } + + async ngOnInit () { + this.appId = this.route.snapshot.paramMap.get('appId') as string + + markAsLoadingDuringP(this.$loading$, Promise.all([ + this.getLogs(), + pauseFor(600), + ])) + } + + async getLogs () { + this.logs = '' + this.$loading$.next(true) + try { + const logs = await this.apiService.getAppLogs(this.appId) + this.logs = logs.join('\n\n') + this.error = '' + setTimeout(async () => await this.content.scrollToBottom(100), 200) + } catch (e) { + console.error(e) + this.error = e.message + } finally { + this.$loading$.next(false) + } + } +} diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts new file mode 100644 index 000000000..9ea5ca66f --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' + +import { IonicModule } from '@ionic/angular' + +import { AppMetricsPage } from './app-metrics.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { QRComponentModule } from 'src/app/components/qr/qr.component.module' +import { SharingModule } from 'src/app/modules/sharing.module' + +const routes: Routes = [ + { + path: '', + component: AppMetricsPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + QRComponentModule, + SharingModule, + ], + declarations: [AppMetricsPage], +}) +export class AppMetricsPageModule { } diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html new file mode 100644 index 000000000..ddb625d60 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.html @@ -0,0 +1,69 @@ + + + + + + Properties + + + + + + + + + + + + + + + + {{ error }} + + + + + +

No properties for {{ app.title }} {{ app.versionInstalled }}.

+
+
+ + +
+ + + + + + +

{{ keyval.key }}

+
+ +
+ + + + + + +

{{ keyval.key }}

+

{{ keyval.value.masked && !unmasked[keyval.key] ? (keyval.value.value | mask ) : (keyval.value.value | truncateEnd : 100) }}

+
+
+ + + + + + + + + +
+
+
+
+ +
+
\ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss new file mode 100644 index 000000000..eea898305 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.scss @@ -0,0 +1,3 @@ +.metric-note { + font-size: 16px; +} \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts new file mode 100644 index 000000000..86cfb7137 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts @@ -0,0 +1,139 @@ +import { Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { ApiService } from 'src/app/services/api/api.service' +import { pauseFor } from 'src/app/util/misc.util' +import { BehaviorSubject } from 'rxjs' +import { copyToClipboard } from 'src/app/util/web.util' +import { AlertController, NavController, PopoverController, ToastController } from '@ionic/angular' +import { AppMetrics } from 'src/app/util/metrics.util' +import { QRComponent } from 'src/app/components/qr/qr.component' +import { AppMetricStore } from './metric-store' +import * as JSONpointer from 'json-pointer' +import { ModelPreload } from 'src/app/models/model-preload' +import { markAsLoadingDuringP } from 'src/app/services/loader.service' +import { AppInstalledFull } from 'src/app/models/app-types' +import { peekProperties } from 'src/app/util/property-subject.util' + +@Component({ + selector: 'app-metrics', + templateUrl: './app-metrics.page.html', + styleUrls: ['./app-metrics.page.scss'], +}) +export class AppMetricsPage { + error = '' + $loading$ = new BehaviorSubject(true) + appId: string + pointer: string + qrCode: string + app: AppInstalledFull + $metrics$ = new BehaviorSubject({ }) + $hasMetrics$ = new BehaviorSubject(null) + unmasked: { [key: string]: boolean } = { } + + constructor ( + private readonly route: ActivatedRoute, + private readonly apiService: ApiService, + private readonly alertCtrl: AlertController, + private readonly toastCtrl: ToastController, + private readonly popoverCtrl: PopoverController, + private readonly metricStore: AppMetricStore, + private readonly navCtrl: NavController, + private readonly preload: ModelPreload, + ) { } + + ngOnInit () { + this.appId = this.route.snapshot.paramMap.get('appId') + this.pointer = this.route.queryParams['pointer'] + + markAsLoadingDuringP(this.$loading$, Promise.all([ + this.preload.appFull(this.appId).toPromise(), + this.getMetrics(), + pauseFor(600), + ])).then(([app]) => { + this.app = peekProperties(app) + this.metricStore.watch().subscribe(m => { + const metrics = JSONpointer.get(m, this.pointer || '') + this.$metrics$.next(metrics) + }) + this.$metrics$.subscribe(m => { + this.$hasMetrics$.next(!!Object.keys(m || { }).length) + }) + this.route.queryParams.subscribe(queryParams => { + if (queryParams['pointer'] === this.pointer) return + this.pointer = queryParams['pointer'] + const metrics = JSONpointer.get(this.metricStore.$metrics$.getValue(), this.pointer || '') + this.$metrics$.next(metrics) + }) + }) + } + + async doRefresh (event: any) { + await Promise.all([ + this.getMetrics(), + pauseFor(600), + ]) + event.target.complete() + } + + async getMetrics (): Promise { + try { + const metrics = await this.apiService.getAppMetrics(this.appId) + this.metricStore.update(metrics) + } catch (e) { + console.error(e) + this.error = e.message + } + } + + async presentDescription (metric: { key: string, value: AppMetrics[''] }, e: Event) { + e.stopPropagation() + + const alert = await this.alertCtrl.create({ + header: metric.key, + message: metric.value.description, + }) + await alert.present() + } + + async goToNested (key: string): Promise { + this.navCtrl.navigateForward(`/services/installed/${this.appId}/metrics`, { + queryParams: { + pointer: `${this.pointer || ''}/${key}/value`, + }, + }) + } + + async copy (text: string): Promise { + let message = '' + await copyToClipboard(text).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'}) + + const toast = await this.toastCtrl.create({ + header: message, + position: 'bottom', + duration: 1000, + cssClass: 'notification-toast', + }) + await toast.present() + } + + async showQR (text: string, ev: any): Promise { + const popover = await this.popoverCtrl.create({ + component: QRComponent, + cssClass: 'qr-popover', + event: ev, + componentProps: { + text, + }, + }) + return await popover.present() + } + + toggleMask (key: string) { + this.unmasked[key] = !this.unmasked[key] + console.log(this.unmasked) + } + + asIsOrder (a: any, b: any) { + return 0 + } +} diff --git a/ui/src/app/pages/apps-routes/app-metrics/metric-store.ts b/ui/src/app/pages/apps-routes/app-metrics/metric-store.ts new file mode 100644 index 000000000..c33841b57 --- /dev/null +++ b/ui/src/app/pages/apps-routes/app-metrics/metric-store.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core' +import { BehaviorSubject } from 'rxjs' +import { AppMetrics } from '../../../util/metrics.util' + +@Injectable({ + providedIn: 'root', +}) +export class AppMetricStore { + $metrics$: BehaviorSubject = new BehaviorSubject({ }) + watch () { return this.$metrics$.asObservable() } + + update (metrics: AppMetrics): void { + this.$metrics$.next(metrics) + } +} \ No newline at end of file diff --git a/ui/src/app/pages/apps-routes/apps-routing.module.ts b/ui/src/app/pages/apps-routes/apps-routing.module.ts new file mode 100644 index 000000000..952909371 --- /dev/null +++ b/ui/src/app/pages/apps-routes/apps-routing.module.ts @@ -0,0 +1,52 @@ +import { NgModule } from '@angular/core' +import { Routes, RouterModule } from '@angular/router' + +const routes: Routes = [ + { + path: '', + redirectTo: 'installed', + pathMatch: 'full', + }, + { + path: 'marketplace', + loadChildren: () => import('./app-available-list/app-available-list.module').then(m => m.AppAvailableListPageModule), + }, + { + path: 'installed', + loadChildren: () => import('./app-installed-list/app-installed-list.module').then(m => m.AppInstalledListPageModule), + }, + { + path: 'marketplace/:appId', + loadChildren: () => import('./app-available-show/app-available-show.module').then(m => m.AppAvailableShowPageModule), + }, + { + path: 'installed/:appId', + loadChildren: () => import('./app-installed-show/app-installed-show.module').then(m => m.AppInstalledShowPageModule), + }, + { + path: 'installed/:appId/instructions', + loadChildren: () => import('./app-instructions/app-instructions.module').then(m => m.AppInstructionsPageModule), + }, + { + path: 'installed/:appId/config', + loadChildren: () => import('./app-config/app-config.module').then(m => m.AppConfigPageModule), + }, + { + path: 'installed/:appId/config/:edit', + loadChildren: () => import('./app-config/app-config.module').then(m => m.AppConfigPageModule), + }, + { + path: 'installed/:appId/logs', + loadChildren: () => import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule), + }, + { + path: 'installed/:appId/metrics', + loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule), + }, +] + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AppsRoutingModule { } \ No newline at end of file diff --git a/ui/src/app/pages/authenticate/authenticate-routing.module.ts b/ui/src/app/pages/authenticate/authenticate-routing.module.ts new file mode 100644 index 000000000..eebaff215 --- /dev/null +++ b/ui/src/app/pages/authenticate/authenticate-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { AuthenticatePage } from './authenticate.page'; + +const routes: Routes = [ + { + path: '', + component: AuthenticatePage + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AuthenticatePageRoutingModule {} diff --git a/ui/src/app/pages/authenticate/authenticate.module.ts b/ui/src/app/pages/authenticate/authenticate.module.ts new file mode 100644 index 000000000..12bf60358 --- /dev/null +++ b/ui/src/app/pages/authenticate/authenticate.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import { AuthenticatePageRoutingModule } from './authenticate-routing.module'; +import { AuthenticatePage } from './authenticate.page'; +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'; +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + IonicModule, + AuthenticatePageRoutingModule, + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [AuthenticatePage], +}) +export class AuthenticatePageModule { } diff --git a/ui/src/app/pages/authenticate/authenticate.page.html b/ui/src/app/pages/authenticate/authenticate.page.html new file mode 100644 index 000000000..cdadc1a04 --- /dev/null +++ b/ui/src/app/pages/authenticate/authenticate.page.html @@ -0,0 +1,27 @@ + + + Login + + + + + + + +
+ + + + + + + + + {{ e }} + + + + Login + +
+
\ No newline at end of file diff --git a/ui/src/app/pages/authenticate/authenticate.page.scss b/ui/src/app/pages/authenticate/authenticate.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/authenticate/authenticate.page.ts b/ui/src/app/pages/authenticate/authenticate.page.ts new file mode 100644 index 000000000..7e9ec6123 --- /dev/null +++ b/ui/src/app/pages/authenticate/authenticate.page.ts @@ -0,0 +1,44 @@ +import { Component, OnInit } from '@angular/core' +import { AuthService } from '../../services/auth.service' +import { LoaderService } from '../../services/loader.service' +import { BehaviorSubject } from 'rxjs' +import { Router } from '@angular/router' + +@Component({ + selector: 'app-authenticate', + templateUrl: './authenticate.page.html', + styleUrls: ['./authenticate.page.scss'], +}) +export class AuthenticatePage implements OnInit { + password: string = '' + unmasked = false + $error$ = new BehaviorSubject(undefined) + + constructor ( + private readonly authStore: AuthService, + private readonly loader: LoaderService, + private readonly router: Router, + ) { } + + ngOnInit () { } + + ionViewDidEnter () { + this.$error$.next(undefined) + } + + toggleMask () { + this.unmasked = !this.unmasked + } + + async submitPassword () { + try { + await this.loader.displayDuringP( + this.authStore.login(this.password), + ) + this.password = '' + return this.router.navigate(['']) + } catch (e) { + this.$error$.next(e.message) + } + } +} diff --git a/ui/src/app/pages/notifications/notifications.module.ts b/ui/src/app/pages/notifications/notifications.module.ts new file mode 100644 index 000000000..79251f051 --- /dev/null +++ b/ui/src/app/pages/notifications/notifications.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { NotificationsPage } from './notifications.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' + +const routes: Routes = [ + { + path: '', + component: NotificationsPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [NotificationsPage], +}) +export class NotificationsPageModule { } diff --git a/ui/src/app/pages/notifications/notifications.page.html b/ui/src/app/pages/notifications/notifications.page.html new file mode 100644 index 000000000..3952950bd --- /dev/null +++ b/ui/src/app/pages/notifications/notifications.page.html @@ -0,0 +1,64 @@ + + + + + + Notifications + + + + + + + + + + + + + + {{ error }} + + + + + + + +

+ Notifications about Embassy and services will appear here. +

+
+
+
+ + + + + + + + + + +

+ {{ not.title }} +

+

{{ not.message }}

+

{{ not.createdAt | date: 'short' }}

+

+ {{ not.appId }} + - + Code: {{ not.code }} +

+
+
+
+
+ + + + + +
\ No newline at end of file diff --git a/ui/src/app/pages/notifications/notifications.page.scss b/ui/src/app/pages/notifications/notifications.page.scss new file mode 100644 index 000000000..d472cbe22 --- /dev/null +++ b/ui/src/app/pages/notifications/notifications.page.scss @@ -0,0 +1,3 @@ +.notification-message { + margin: 10px 0 12px 0; +} \ No newline at end of file diff --git a/ui/src/app/pages/notifications/notifications.page.ts b/ui/src/app/pages/notifications/notifications.page.ts new file mode 100644 index 000000000..fb52a5590 --- /dev/null +++ b/ui/src/app/pages/notifications/notifications.page.ts @@ -0,0 +1,98 @@ +import { Component } from '@angular/core' +import { ServerModel, S9Notification } from 'src/app/models/server-model' +import { ApiService } from 'src/app/services/api/api.service' +import { pauseFor } from 'src/app/util/misc.util' +import { LoaderService } from 'src/app/services/loader.service' + +@Component({ + selector: 'notifications', + templateUrl: 'notifications.page.html', + styleUrls: ['notifications.page.scss'], +}) +export class NotificationsPage { + error = '' + loading = true + notifications: S9Notification[] = [] + page = 1 + needInfinite = false + readonly perPage = 20 + + constructor ( + private readonly serverModel: ServerModel, + private readonly apiService: ApiService, + private readonly loader: LoaderService, + ) { } + + async ngOnInit () { + const [notifications] = await Promise.all([ + this.getNotifications(), + pauseFor(600), + ]) + this.notifications = notifications + this.serverModel.update({ badge: 0 }) + this.loading = false + } + + async doRefresh (e: any) { + this.page = 1 + await Promise.all([ + this.getNotifications(), + pauseFor(600), + ]) + e.target.complete() + } + + async doInfinite (e: any) { + const notifications = await this.getNotifications() + this.notifications = this.notifications.concat(notifications) + e.target.complete() + } + + async getNotifications (): Promise { + let notifications: S9Notification[] = [] + try { + notifications = await this.apiService.getNotifications(this.page, this.perPage) + this.needInfinite = notifications.length >= this.perPage + this.page++ + this.error = '' + } catch (e) { + console.error(e) + this.error = e.message + } finally { + return notifications + } + } + + getColor (notification: S9Notification): string { + const char = notification.code.charAt(0) + switch (char) { + case '0': + return 'primary' + case '1': + return 'success' + case '2': + return 'warning' + case '3': + return 'danger' + default: + return '' + } + } + + async remove (notificationId: string, index: number): Promise { + this.loader.of({ + message: 'Deleting...', + spinner: 'lines', + cssClass: 'loader', + }).displayDuringP( + this.apiService.deleteNotification(notificationId).then(() => { + this.notifications.splice(index, 1) + this.error = '' + }), + ).catch(e => { + console.error(e) + this.error = e.message + }) + } +} + diff --git a/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.module.ts b/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.module.ts new file mode 100644 index 000000000..92529432c --- /dev/null +++ b/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { DevOptionsPage } from './dev-options.page' +import { Routes, RouterModule } from '@angular/router' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module' + +const routes: Routes = [ + { + path: '', + component: DevOptionsPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + ObjectConfigComponentModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [DevOptionsPage], +}) +export class DevOptionsPageModule { } diff --git a/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.page.html b/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.page.html new file mode 100644 index 000000000..f273221c5 --- /dev/null +++ b/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.page.html @@ -0,0 +1,29 @@ + + + + + + Developer Options + + + + + + + + + + + + + + + SSH Keys + + + + + \ No newline at end of file diff --git a/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.page.scss b/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.page.ts b/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.page.ts new file mode 100644 index 000000000..39ae0287b --- /dev/null +++ b/ui/src/app/pages/server-routes/developer-routes/dev-options/dev-options.page.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core' +import { PropertySubject } from 'src/app/util/property-subject.util' +import { S9Server } from 'src/app/models/server-model' +import { ServerConfigService } from 'src/app/services/server-config.service' +import { ApiService } from 'src/app/services/api/api.service' +import { pauseFor } from 'src/app/util/misc.util' +import { LoaderService } from 'src/app/services/loader.service' +import { ModelPreload } from 'src/app/models/model-preload' + +@Component({ + selector: 'dev-options', + templateUrl: './dev-options.page.html', + styleUrls: ['./dev-options.page.scss'], +}) +export class DevOptionsPage { + server: PropertySubject = { } as any + + constructor ( + private readonly serverConfigService: ServerConfigService, + private readonly apiService: ApiService, + private readonly loader: LoaderService, + private readonly preload: ModelPreload, + ) { } + + ngOnInit () { + this.loader.displayDuring$( + this.preload.server(), + ).subscribe(s => this.server = s) + } + + async doRefresh (event: any) { + await Promise.all([ + this.apiService.getServer(), + pauseFor(600), + ]) + event.target.complete() + } + + async presentModalValueEdit (key: string): Promise { + await this.serverConfigService.presentModalValueEdit(key) + } +} diff --git a/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.module.ts b/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.module.ts new file mode 100644 index 000000000..a629bffe8 --- /dev/null +++ b/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { DevSSHKeysPage } from './dev-ssh-keys.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' + +const routes: Routes = [ + { + path: '', + component: DevSSHKeysPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [DevSSHKeysPage], +}) +export class DevSSHKeysPageModule { } diff --git a/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.page.html b/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.page.html new file mode 100644 index 000000000..9a8975c7f --- /dev/null +++ b/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.page.html @@ -0,0 +1,51 @@ + + + + + + SSH Keys + + + + + + + + + + + + + + {{ error }} + + + + Description + + +

Add SSH keys to your Embassy to gain root access from the command line.

+
+
+ + Saved Keys + + + + + + + + {{ fingerprint.alg }} {{ fingerprint.hash }} {{ fingerprint.hostname }} + + + +
+ + + + + + + +
\ No newline at end of file diff --git a/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.page.scss b/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.page.ts b/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.page.ts new file mode 100644 index 000000000..9c75484be --- /dev/null +++ b/ui/src/app/pages/server-routes/developer-routes/dev-ssh-keys/dev-ssh-keys.page.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core' +import { SSHFingerprint, S9Server } from 'src/app/models/server-model' +import { ApiService } from 'src/app/services/api/api.service' +import { pauseFor } from 'src/app/util/misc.util' +import { PropertySubject } from 'src/app/util/property-subject.util' +import { ServerConfigService } from 'src/app/services/server-config.service' +import { LoaderService } from 'src/app/services/loader.service' +import { ModelPreload } from 'src/app/models/model-preload' + +@Component({ + selector: 'dev-ssh-keys', + templateUrl: 'dev-ssh-keys.page.html', + styleUrls: ['dev-ssh-keys.page.scss'], +}) +export class DevSSHKeysPage { + server: PropertySubject = { } as any + error = '' + + constructor ( + private readonly apiService: ApiService, + private readonly loader: LoaderService, + private readonly preload: ModelPreload, + private readonly serverConfigService: ServerConfigService, + ) { } + + ngOnInit () { + this.loader.displayDuring$( + this.preload.server(), + ).subscribe(s => this.server = s) + } + + async doRefresh (event: any) { + await Promise.all([ + this.apiService.getServer(), + pauseFor(600), + ]) + event.target.complete() + } + + async presentModalAdd () { + await this.serverConfigService.presentModalValueEdit('ssh', true) + } + + async delete (fingerprint: SSHFingerprint) { + this.loader.of({ + message: 'Deleting...', + spinner: 'lines', + cssClass: 'loader', + }).displayDuringP( + this.apiService.deleteSSHKey(fingerprint).then(() => this.error = ''), + ).catch(e => { + console.error(e) + this.error = e.message + }) + } +} diff --git a/ui/src/app/pages/server-routes/developer-routes/developer-routing.module.ts b/ui/src/app/pages/server-routes/developer-routes/developer-routing.module.ts new file mode 100644 index 000000000..5ec809ba2 --- /dev/null +++ b/ui/src/app/pages/server-routes/developer-routes/developer-routing.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core' +import { Routes, RouterModule } from '@angular/router' + +const routes: Routes = [ + { + path: '', + loadChildren: () => import('./dev-options/dev-options.module').then(m => m.DevOptionsPageModule), + }, + { + path: 'ssh-keys', + loadChildren: () => import('./dev-ssh-keys/dev-ssh-keys.module').then(m => m.DevSSHKeysPageModule), + }, +] + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class DeveloperRoutingModule { } \ No newline at end of file diff --git a/ui/src/app/pages/server-routes/lan/lan.module.ts b/ui/src/app/pages/server-routes/lan/lan.module.ts new file mode 100644 index 000000000..f93ff781a --- /dev/null +++ b/ui/src/app/pages/server-routes/lan/lan.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { LANPage } from './lan.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { SharingModule } from 'src/app/modules/sharing.module' + +const routes: Routes = [ + { + path: '', + component: LANPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + SharingModule, + ], + declarations: [LANPage], +}) +export class LANPageModule { } diff --git a/ui/src/app/pages/server-routes/lan/lan.page.html b/ui/src/app/pages/server-routes/lan/lan.page.html new file mode 100644 index 000000000..ed3532f30 --- /dev/null +++ b/ui/src/app/pages/server-routes/lan/lan.page.html @@ -0,0 +1,67 @@ + + + + + + Secure LAN Setup + + + + + + + + + + + For a faster experience, you can also securely communicate with your Embassy by visiting its Local Area Network (LAN) address. + + + + + +

Instructions

+ +
    +
  • Download your Embassy's SSL Certificate Authority by clicking the download button below.
  • +
  • Install and trust the CA.
  • +
  • Connect this device to the same network as the Embassy. This should be your private home network.
  • +
  • Navigate to your Embassy LAN address, indicated below.
  • +
+
+
+

+
+ full documentation + full documentation + +
+
+ + + + + +

SSL Certificate

+

Embassy Local CA

+
+ + + +
+ + + +

LAN Address

+ {{ lanAddress }} +
+ + + +
+
+ + + + +
\ No newline at end of file diff --git a/ui/src/app/pages/server-routes/lan/lan.page.scss b/ui/src/app/pages/server-routes/lan/lan.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/server-routes/lan/lan.page.ts b/ui/src/app/pages/server-routes/lan/lan.page.ts new file mode 100644 index 000000000..93eaf4318 --- /dev/null +++ b/ui/src/app/pages/server-routes/lan/lan.page.ts @@ -0,0 +1,82 @@ +import { Component } from '@angular/core' +import { isPlatform, ToastController } from '@ionic/angular' +import { ServerModel } from 'src/app/models/server-model' +import { copyToClipboard } from 'src/app/util/web.util' +import { ConfigService } from 'src/app/services/config.service' + +@Component({ + selector: 'lan', + templateUrl: './lan.page.html', + styleUrls: ['./lan.page.scss'], +}) +export class LANPage { + torDocs = 'docs.privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion/user-manuals/embassyos/general/secure-lan' + lanDocs = 'docs.start9labs.com/user-manuals/embassyos/general/secure-lan' + + lanAddress: string + isTor: boolean + fullDocumentationLink: string + isConsulate: boolean + lanDisabled: LanSetupIssue = undefined + readonly lanDisabledExplanation: { [k in LanSetupIssue]: string } = { + NotDesktop: `We have detected you are on a mobile device. To setup LAN on a mobile device, use the Start9 Setup App.`, + NotTor: `We have detected you are not using a Tor connection. For security reasons, you must setup LAN over a Tor connection.

Navigate to your Embassy Tor Address and try again.`, + } + + constructor ( + private readonly serverModel: ServerModel, + private readonly toastCtrl: ToastController, + private readonly config: ConfigService, + ) { } + + ngOnInit () { + if (isPlatform('ios') || isPlatform('android')) { + this.lanDisabled = 'NotDesktop' + } else if (!this.config.isTor()) { + this.lanDisabled = 'NotTor' + } + + this.isConsulate = this.config.isConsulateIos || this.config.isConsulateAndroid + + if (this.config.isTor()) { + this.fullDocumentationLink = `http://${this.torDocs}` + } else { + this.fullDocumentationLink = `https://${this.lanDocs}` + } + + const server = this.serverModel.peek() + this.lanAddress = `https://${server.serverId}.local` + } + + async copyLAN (): Promise < void > { + const message = await copyToClipboard(this.lanAddress).then(success => success ? 'copied to clipboard!' : 'failed to copy') + + const toast = await this.toastCtrl.create({ + header: message, + position: 'bottom', + duration: 1000, + cssClass: 'notification-toast', + }) + await toast.present() + } + + async copyDocumentation (): Promise < void > { + const message = await copyToClipboard(this.fullDocumentationLink).then( + success => success ? 'copied documentation link to clipboard!' : 'failed to copy', + ) + + const toast = await this.toastCtrl.create({ + header: message, + position: 'bottom', + duration: 1000, + cssClass: 'notification-toast', + }) + await toast.present() + } + + installCert (): void { + document.getElementById('install-cert').click() + } +} + +type LanSetupIssue = 'NotTor' | 'NotDesktop' \ No newline at end of file diff --git a/ui/src/app/pages/server-routes/server-config/server-config.module.ts b/ui/src/app/pages/server-routes/server-config/server-config.module.ts new file mode 100644 index 000000000..3847521a3 --- /dev/null +++ b/ui/src/app/pages/server-routes/server-config/server-config.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { ServerConfigPage } from './server-config.page' +import { Routes, RouterModule } from '@angular/router' +import { SharingModule } from 'src/app/modules/sharing.module' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module' + +const routes: Routes = [ + { + path: '', + component: ServerConfigPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + SharingModule, + ObjectConfigComponentModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [ServerConfigPage], +}) +export class ServerConfigPageModule { } diff --git a/ui/src/app/pages/server-routes/server-config/server-config.page.html b/ui/src/app/pages/server-routes/server-config/server-config.page.html new file mode 100644 index 000000000..ae0f75bdd --- /dev/null +++ b/ui/src/app/pages/server-routes/server-config/server-config.page.html @@ -0,0 +1,29 @@ + + + + + + Config + + + + + + + + + + + + + + Device Name + {{ server.name | async }} + + + + + diff --git a/ui/src/app/pages/server-routes/server-config/server-config.page.scss b/ui/src/app/pages/server-routes/server-config/server-config.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/server-routes/server-config/server-config.page.ts b/ui/src/app/pages/server-routes/server-config/server-config.page.ts new file mode 100644 index 000000000..6d711519c --- /dev/null +++ b/ui/src/app/pages/server-routes/server-config/server-config.page.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core' +import { ServerConfigService } from 'src/app/services/server-config.service' +import { pauseFor } from 'src/app/util/misc.util' +import { NavController } from '@ionic/angular' +import { PropertySubject } from 'src/app/util/property-subject.util' +import { S9Server, ServerModel } from 'src/app/models/server-model' +import { ApiService } from 'src/app/services/api/api.service' + +@Component({ + selector: 'server-config', + templateUrl: './server-config.page.html', + styleUrls: ['./server-config.page.scss'], +}) +export class ServerConfigPage { + server: PropertySubject + + constructor ( + private readonly serverModel: ServerModel, + private readonly serverConfigService: ServerConfigService, + private readonly apiService: ApiService, + private readonly navController: NavController, + ) { } + + ngOnInit () { + this.server = this.serverModel.watch() + } + + async doRefresh (event: any) { + await Promise.all([ + this.apiService.getServer(), + pauseFor(600), + ]) + event.target.complete() + } + + async presentModalValueEdit (key: string, add = false): Promise { + await this.serverConfigService.presentModalValueEdit(key, add) + } + + navigateBack () { + this.navController.back() + } +} diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.module.ts b/ui/src/app/pages/server-routes/server-metrics/server-metrics.module.ts new file mode 100644 index 000000000..d73633272 --- /dev/null +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' + +import { IonicModule } from '@ionic/angular' + +import { ServerMetricsPage } from './server-metrics.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' + +const routes: Routes = [ + { + path: '', + component: ServerMetricsPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [ServerMetricsPage], +}) +export class ServerMetricsPageModule { } diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html new file mode 100644 index 000000000..63f6fbbc1 --- /dev/null +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html @@ -0,0 +1,32 @@ + + + + + + Metrics + + + + + + + + + {{ error }} + + + + + + {{ metricGroup.key }} + + + {{ metric.key }} + + + {{ metric.value.value }} {{ metric.value.unit }} + + + + + \ No newline at end of file diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.scss b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.scss new file mode 100644 index 000000000..eea898305 --- /dev/null +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.scss @@ -0,0 +1,3 @@ +.metric-note { + font-size: 16px; +} \ No newline at end of file diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts new file mode 100644 index 000000000..8b2e6660c --- /dev/null +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts @@ -0,0 +1,70 @@ +import { Component } from '@angular/core' +import { ServerMetrics } from 'src/app/models/server-model' +import { ApiService } from 'src/app/services/api/api.service' +import { pauseFor } from 'src/app/util/misc.util' + +@Component({ + selector: 'server-metrics', + templateUrl: './server-metrics.page.html', + styleUrls: ['./server-metrics.page.scss'], +}) +export class ServerMetricsPage { + error = '' + loading = true + going = false + metrics: ServerMetrics = { } + + constructor ( + private readonly apiService: ApiService, + ) { } + + async ngOnInit () { + await Promise.all([ + this.getMetrics(), + pauseFor(600), + ]) + + this.loading = false + + this.startDaemon() + } + + ngOnDestroy () { + this.stopDaemon() + } + + async startDaemon (): Promise { + this.going = true + while (this.going) { + await pauseFor(250) + await this.getMetrics() + } + } + + stopDaemon () { + this.going = false + } + + async getMetrics (): Promise { + try { + const metrics = await this.apiService.getServerMetrics() + Object.keys(metrics).forEach(outerKey => { + if (!this.metrics[outerKey]) { + this.metrics[outerKey] = metrics[outerKey] + } else { + Object.entries(metrics[outerKey]).forEach(([key, value]) => { + this.metrics[outerKey][key] = value + }) + } + }) + } catch (e) { + console.error(e) + this.error = e.message + this.stopDaemon() + } + } + + asIsOrder (a: any, b: any) { + return 0 + } +} diff --git a/ui/src/app/pages/server-routes/server-routing.module.ts b/ui/src/app/pages/server-routes/server-routing.module.ts new file mode 100644 index 000000000..c4cd29ec2 --- /dev/null +++ b/ui/src/app/pages/server-routes/server-routing.module.ts @@ -0,0 +1,47 @@ +import { NgModule } from '@angular/core' +import { Routes, RouterModule } from '@angular/router' +import { AuthGuard } from '../../guards/auth.guard' + +const routes: Routes = [ + { + path: '', + canActivate: [AuthGuard], + loadChildren: () => import('./server-show/server-show.module').then(m => m.ServerShowPageModule), + }, + { + path: 'specs', + canActivate: [AuthGuard], + loadChildren: () => import('./server-specs/server-specs.module').then(m => m.ServerSpecsPageModule), + }, + { + path: 'metrics', + canActivate: [AuthGuard], + loadChildren: () => import('./server-metrics/server-metrics.module').then(m => m.ServerMetricsPageModule), + }, + { + path: 'config', + canActivate: [AuthGuard], + loadChildren: () => import('./server-config/server-config.module').then(m => m.ServerConfigPageModule), + }, + { + path: 'wifi', + canActivate: [AuthGuard], + loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiListPageModule), + }, + { + path: 'lan', + canActivate: [AuthGuard], + loadChildren: () => import('./lan/lan.module').then(m => m.LANPageModule), + }, + { + path: 'developer', + canActivate: [AuthGuard], + loadChildren: () => import('./developer-routes/developer-routing.module').then( m => m.DeveloperRoutingModule), + }, +] + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class ServerRoutingModule { } \ No newline at end of file diff --git a/ui/src/app/pages/server-routes/server-show/server-show.module.ts b/ui/src/app/pages/server-routes/server-show/server-show.module.ts new file mode 100644 index 000000000..79cc2be2b --- /dev/null +++ b/ui/src/app/pages/server-routes/server-show/server-show.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { ServerShowPage } from './server-show.page' +import { StatusComponentModule } from 'src/app/components/status/status.component.module' +import { FormsModule } from '@angular/forms' +import { SharingModule } from 'src/app/modules/sharing.module' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' + +const routes: Routes = [ + { + path: '', + component: ServerShowPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + StatusComponentModule, + IonicModule, + RouterModule.forChild(routes), + SharingModule, + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [ServerShowPage], +}) +export class ServerShowPageModule { } diff --git a/ui/src/app/pages/server-routes/server-show/server-show.page.html b/ui/src/app/pages/server-routes/server-show/server-show.page.html new file mode 100644 index 000000000..7a93cb72c --- /dev/null +++ b/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -0,0 +1,86 @@ + + + {{ server.name | async }} + + + + + + + + + +
+ Server Updating + +
+
+
+ + + + + + + + + {{ error }} + + + + + + + + About + + + + + Monitor + + + + + Config + + + + + + + Check for Updates + + + + + + + Secure LAN Setup + + + + + WiFi + + + + + Developer Options + + + + + + + Restart + + + + + Shutdown + + + + +
\ No newline at end of file diff --git a/ui/src/app/pages/server-routes/server-show/server-show.page.scss b/ui/src/app/pages/server-routes/server-show/server-show.page.scss new file mode 100644 index 000000000..272d5c3dc --- /dev/null +++ b/ui/src/app/pages/server-routes/server-show/server-show.page.scss @@ -0,0 +1,12 @@ +.notification-button { + ion-badge { + position: absolute; + font-size: 8px; + bottom: .7rem; + left: .8rem; + } +} + +ion-item-divider { + margin-top: 0px; +} \ No newline at end of file diff --git a/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/ui/src/app/pages/server-routes/server-show/server-show.page.ts new file mode 100644 index 000000000..978f6002f --- /dev/null +++ b/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -0,0 +1,222 @@ +import { Component } from '@angular/core' +import { LoadingOptions } from '@ionic/core' +import { ServerModel, ServerStatus } from 'src/app/models/server-model' +import { AlertController } from '@ionic/angular' +import { S9Server } from 'src/app/models/server-model' +import { ApiService } from 'src/app/services/api/api.service' +import { SyncDaemon } from 'src/app/services/sync.service' +import { Subscription, Observable } from 'rxjs' +import { PropertySubject, toObservable } from 'src/app/util/property-subject.util' +import { doForAtLeast } from 'src/app/util/misc.util' +import { LoaderService } from 'src/app/services/loader.service' +import { Emver } from 'src/app/services/emver.service' + +@Component({ + selector: 'server-show', + templateUrl: 'server-show.page.html', + styleUrls: ['server-show.page.scss'], +}) +export class ServerShowPage { + error = '' + s9Host$: Observable + + server: PropertySubject + currentServer: S9Server + + subsToTearDown: Subscription[] = [] + + updatingFreeze = false + updating = false + + constructor ( + private readonly serverModel: ServerModel, + private readonly alertCtrl: AlertController, + private readonly loader: LoaderService, + private readonly apiService: ApiService, + private readonly syncDaemon: SyncDaemon, + private readonly emver: Emver, + ) { } + + async ngOnInit () { + this.server = this.serverModel.watch() + this.subsToTearDown.push( + // serverUpdateSubscription + this.server.status.subscribe(status => { + if (status === ServerStatus.UPDATING) { + this.updating = true + } else { + if (!this.updatingFreeze) { this.updating = false } + } + }), + // currentServerSubscription + toObservable(this.server).subscribe(currentServerProperties => { + this.currentServer = currentServerProperties + }), + ) + } + + ionViewDidEnter () { + this.error = '' + } + + ngOnDestroy () { + this.subsToTearDown.forEach(s => s.unsubscribe()) + } + + async doRefresh (event: any) { + await doForAtLeast([this.getServerAndApps()], 600) + event.target.complete() + } + + async getServerAndApps (): Promise { + try { + this.syncDaemon.sync() + this.error = '' + } catch (e) { + console.error(e) + this.error = e.message + } + } + + async checkForUpdates (): Promise { + const loader = await this.loader.ctrl.create(LoadingSpinner('Checking for updates...')) + await loader.present() + + try { + const { versionLatest } = await this.apiService.getVersionLatest() + if (this.emver.compare(this.server.versionInstalled.getValue(), versionLatest) === -1) { + this.presentAlertUpdate(versionLatest) + } else { + this.presentAlertUpToDate() + } + } catch (e) { + console.error(e) + this.error = e.message + } finally { + await loader.dismiss() + } + } + + async presentAlertUpToDate () { + const alert = await this.alertCtrl.create({ + header: 'Up To Date', + message: `You are running the latest version of EmbassyOS!`, + buttons: ['OK'], + }) + await alert.present() + } + + async presentAlertUpdate (versionLatest: string) { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Confirm', + message: `Update EmbassyOS to ${versionLatest}?`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Update', + handler: () => { + this.updateEmbassyOS(versionLatest) + }, + }, + ], + }) + await alert.present() + } + + async presentAlertRestart () { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Confirm', + message: `Are you sure you want to restart your Embassy?`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Restart', + cssClass: 'alert-danger', + handler: () => { + this.restart() + }, + }, + ]}, + ) + await alert.present() + } + + async presentAlertShutdown () { + const alert = await this.alertCtrl.create({ + backdropDismiss: false, + header: 'Confirm', + message: `Are you sure you shut down your Embassy? To turn it back on, you will need to physically unplug the device and plug it back in.`, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Shutdown', + cssClass: 'alert-danger', + handler: () => { + this.shutdown() + }, + }, + ], + }) + await alert.present() + } + + private async updateEmbassyOS (versionLatest: string) { + this.loader + .displayDuringAsync(async () => { + await this.apiService.updateAgent(versionLatest) + this.serverModel.update({ status: ServerStatus.UPDATING }) + // hides the "Update Embassy to..." button for this intance of the component + this.updatingFreeze = true + this.updating = true + setTimeout(() => this.updatingFreeze = false, 8000) + }) + .catch(e => this.setError(e)) + } + + private async restart () { + this.loader + .of(LoadingSpinner(`Restarting ${this.currentServer.name}...`)) + .displayDuringAsync( async () => { + this.serverModel.markUnreachable() + await this.apiService.restartServer() + }) + .catch(e => this.setError(e)) + } + + private async shutdown () { + this.loader + .of(LoadingSpinner(`Shutting down ${this.currentServer.name}...`)) + .displayDuringAsync( async () => { + this.serverModel.markUnreachable() + await this.apiService.shutdownServer() + }) + .catch(e => this.setError(e)) + } + + setError (e: Error) { + console.error(e) + this.error = e.message + } +} + +const LoadingSpinner: (m?: string) => LoadingOptions = (m) => { + const toMergeIn = m ? { message: m } : { } + return { + spinner: 'lines', + cssClass: 'loader', + ...toMergeIn, + } as LoadingOptions +} + + diff --git a/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts b/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts new file mode 100644 index 000000000..82b2c8d59 --- /dev/null +++ b/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' + +import { IonicModule } from '@ionic/angular' + +import { ServerSpecsPage } from './server-specs.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' +import { SharingModule } from 'src/app/modules/sharing.module' + +const routes: Routes = [ + { + path: '', + component: ServerSpecsPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + SharingModule, + ], + declarations: [ServerSpecsPage], +}) +export class ServerSpecsPageModule { } diff --git a/ui/src/app/pages/server-routes/server-specs/server-specs.page.html b/ui/src/app/pages/server-routes/server-specs/server-specs.page.html new file mode 100644 index 000000000..b41271e84 --- /dev/null +++ b/ui/src/app/pages/server-routes/server-specs/server-specs.page.html @@ -0,0 +1,33 @@ + + + + + + About + + + + + + + + + + + + {{ error }} + + + + + + +

{{ spec.key }}

+

{{ spec.value | displayEmver }}

+

{{ spec.value }}

+
+
+
+
+ +
\ No newline at end of file diff --git a/ui/src/app/pages/server-routes/server-specs/server-specs.page.scss b/ui/src/app/pages/server-routes/server-specs/server-specs.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts b/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts new file mode 100644 index 000000000..22f262f90 --- /dev/null +++ b/ui/src/app/pages/server-routes/server-specs/server-specs.page.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core' +import { S9Server } from 'src/app/models/server-model' + +import { ToastController } from '@ionic/angular' +import { copyToClipboard } from 'src/app/util/web.util' +import { PropertySubject } from 'src/app/util/property-subject.util' +import { ModelPreload } from 'src/app/models/model-preload' +import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service' +import { BehaviorSubject } from 'rxjs' + +@Component({ + selector: 'server-specs', + templateUrl: './server-specs.page.html', + styleUrls: ['./server-specs.page.scss'], +}) +export class ServerSpecsPage { + server: PropertySubject = { } as any + error = '' + $loading$ = new BehaviorSubject(true) + + constructor ( + private readonly toastCtrl: ToastController, + private readonly preload: ModelPreload, + ) { } + + async ngOnInit () { + markAsLoadingDuring$(this.$loading$, this.preload.server()).subscribe({ + next: s => this.server = s, + error: e => { + console.error(e) + this.error = e.message + }, + }) + } + + async copyTor () { + let message = '' + await copyToClipboard((this.server.specs.getValue()['Tor Address'] as string).trim() || '') + .then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'}) + + const toast = await this.toastCtrl.create({ + header: message, + position: 'bottom', + duration: 1000, + cssClass: 'notification-toast', + }) + await toast.present() + } + + asIsOrder (a: any, b: any) { + return 0 + } +} diff --git a/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.module.ts b/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.module.ts new file mode 100644 index 000000000..5e89b88fb --- /dev/null +++ b/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { FormsModule } from '@angular/forms' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { WifiAddPage } from './wifi-add.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' + +const routes: Routes = [ + { + path: '', + component: WifiAddPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [WifiAddPage], +}) +export class WifiAddPageModule { } diff --git a/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.page.html b/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.page.html new file mode 100644 index 000000000..724789978 --- /dev/null +++ b/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.page.html @@ -0,0 +1,52 @@ + + + + + + Add Network + + + + + + + + + + {{ error }} + + + + + Select Country + + + {{ country.key }} - {{ country.value }} + + + + Network and Password + + + + + + + + + + + + + Add + + + + + Add and Connect + + + + + + \ No newline at end of file diff --git a/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.page.scss b/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.page.ts b/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.page.ts new file mode 100644 index 000000000..47819424d --- /dev/null +++ b/ui/src/app/pages/server-routes/wifi/wifi-add/wifi-add.page.ts @@ -0,0 +1,67 @@ +import { Component } from '@angular/core' +import { NavController } from '@ionic/angular' +import { ApiService } from 'src/app/services/api/api.service' +import { WifiService } from '../wifi.service' +import { LoaderService } from 'src/app/services/loader.service' + +@Component({ + selector: 'wifi-add', + templateUrl: 'wifi-add.page.html', + styleUrls: ['wifi-add.page.scss'], +}) +export class WifiAddPage { + countries = require('../../../../util/countries.json') + countryCode = 'US' + ssid = '' + password = '' + error = '' + + constructor ( + private readonly navCtrl: NavController, + private readonly apiService: ApiService, + private readonly loader: LoaderService, + private readonly wifiService: WifiService, + ) { } + + async add (): Promise { + this.loader.of({ + message: 'Saving...', + spinner: 'lines', + cssClass: 'loader', + }).displayDuringAsync( async () => { + await this.apiService.addWifi(this.ssid, this.password, this.countryCode, false) + this.wifiService.addWifi(this.ssid) + this.ssid = '' + this.password = '' + this.error = '' + this.navCtrl.back() + }).catch(e => { + console.error(e) + this.error = e.message + }) + } + + async addAndConnect (): Promise { + this.loader.of({ + message: 'Connecting. This could take while...', + spinner: 'lines', + cssClass: 'loader', + }).displayDuringAsync( async () => { + await this.apiService.addWifi(this.ssid, this.password, this.countryCode, true) + const success = await this.wifiService.confirmWifi(this.ssid) + if (!success) { return } + this.wifiService.addWifi(this.ssid) + this.ssid = '' + this.password = '' + this.error = '' + this.navCtrl.back() + }).catch (e => { + console.error(e) + this.error = e.message + }) + } + + asIsOrder (a: any, b: any) { + return 0 + } +} diff --git a/ui/src/app/pages/server-routes/wifi/wifi.module.ts b/ui/src/app/pages/server-routes/wifi/wifi.module.ts new file mode 100644 index 000000000..ffd503c52 --- /dev/null +++ b/ui/src/app/pages/server-routes/wifi/wifi.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { WifiListPage } from './wifi.page' +import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module' +import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' + +const routes: Routes = [ + { + path: '', + component: WifiListPage, + }, + { + path: 'add', + loadChildren: () => import('./wifi-add/wifi-add.module').then(m => m.WifiAddPageModule), + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PwaBackComponentModule, + BadgeMenuComponentModule, + ], + declarations: [WifiListPage], +}) +export class WifiListPageModule { } diff --git a/ui/src/app/pages/server-routes/wifi/wifi.page.html b/ui/src/app/pages/server-routes/wifi/wifi.page.html new file mode 100644 index 000000000..283ea0889 --- /dev/null +++ b/ui/src/app/pages/server-routes/wifi/wifi.page.html @@ -0,0 +1,47 @@ + + + + + + Wifi + + + + + + + + + + + + + + {{ error }} + + + + + +

+ Add WiFi credentials to your Embassy so it can connect to the Internet without an ethernet cable. +

+
+
+ + + + Saved Networks + + {{ ssid }} + + +
+ + + + + + + +
\ No newline at end of file diff --git a/ui/src/app/pages/server-routes/wifi/wifi.page.scss b/ui/src/app/pages/server-routes/wifi/wifi.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/ui/src/app/pages/server-routes/wifi/wifi.page.ts new file mode 100644 index 000000000..7369d3991 --- /dev/null +++ b/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -0,0 +1,102 @@ +import { Component } from '@angular/core' +import { ActionSheetController } from '@ionic/angular' +import { ApiService } from 'src/app/services/api/api.service' +import { ActionSheetButton } from '@ionic/core' +import { pauseFor } from 'src/app/util/misc.util' +import { WifiService } from './wifi.service' +import { PropertySubject } from 'src/app/util/property-subject.util' +import { S9Server } from 'src/app/models/server-model' +import { LoaderService } from 'src/app/services/loader.service' +import { ModelPreload } from 'src/app/models/model-preload' + +@Component({ + selector: 'wifi', + templateUrl: 'wifi.page.html', + styleUrls: ['wifi.page.scss'], +}) +export class WifiListPage { + server: PropertySubject = { } as any + error: string + + constructor ( + private readonly apiService: ApiService, + private readonly loader: LoaderService, + private readonly actionCtrl: ActionSheetController, + private readonly wifiService: WifiService, + private readonly preload: ModelPreload, + ) { } + + async ngOnInit () { + this.loader.displayDuring$( + this.preload.server(), + ).subscribe(s => this.server = s) + } + + async doRefresh (event: any) { + await Promise.all([ + this.apiService.getServer(), + pauseFor(600), + ]) + event.target.complete() + } + + async presentAction (ssid: string) { + const buttons: ActionSheetButton[] = [ + { + text: 'Forget', + cssClass: 'alert-danger', + handler: () => { + this.delete(ssid) + }, + }, + ] + + if (ssid !== this.server.wifi.getValue().current) { + buttons.unshift( + { + text: 'Connect', + handler: () => { + this.connect(ssid) + }, + }, + ) + } + + const action = await this.actionCtrl.create({ + buttons, + }) + + await action.present() + } + + // Let's add country code here. + async connect (ssid: string): Promise { + this.loader.of({ + message: 'Connecting. This could take while...', + spinner: 'lines', + cssClass: 'loader', + }).displayDuringAsync(async () => { + await this.apiService.connectWifi(ssid) + await this.wifiService.confirmWifi(ssid) + this.error = '' + }).catch(e => { + console.error(e) + this.error = e.message + }) + } + + async delete (ssid: string): Promise { + this.loader.of({ + message: 'Deleting...', + spinner: 'lines', + cssClass: 'loader', + }).displayDuringAsync( async () => { + await this.apiService.deleteWifi(ssid) + this.wifiService.removeWifi(ssid) + this.error = '' + }).catch(e => { + console.error(e) + this.error = e.message + }) + } +} diff --git a/ui/src/app/pages/server-routes/wifi/wifi.service.ts b/ui/src/app/pages/server-routes/wifi/wifi.service.ts new file mode 100644 index 000000000..bba95503c --- /dev/null +++ b/ui/src/app/pages/server-routes/wifi/wifi.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@angular/core' +import { ToastController } from '@ionic/angular' +import { ApiService } from 'src/app/services/api/api.service' +import { pauseFor } from 'src/app/util/misc.util' +import { ServerModel } from 'src/app/models/server-model' + +@Injectable({ + providedIn: 'root', +}) +export class WifiService { + + constructor ( + private readonly apiService: ApiService, + private readonly toastCtrl: ToastController, + private readonly serverModel: ServerModel, + ) { } + + addWifi (ssid: string): void { + const wifi = this.serverModel.peek().wifi + this.serverModel.update({ wifi: { ...wifi, ssids: [...new Set([ssid, ...wifi.ssids])] } }) + } + + removeWifi (ssid: string): void { + const wifi = this.serverModel.peek().wifi + this.serverModel.update({ wifi: { ...wifi, ssids: wifi.ssids.filter(s => s !== ssid) } }) + } + + async confirmWifi (ssid: string): Promise { + const timeout = 4000 + const maxAttempts = 5 + let attempts = 0 + + while (attempts < maxAttempts) { + try { + const start = new Date().valueOf() + const { current, ssids } = (await this.apiService.getServer(timeout)).wifi + const end = new Date().valueOf() + if (current === ssid) { + this.serverModel.update({ wifi: { current, ssids } }) + break + } else { + attempts++ + const diff = end - start + await pauseFor(Math.max(0, timeout - diff)) + if (attempts === maxAttempts) { + this.serverModel.update({ wifi: { current, ssids } }) + } + } + } catch (e) { + attempts++ + console.error(e) + } + } + + if (this.serverModel.peek().wifi.current === ssid) { + return true + } else { + const toast = await this.toastCtrl.create({ + header: 'Failed to connect:', + message: `Check credentials and try again`, + position: 'bottom', + duration: 4000, + buttons: [ + { + side: 'start', + icon: 'close', + handler: () => { + return true + }, + }, + ], + cssClass: 'notification-toast', + }) + + setTimeout(() => toast.present(), 300) + + return false + } + } +} diff --git a/ui/src/app/pipes/annotation-status.pipe.ts b/ui/src/app/pipes/annotation-status.pipe.ts new file mode 100644 index 000000000..016c6ed05 --- /dev/null +++ b/ui/src/app/pipes/annotation-status.pipe.ts @@ -0,0 +1,33 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { Annotation } from '../app-config/config-utilities' + +@Pipe({ + name: 'annotationStatus', +}) +export class AnnotationStatusPipe implements PipeTransform { + transform (a: Annotation, target: FieldStatus): boolean { + return target === getStatus(a) + } +} + +function getStatus (a: Annotation): FieldStatus { + if (isInvalid(a)) return 'Invalid' + if (isEdited(a)) return 'Edited' + if (isAdded(a)) return 'Added' + return 'NoChange' +} + +function isInvalid (a: Annotation): boolean { + return !!a.invalid +} + +// edited only registers if its a valid edit +function isEdited (a: Annotation): boolean { + return a.edited && !a.invalid +} + +function isAdded (a: Annotation): boolean { + return a.added && !a.edited && !a.invalid +} + +type FieldStatus = 'Edited' | 'Added' | 'Invalid' | 'NoChange' diff --git a/ui/src/app/pipes/display-bulb.pipe.ts b/ui/src/app/pipes/display-bulb.pipe.ts new file mode 100644 index 000000000..2e6dbbe69 --- /dev/null +++ b/ui/src/app/pipes/display-bulb.pipe.ts @@ -0,0 +1,21 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { AppStatus } from '../models/app-model' +import { AppStatusRendering } from '../util/status-rendering' + +@Pipe({ + name: 'displayBulb', +}) +export class DisplayBulbPipe implements PipeTransform { + + transform (status: AppStatus, d: DisplayBulb): boolean { + switch (AppStatusRendering[status].color) { + case 'danger': return d === 'red' + case 'success': return d === 'green' + case 'warning': return d === 'yellow' + default: return d === 'off' + } + } + +} + +type DisplayBulb = 'off' | 'red' | 'green' | 'yellow' diff --git a/ui/src/app/pipes/emver.pipe.ts b/ui/src/app/pipes/emver.pipe.ts new file mode 100644 index 000000000..cbff391de --- /dev/null +++ b/ui/src/app/pipes/emver.pipe.ts @@ -0,0 +1,64 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { Emver } from '../services/emver.service' +@Pipe({ + name: 'satisfiesEmver', +}) +export class EmverSatisfiesPipe implements PipeTransform { + constructor (private readonly emver: Emver) { } + + transform (versionUnderTest: string, range: string): boolean { + return this.emver.satisfies(versionUnderTest, range) + } +} + +@Pipe({ + name: 'compareEmver', +}) +export class EmverComparesPipe implements PipeTransform { + constructor (private readonly emver: Emver) { } + + transform (first: string, second: string): SemverResult { + try { + return this.emver.compare(first, second) as SemverResult + } catch (e) { + console.warn(`emver comparison failed`, e, first, second) + return 'comparison-impossible' + } + } +} +type SemverResult = 0 | 1 | -1 | 'comparison-impossible' + +@Pipe({ + name: 'displayEmver', +}) +export class EmverDisplayPipe implements PipeTransform { + constructor () { } + + transform (version: string): string { + return displayEmver(version) + } +} + +@Pipe({ + name: 'isValidEmver', +}) +export class EmverIsValidPipe implements PipeTransform { + constructor () { } + + transform (version: string): boolean { + return isValidEmver(version) + } +} + +export function isValidEmver (version: string): boolean { + const vs = version.split('.') + if (vs.length < 3 || vs.length > 5) return false + if (!vs.every(v => !isNaN(parseFloat(v)))) return false + return true +} + +export function displayEmver (version: string): string { + const vs = version.split('.') + if (vs.length === 4) return `${vs[0]}.${vs[1]}.${vs[2]}~${vs[3]}` + return version +} \ No newline at end of file diff --git a/ui/src/app/pipes/icon.pipe.ts b/ui/src/app/pipes/icon.pipe.ts new file mode 100644 index 000000000..dba6711cd --- /dev/null +++ b/ui/src/app/pipes/icon.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core' + +@Pipe({ + name: 'iconParse', +}) +export class IconPipe implements PipeTransform { + transform (iconUrl: string): string { + if (iconUrl.startsWith('/')) return '/api' + iconUrl + return iconUrl + } +} diff --git a/ui/src/app/pipes/includes.pipe.ts b/ui/src/app/pipes/includes.pipe.ts new file mode 100644 index 000000000..95cd109b4 --- /dev/null +++ b/ui/src/app/pipes/includes.pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core' + +@Pipe({ + name: 'includes', +}) +export class IncludesPipe implements PipeTransform { + transform (set: T[], val: T): boolean { + return set.includes(val) + } +} \ No newline at end of file diff --git a/ui/src/app/pipes/installed-latest-comparison.pipe.ts b/ui/src/app/pipes/installed-latest-comparison.pipe.ts new file mode 100644 index 000000000..7e1fda7ab --- /dev/null +++ b/ui/src/app/pipes/installed-latest-comparison.pipe.ts @@ -0,0 +1,46 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { combineLatest, Observable } from 'rxjs' +import { map } from 'rxjs/operators' +import { AppAvailableFull, AppAvailablePreview } from 'src/app/models/app-types' +import { Emver } from '../services/emver.service' +import { PropertySubject } from '../util/property-subject.util' + +@Pipe({ + name: 'compareInstalledAndLatest', +}) +export class InstalledLatestComparisonPipe implements PipeTransform { + constructor (private readonly emver: Emver) { } + + transform (app: PropertySubject): Observable<'not-installed' | 'installed-below' | 'installed-above' | 'installed-equal'> { + return combineLatest([app.versionInstalled, app.versionLatest]).pipe( + map(([i, l]) => { + if (!i) return 'not-installed' + switch (this.emver.compare(i, l)){ + case 0: return 'installed-equal' + case 1: return 'installed-above' + case -1: return 'installed-below' + } + }), + ) + } +} + +@Pipe({ + name: 'compareInstalledAndViewing', +}) +export class InstalledViewingComparisonPipe implements PipeTransform { + constructor (private readonly emver: Emver) { } + + transform (app: PropertySubject): Observable<'not-installed' | 'installed-below' | 'installed-above' | 'installed-equal'> { + return combineLatest([app.versionInstalled, app.versionViewing]).pipe( + map(([i, l]) => { + if (!i) return 'not-installed' + switch (this.emver.compare(i, l)){ + case 0: return 'installed-equal' + case 1: return 'installed-above' + case -1: return 'installed-below' + } + }), + ) + } +} diff --git a/ui/src/app/pipes/markdown.pipe.ts b/ui/src/app/pipes/markdown.pipe.ts new file mode 100644 index 000000000..3b4805b19 --- /dev/null +++ b/ui/src/app/pipes/markdown.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core' +import * as marked from 'marked' + +@Pipe({ + name: 'markdown', +}) +export class MarkdownPipe implements PipeTransform { + transform (value: any): any { + if (value && value.length > 0) { + return marked(value) + } + return value + } +} diff --git a/ui/src/app/pipes/mask.pipe.ts b/ui/src/app/pipes/mask.pipe.ts new file mode 100644 index 000000000..143017beb --- /dev/null +++ b/ui/src/app/pipes/mask.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core' + +@Pipe({ + name: 'mask', +}) +export class MaskPipe implements PipeTransform { + transform (val: string, max = 16): string { + if (!val) return val + const times = val.length <= max ? val.length : max + return '●'.repeat(times) + } +} \ No newline at end of file diff --git a/ui/src/app/pipes/peek-properties.pipe.ts b/ui/src/app/pipes/peek-properties.pipe.ts new file mode 100644 index 000000000..901fff628 --- /dev/null +++ b/ui/src/app/pipes/peek-properties.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { peekProperties, PropertySubject } from '../util/property-subject.util' + +@Pipe({ + name: 'peekProperties', +}) +export class PeekPropertiesPipe implements PipeTransform { + transform (value: PropertySubject): T { + return peekProperties(value) + } +} diff --git a/ui/src/app/pipes/truncate.pipe.ts b/ui/src/app/pipes/truncate.pipe.ts new file mode 100644 index 000000000..9a76a428f --- /dev/null +++ b/ui/src/app/pipes/truncate.pipe.ts @@ -0,0 +1,32 @@ +import { Pipe, PipeTransform } from '@angular/core' + +@Pipe({ + name: 'truncateCenter', +}) +export class TruncateCenterPipe implements PipeTransform { + transform (value: string, front: number, back: number, fullOnDesktop: boolean = false): unknown { + if (value.length <= front + back + 3) return value + if (fullOnDesktop && screen.width > 500) return value + return value.slice(0, front) + '...' + value.slice(value.length - back, value.length) + } +} + +@Pipe({ + name: 'truncateEnd', +}) +export class TruncateEndPipe implements PipeTransform { + transform (val: string, length: number): unknown { + if (val.length <= length) return val + return val.slice(0, length) + '...' + } +} + + +// 4 and 4 + +// 12345678 => 12345678 +// 123456789 => 123456789 +// 1234567890 => 1234567890 +// 12345678901 => 12345678901 +// 1234...9012 => 1234...9012 +// 1234...0123 => 1234...0123 \ No newline at end of file diff --git a/ui/src/app/pipes/typeof.pipe.ts b/ui/src/app/pipes/typeof.pipe.ts new file mode 100644 index 000000000..6881690bf --- /dev/null +++ b/ui/src/app/pipes/typeof.pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core' + +@Pipe({ + name: 'typeof', +}) +export class TypeofPipe implements PipeTransform { + transform (value: any): any { + return typeof value + } +} \ No newline at end of file diff --git a/ui/src/app/services/api/API.def b/ui/src/app/services/api/API.def new file mode 100644 index 000000000..eeb0ca4cd --- /dev/null +++ b/ui/src/app/services/api/API.def @@ -0,0 +1,107 @@ + +//////////////// Install/Uninstall //////////////////////////////////////////////// + +type AppDependentBreakage = { + // id of the dependent app which will or did break (Stopped) given the action. + id: string + title: string + iconUrl: string +} + +POST /apps/:appId/install(?dryrun) + +body: { + version: string, //semver +} +response : ApiAppInstalledFull & { breakages: AppDependentBreakage[] } + + + +POST /apps/:appId/uninstall(?dryrun) + +response : { breakages: AppDependentBreakage[] } + +/////////////////////////////// Store/Show ///////////////////////////////////////////////// + + +type ApiAppAvailableFull = ... { + // app base data + id: string + title: string + status: AppStatus | null + versionInstalled: string | null + iconURL: string + + // preview data + versionLatest: string + descriptionShort: string + + // version specific data + releaseNotes: string + serviceRequirements: AppDependencyRequirement[] + + // other data + descriptionLong: string, + version: string[], +} + +type AppDependencyRequirement = ... { + //app base data (minus status + version installed) + id: string + title: string + iconURL: string + + // dependency data + optional: string | null + default: boolean + versionSpec: string + description: string | null + violation: AppDependencyRequirementViolation | null +} + +type AppDependencyRequirementViolation = + { name: 'missing'; suggestedVersion: string; } | + { name: 'incompatible-version'; suggestedVersion: string; } | + { name: 'incompatible-config'; ruleViolations: string; auto-configurable: boolean } | // (auto-configurable for if/when we do that) + { name: 'incompatible-status'; status: AppStatus; } + + +// Get App Available Full +GET /apps/:appId/store + +response: ApiAppAvailableFull + + +// Get Version Specific Data for an App Available +GET /apps/:appId/store/:version + +response: { + // version specific data + releaseNotes: string + serviceRequirements: AppDependencyRequirement[] +} + +///////////////////////////// Installed/Show /////////////////////////////////////////// + + +type ApiAppInstalledFull { + // app base data + id: string + title: string + status: AppStatus | null + versionInstalled: string | null + iconURL: string + + // preview data + + // other data + instructions: string | null + lastBackup: string | null + configuredRequirements: AppDependencyRequirement[] | null // null if not yet configured +} + + +// Get App Installed Full +GET /apps/:appId/installed + +reseponse: AppInstalledFull \ No newline at end of file diff --git a/ui/src/app/services/api/api-types.ts b/ui/src/app/services/api/api-types.ts new file mode 100644 index 000000000..daef2f32a --- /dev/null +++ b/ui/src/app/services/api/api-types.ts @@ -0,0 +1,32 @@ +import { ConfigSpec } from 'src/app/app-config/config-types' +import { AppAvailableFull, AppInstalledFull } from 'src/app/models/app-types' +import { Rules } from '../../models/app-model' +import { SSHFingerprint, ServerStatus, ServerSpecs } from '../../models/server-model' + +/** SERVER **/ + +export interface ApiServer { + name: string + status: ServerStatus + versionInstalled: string + alternativeRegistryUrl: string | null + specs: ServerSpecs + wifi: { ssids: string[]; current: string; } + ssh: SSHFingerprint[] + serverId: string +} + +/** APPS **/ +export type ApiAppAvailableFull = Omit +export type ApiAppInstalledFull = Omit + +export interface ApiAppConfig { + spec: ConfigSpec + config: object | null + rules: Rules[] +} + +/** MISC **/ + +export type Unit = { never?: never; } // hack for the unit typ + diff --git a/ui/src/app/services/api/api.service.factory.ts b/ui/src/app/services/api/api.service.factory.ts new file mode 100644 index 000000000..d0db9a798 --- /dev/null +++ b/ui/src/app/services/api/api.service.factory.ts @@ -0,0 +1,14 @@ +import { HttpService } from '../http.service' +import { AppModel } from '../../models/app-model' +import { MockApiService } from './mock-api.service' +import { LiveApiService } from './live-api.service' +import { ServerModel } from 'src/app/models/server-model' +import { ConfigService } from '../config.service' + +export function ApiServiceFactory (config: ConfigService, http: HttpService, appModel: AppModel, serverModel: ServerModel) { + if (config.api.useMocks) { + return new MockApiService(appModel, serverModel) + } else { + return new LiveApiService(http, appModel, serverModel) + } +} diff --git a/ui/src/app/services/api/api.service.ts b/ui/src/app/services/api/api.service.ts new file mode 100644 index 000000000..c0df7176e --- /dev/null +++ b/ui/src/app/services/api/api.service.ts @@ -0,0 +1,97 @@ +import { Rules } from '../../models/app-model' +import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo } from '../../models/app-types' +import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model' +import { Subject, Observable } from 'rxjs' +import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull } from './api-types' +import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util' +import { ConfigSpec } from 'src/app/app-config/config-types' + +export abstract class ApiService { + private $unauthorizedApiResponse$: Subject<{ }> = new Subject() + + watch401$ (): Observable<{ }> { + return this.$unauthorizedApiResponse$.asObservable() + } + + authenticatedRequestsEnabled: boolean = false + + protected received401 () { + this.authenticatedRequestsEnabled = false + this.$unauthorizedApiResponse$.next() + } + + abstract getCheckAuth (): Promise // Throws an error on failed auth. + abstract postLogin (password: string): Promise // Throws an error on failed auth. + abstract postLogout (): Promise // Throws an error on failed auth. + abstract getServer (timeout?: number): Promise + abstract getVersionLatest (): Promise + abstract getServerMetrics (): Promise + abstract getNotifications (page: number, perPage: number): Promise + abstract deleteNotification (id: string): Promise + abstract updateAgent (thing: any): Promise + abstract getAvailableApps (): Promise + abstract getAvailableApp (appId: string): Promise + abstract getAvailableAppVersionSpecificInfo (appId: string, versionSpec: string): Promise + abstract getInstalledApp (appId: string): Promise + abstract getAppMetrics (appId: string): Promise + abstract getInstalledApps (): Promise + abstract getExternalDisks (): Promise + abstract getAppConfig (appId: string): Promise<{ spec: ConfigSpec, config: object, rules: Rules[]}> + abstract getAppLogs (appId: string, params?: ReqRes.GetAppLogsReq): Promise + abstract installApp (appId: string, version: string, dryRun?: boolean): Promise + abstract uninstallApp (appId: string, dryRun?: boolean): Promise<{ breakages: DependentBreakage[] }> + abstract startApp (appId: string): Promise + abstract stopApp (appId: string, dryRun?: boolean): Promise<{ breakages: DependentBreakage[] }> + abstract restartApp (appId: string): Promise + abstract createAppBackup (appId: string, logicalname: string, password?: string): Promise + abstract restoreAppBackup (appId: string, logicalname: string, password?: string): Promise + abstract stopAppBackup (appId: string): Promise + abstract patchAppConfig (app: AppInstalledPreview, config: object, dryRun?: boolean): Promise<{ breakages: DependentBreakage[] }> + abstract postConfigureDependency(dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{config: object, breakages: DependentBreakage[] }> + abstract patchServerConfig (attr: string, value: any): Promise + abstract wipeAppData (app: AppInstalledPreview): Promise + abstract addSSHKey (sshKey: string): Promise + abstract deleteSSHKey (sshKey: SSHFingerprint): Promise + abstract addWifi (ssid: string, password: string, country: string, connect: boolean): Promise + abstract connectWifi (ssid: string): Promise + abstract deleteWifi (ssid: string): Promise + abstract restartServer (): Promise + abstract shutdownServer (): Promise +} + +export module ReqRes { + export type GetVersionRes = { version: string } + export type PostLoginReq = { password: string } + export type PostLoginRes = Unit + export type GetCheckAuthRes = { } + export type GetServerRes = ApiServer + export type GetVersionLatestRes = { versionLatest: string, canUpdate: boolean } + export type GetServerMetricsRes = ServerMetrics + export type GetAppAvailableRes = ApiAppAvailableFull + export type GetAppAvailableVersionInfoRes = AppAvailableVersionSpecificInfo + export type GetAppsAvailableRes = AppAvailablePreview[] + export type GetExternalDisksRes = DiskInfo[] + export type GetAppInstalledRes = ApiAppInstalledFull + export type GetAppConfigRes = ApiAppConfig + export type GetAppLogsReq = { after?: string, before?: string, page?: string, perPage?: string } + export type GetAppLogsRes = string[] + export type GetAppMetricsRes = AppMetricsVersioned + export type GetAppsInstalledRes = AppInstalledPreview[] + export type PostInstallAppReq = { version: string } + export type PostInstallAppRes = ApiAppInstalledFull & { breakages: DependentBreakage[] } + export type PostUpdateAgentReq = { version: string } + export type PostAppBackupCreateReq = { logicalname: string, password: string } + export type PostAppBackupCreateRes = Unit + export type PostAppBackupRestoreReq = { logicalname: string, password: string } + export type PostAppBackupRestoreRes = Unit + export type PostAppBackupStopRes = Unit + export type PatchAppConfigReq = { config: object } + export type PatchServerConfigReq = { value: string } + export type GetNotificationsReq = { page: string, perPage: string } + export type GetNotificationsRes = S9Notification[] + export type PostAddWifiReq = { ssid: string, password: string, country: string, skipConnect: boolean } + export type PostConnectWifiReq = { country: string } + export type PostAddSSHKeyReq = { sshKey: string } + export type PostAddSSHKeyRes = SSHFingerprint +} + diff --git a/ui/src/app/services/api/live-api.service.ts b/ui/src/app/services/api/live-api.service.ts new file mode 100644 index 000000000..f1887a1f9 --- /dev/null +++ b/ui/src/app/services/api/live-api.service.ts @@ -0,0 +1,257 @@ +import { Injectable } from '@angular/core' +import { HttpService, Method, HttpOptions } from '../http.service' +import { AppModel, AppStatus } from '../../models/app-model' +import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo } from '../../models/app-types' +import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model' +import { ApiService, ReqRes } from './api.service' +import { ApiServer, Unit } from './api-types' +import { HttpErrorResponse } from '@angular/common/http' +import { isUnauthorized } from 'src/app/util/web.util' +import { Replace } from 'src/app/util/types.util' +import { AppMetrics, parseMetricsPermissive } from 'src/app/util/metrics.util' + +@Injectable() +export class LiveApiService extends ApiService { + constructor ( + private readonly http: HttpService, + // TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel + private readonly appModel: AppModel, + private readonly serverModel: ServerModel, + ) { super() } + + // Used to check whether password or key is valid. If so, it will be used implicitly by all other calls. + async getCheckAuth (): Promise { + return this.http.serverRequest({ method: Method.GET, url: '/authenticate' }, { version: '' }) + } + + async postLogin (password: string): Promise { + return this.http.serverRequest({ method: Method.POST, url: '/auth/login', data: { password } }, { version: '' }) + } + + async postLogout (): Promise { + return this.http.serverRequest({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return { } }) + } + + async getServer (timeout?: number): Promise { + return this.authRequest({ method: Method.GET, url: '/', readTimeout: timeout }) + } + + async getVersionLatest (): Promise { + return this.authRequest({ method: Method.GET, url: '/versionLatest' }, { version: '' }) + } + + async getServerMetrics (): Promise { + return this.authRequest({ method: Method.GET, url: `/metrics` }) + } + + async getNotifications (page: number, perPage: number): Promise { + const params: ReqRes.GetNotificationsReq = { + page: String(page), + perPage: String(perPage), + } + return this.authRequest({ method: Method.GET, url: `/notifications`, params }) + } + + async deleteNotification (id: string): Promise { + return this.authRequest({ method: Method.DELETE, url: `/notifications/${id}` }) + } + + async getExternalDisks (): Promise { + return this.authRequest({ method: Method.GET, url: `/disks` }) + } + + async updateAgent (version: string): Promise { + const data: ReqRes.PostUpdateAgentReq = { + version: `=${version}`, + } + return this.authRequest({ method: Method.POST, url: '/update', data }) + } + + async getAvailableAppVersionSpecificInfo (appId: string, versionSpec: string): Promise { + return this + .authRequest>( { method: Method.GET, url: `/apps/${appId}/store/${versionSpec}` }) + .then( res => ({ ...res, versionViewing: res.version })) + .then( res => { + delete res['version'] + return res + }) + } + + async getAvailableApps (): Promise { + return this.authRequest({ method: Method.GET, url: '/apps/store' }) + } + + async getAvailableApp (appId: string): Promise { + return this.authRequest({ method: Method.GET, url: `/apps/${appId}/store` }) + .then(res => { + return { + ...res, + versionViewing: res.versionLatest, + } + }) + } + + async getInstalledApp (appId: string): Promise { + return this.authRequest({ method: Method.GET, url: `/apps/${appId}/installed` }) + .then(app => ({ ...app, hasFetchedFull: true })) + } + + async getInstalledApps (): Promise { + return this.authRequest({ method: Method.GET, url: `/apps/installed` }) + } + + async getAppConfig ( appId: string): Promise { + return this.authRequest({ method: Method.GET, url: `/apps/${appId}/config` }) + } + + async getAppLogs (appId: string, params: ReqRes.GetAppLogsReq = { }): Promise { + return this.authRequest( { method: Method.GET, url: `/apps/${appId}/logs`, params: params as any }) + } + + async getAppMetrics (appId: string): Promise { + return this.authRequest( { method: Method.GET, url: `/apps/${appId}/metrics` }) + .then(parseMetricsPermissive) + } + + async installApp (appId: string, version: string, dryRun: boolean = false): Promise { + const data: ReqRes.PostInstallAppReq = { + version, + } + return this.authRequest({ method: Method.POST, url: `/apps/${appId}/install${dryRunParam(dryRun, true)}`, data }) + .then(res => ({ ...res, hasFetchedFull: false })) + } + + async uninstallApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> { + return this.authRequest({ method: Method.POST, url: `/apps/${appId}/uninstall${dryRunParam(dryRun, true)}`, readTimeout: 30000 }) + } + + async startApp (appId: string): Promise { + return this.authRequest({ method: Method.POST, url: `/apps/${appId}/start`, readTimeout: 30000 }) + .then(() => this.appModel.update({ id: appId, status: AppStatus.RUNNING })) + .then(() => ({ })) + } + + async stopApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> { + const res = await this.authRequest<{ breakages: DependentBreakage[] }>({ method: Method.POST, url: `/apps/${appId}/stop${dryRunParam(dryRun, true)}`, readTimeout: 30000 }) + if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPING }) + return res + } + + async restartApp (appId: string): Promise { + return this.authRequest({ method: Method.POST, url: `/apps/${appId}/restart`, readTimeout: 30000 }) + .then(() => ({ } as any)) + } + + async createAppBackup (appId: string, logicalname: string, password?: string): Promise { + const data: ReqRes.PostAppBackupCreateReq = { + password: password || undefined, + logicalname, + } + return this.authRequest({ method: Method.POST, url: `/apps/${appId}/backup`, data, readTimeout: 30000 }) + .then(() => this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP })) + .then(() => ({ })) + } + + async stopAppBackup (appId: string): Promise { + return this.authRequest({ method: Method.POST, url: `/apps/${appId}/backup/stop`, readTimeout: 30000 }) + .then(() => this.appModel.update({ id: appId, status: AppStatus.STOPPED })) + .then(() => ({ })) + } + + async restoreAppBackup (appId: string, logicalname: string, password?: string): Promise { + const data: ReqRes.PostAppBackupRestoreReq = { + password: password || undefined, + logicalname, + } + return this.authRequest({ method: Method.POST, url: `/apps/${appId}/backup/restore`, data, readTimeout: 30000 }) + .then(() => this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP })) + .then(() => ({ })) + } + + async patchAppConfig (app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> { + const data: ReqRes.PatchAppConfigReq = { + config, + } + return this.authRequest({ method: Method.PATCH, url: `/apps/${app.id}/config${dryRunParam(dryRun, true)}`, data, readTimeout: 30000 }) + } + + async postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> { + return this.authRequest({ method: Method.POST, url: `/apps/${dependencyId}/autoconfig/${dependentId}${dryRunParam(dryRun, true)}`, readTimeout: 30000 }) + } + + async patchServerConfig (attr: string, value: any): Promise { + const data: ReqRes.PatchServerConfigReq = { + value, + } + return this.authRequest({ method: Method.PATCH, url: `/${attr}`, data, readTimeout: 30000 }) + .then(() => this.serverModel.update({ [attr]: value })) + .then(() => ({ })) + } + + async wipeAppData (app: AppInstalledPreview): Promise { + return this.authRequest({ method: Method.POST, url: `/apps/${app.id}/wipe`, readTimeout: 30000 }).then((res) => { + this.appModel.update({ id: app.id, status: AppStatus.NEEDS_CONFIG }) + return res + }) + } + + async addSSHKey (sshKey: string): Promise { + const data: ReqRes.PostAddSSHKeyReq = { + sshKey, + } + const fingerprint = await this.authRequest({ method: Method.POST, url: `/sshKeys`, data }) + this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] }) + return { } + } + + async addWifi (ssid: string, password: string, country: string, connect: boolean): Promise { + const data: ReqRes.PostAddWifiReq = { + ssid, + password, + country, + skipConnect: !connect, + } + return this.authRequest({ method: Method.POST, url: `/wifi`, data }) + } + + async connectWifi (ssid: string): Promise { + return this.authRequest({ method: Method.POST, url: encodeURI(`/wifi/${ssid}`) }) + } + + async deleteWifi (ssid: string): Promise { + return this.authRequest({ method: Method.DELETE, url: encodeURI(`/wifi/${ssid}`) }) + } + + async deleteSSHKey (fingerprint: SSHFingerprint): Promise { + await this.authRequest({ method: Method.DELETE, url: `/sshKeys/${fingerprint.hash}` }) + const ssh = this.serverModel.peek().ssh + this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) }) + return { } + } + + async restartServer (): Promise { + return this.authRequest({ method: Method.POST, url: '/restart', readTimeout: 30000 }) + } + + async shutdownServer (): Promise { + return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 30000 }) + } + + private async authRequest (opts: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise { + if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`) + + opts.withCredentials = true + return this.http.serverRequest(opts, overrides).catch((e: HttpError) => { + console.log(`Got a server error!`, e) + if (isUnauthorized(e)) this.received401() + throw e + }) + } +} + +type HttpError = HttpErrorResponse & { error: { code: string, message: string } } + +const dryRunParam = (dryRun: boolean, first: boolean) => { + if (!dryRun) return '' + return first ? `?dryrun` : `&dryrun` +} \ No newline at end of file diff --git a/ui/src/app/services/api/md-sample.md b/ui/src/app/services/api/md-sample.md new file mode 100644 index 000000000..7417c4444 --- /dev/null +++ b/ui/src/app/services/api/md-sample.md @@ -0,0 +1,477 @@ +# Size Limit [![Cult Of Martians][cult-img]][cult] + +Size Limit logo by Anton Lovchikov + +Size Limit is a performance budget tool for JavaScript. It checks every commit +on CI, calculates the real cost of your JS for end-users and throws an error +if the cost exceeds the limit. + +* **ES modules** and **tree-shaking** support. +* Add Size Limit to **Travis CI**, **Circle CI**, **GitHub Actions** + or another CI system to know if a pull request adds a massive dependency. +* **Modular** to fit different use cases: big JS applications + that use their own bundler or small npm libraries with many files. +* Can calculate **the time** it would take a browser + to download and **execute** your JS. Time is a much more accurate + and understandable metric compared to the size in bytes. +* Calculations include **all dependencies and polyfills** + used in your JS. + +

+ Size Limit CLI +

+ +With **[GitHub action]** Size Limit will post bundle size changes as a comment +in pull request discussion. + +

+Size Limit comment in pull request about bundle size changes +

+ +With `--why`, Size Limit can tell you *why* your library is of this size +and show the real cost of all your internal dependencies. + +

+ Bundle Analyzer example +

+ +

+ + Sponsored by Evil Martians + +

+ +[GitHub action]: https://github.com/andresz1/size-limit-action +[cult-img]: http://cultofmartians.com/assets/badges/badge.svg +[cult]: http://cultofmartians.com/tasks/size-limit-config.html + +## Who Uses Size Limit + +* [MobX](https://github.com/mobxjs/mobx) +* [Material-UI](https://github.com/callemall/material-ui) +* [Autoprefixer](https://github.com/postcss/autoprefixer) +* [PostCSS](https://github.com/postcss/postcss) reduced + [25% of the size](https://github.com/postcss/postcss/commit/150edaa42f6d7ede73d8c72be9909f0a0f87a70f). +* [Browserslist](https://github.com/ai/browserslist) reduced + [25% of the size](https://github.com/ai/browserslist/commit/640b62fa83a20897cae75298a9f2715642531623). +* [EmojiMart](https://github.com/missive/emoji-mart) reduced + [20% of the size](https://github.com/missive/emoji-mart/pull/111) +* [nanoid](https://github.com/ai/nanoid) reduced + [33% of the size](https://github.com/ai/nanoid/commit/036612e7d6cc5760313a8850a2751a5e95184eab). +* [React Focus Lock](https://github.com/theKashey/react-focus-lock) reduced + [32% of the size](https://github.com/theKashey/react-focus-lock/pull/48). +* [Logux](https://github.com/logux) reduced + [90% of the size](https://github.com/logux/logux-client/commit/62b258e20e1818b23ae39b9c4cd49e2495781e91). + + +## How It Works + +1. Size Limit contains a CLI tool, 3 plugins (`file`, `webpack`, `time`) + and 3 plugin presets for popular use cases (`app`, `big-lib`, `small-lib`). + A CLI tool finds plugins in `package.json` and loads the config. +2. If you use the `webpack` plugin, Size Limit will bundle your JS files into + a single file. It is important to track dependencies and webpack polyfills. + It is also useful for small libraries with many small files and without + a bundler. +3. The `webpack` plugin creates an empty webpack project, adds your library + and looks for the bundle size difference. +4. The `time` plugin compares the current machine performance with that of + a low-priced Android devices to calculate the CPU throttling rate. +5. Then the `time` plugin runs headless Chrome (or desktop Chrome if it’s + available) to track the time a browser takes to compile and execute your JS. + Note that these measurements depend on available resources and might + be unstable. [See here](https://github.com/mbalabash/estimo/issues/5) + for more details. + + +## Usage + +### JS Applications + +Suitable for applications that have their own bundler and send the JS bundle +directly to a client (without publishing it to npm). Think of a user-facing app +or website, like an email client, a CRM, a landing page or a blog with +interactive elements, using React/Vue/Svelte lib or vanilla JS. + +
Show instructions + +1. Install the preset: + + ```sh + $ npm install --save-dev size-limit @size-limit/preset-app + ``` + +2. Add the `size-limit` section and the `size` script to your `package.json`: + + ```diff + + "size-limit": [ + + { + + "path": "dist/app-*.js" + + } + + ], + "scripts": { + "build": "webpack ./webpack.config.js", + + "size": "npm run build && size-limit", + "test": "jest && eslint ." + } + ``` + +3. Here’s how you can get the size for your current project: + + ```sh + $ npm run size + + Package size: 30.08 KB with all dependencies, minified and gzipped + Loading time: 602 ms on slow 3G + Running time: 214 ms on Snapdragon 410 + Total time: 815 ms + ``` + +4. Now, let’s set the limit. Add 25% to the current total time and use that as + the limit in your `package.json`: + + ```diff + "size-limit": [ + { + + "limit": "1 s", + "path": "dist/app-*.js" + } + ], + ``` + +5. Add the `size` script to your test suite: + + ```diff + "scripts": { + "build": "webpack ./webpack.config.js", + "size": "npm run build && size-limit", + - "test": "jest && eslint ." + + "test": "jest && eslint . && npm run size" + } + ``` + +6. If you don’t have a continuous integration service running, don’t forget + to add one — start with [Travis CI]. + +
+ + +### Big Libraries + +JS libraries > 10 KB in size. + +This preset includes headless Chrome, and will measure your lib’s execution +time. You likely don’t need this overhead for a small 2 KB lib, but for larger +ones the execution time is a more accurate and understandable metric that +the size in bytes. Library like [React] is a good example for this preset. + +
Show instructions + +1. Install preset: + + ```sh + $ npm install --save-dev size-limit @size-limit/preset-big-lib + ``` + +2. Add the `size-limit` section and the `size` script to your `package.json`: + + ```diff + + "size-limit": [ + + { + + "path": "dist/react.production-*.js" + + } + + ], + "scripts": { + "build": "webpack ./scripts/rollup/build.js", + + "size": "npm run build && size-limit", + "test": "jest && eslint ." + } + ``` + +3. If you use ES modules you can test the size after tree-shaking with `import` + option: + + ```diff + "size-limit": [ + { + "path": "dist/react.production-*.js", + + "import": "{ createComponent }" + } + ], + ``` + +4. Here’s how you can get the size for your current project: + + ```sh + $ npm run size + + Package size: 30.08 KB with all dependencies, minified and gzipped + Loading time: 602 ms on slow 3G + Running time: 214 ms on Snapdragon 410 + Total time: 815 ms + ``` + +5. Now, let’s set the limit. Add 25% to the current total time and use that + as the limit in your `package.json`: + + ```diff + "size-limit": [ + { + + "limit": "1 s", + "path": "dist/react.production-*.js" + } + ], + ``` + +6. Add a `size` script to your test suite: + + ```diff + "scripts": { + "build": "rollup ./scripts/rollup/build.js", + "size": "npm run build && size-limit", + - "test": "jest && eslint ." + + "test": "jest && eslint . && npm run size" + } + ``` + +7. If you don’t have a continuous integration service running, don’t forget + to add one — start with [Travis CI]. +8. Add the library size to docs, it will help users to choose your project: + + ```diff + # Project Name + + Short project description + + * **Fast.** 10% faster than competitor. + + * **Small.** 15 KB (minified and gzipped). + + [Size Limit](https://github.com/ai/size-limit) controls the size. + ``` + +
+ + +### Small Libraries + +JS libraries < 10 KB in size. + +This preset will only measure the size, without the execution time, so it’s +suitable for small libraries. If your library is larger, you likely want +the Big Libraries preset above. [Nano ID] or [Storeon] are good examples +for this preset. + +
Show instructions + +1. First, install `size-limit`: + + ```sh + $ npm install --save-dev size-limit @size-limit/preset-small-lib + ``` + +2. Add the `size-limit` section and the `size` script to your `package.json`: + + ```diff + + "size-limit": [ + + { + + "path": "index.js" + + } + + ], + "scripts": { + + "size": "size-limit", + "test": "jest && eslint ." + } + ``` + +3. Here’s how you can get the size for your current project: + + ```sh + $ npm run size + + Package size: 177 B with all dependencies, minified and gzipped + ``` + +4. If your project size starts to look bloated, run `--why` for analysis: + + ```sh + $ npm run size -- --why + ``` + +5. Now, let’s set the limit. Determine the current size of your library, + add just a little bit (a kilobyte, maybe) and use that as the limit + in your `package.json`: + + ```diff + "size-limit": [ + { + + "limit": "9 KB", + "path": "index.js" + } + ], + ``` + +6. Add the `size` script to your test suite: + + ```diff + "scripts": { + "size": "size-limit", + - "test": "jest && eslint ." + + "test": "jest && eslint . && npm run size" + } + ``` + +7. If you don’t have a continuous integration service running, don’t forget + to add one — start with [Travis CI]. +8. Add the library size to docs, it will help users to choose your project: + + ```diff + # Project Name + + Short project description + + * **Fast.** 10% faster than competitor. + + * **Small.** 500 bytes (minified and gzipped). No dependencies. + + [Size Limit](https://github.com/ai/size-limit) controls the size. + ``` + +
+ +[Travis CI]: https://github.com/dwyl/learn-travis +[Storeon]: https://github.com/ai/storeon/ +[Nano ID]: https://github.com/ai/nanoid/ +[React]: https://github.com/facebook/react/ + + +## Reports + +Size Limit has a [GitHub action] that comments and rejects pull requests based +on Size Limit output. + +1. Install and configure Size Limit as shown above. +2. Add the following action inside `.github/workflows/size-limit.yml` + +```yaml +name: "size" +on: + pull_request: + branches: + - master +jobs: + size: + runs-on: ubuntu-latest + env: + CI_JOB_NUMBER: 1 + steps: + - uses: actions/checkout@v1 + - uses: andresz1/size-limit-action@v1.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} +``` + + +## Config + +Size Limits supports three ways to define config. + +1. `size-limit` section in `package.json`: + + ```json + "size-limit": [ + { + "path": "index.js", + "import": "{ createStore }", + "limit": "500 ms" + } + ] + ``` + +2. or a separate `.size-limit.json` config file: + + ```js + [ + { + "path": "index.js", + "import": "{ createStore }", + "limit": "500 ms" + } + ] + ``` + +3. or a more flexible `.size-limit.js` config file: + + ```js + module.exports = [ + { + path: "index.js", + import: "{ createStore }", + limit: "500 ms" + } + ] + ``` + +Each section in the config can have these options: + +* **path**: relative paths to files. The only mandatory option. + It could be a path `"index.js"`, a [pattern] `"dist/app-*.js"` + or an array `["index.js", "dist/app-*.js", "!dist/app-exclude.js"]`. +* **import**: partial import to test tree-shaking. It could be `"{ lib }"` + to test `import { lib } from 'lib'` or `{ "a.js": "{ a }", "b.js": "{ b }" }` + to test multiple files. +* **limit**: size or time limit for files from the `path` option. It should be + a string with a number and unit, separated by a space. + Format: `100 B`, `10 KB`, `500 ms`, `1 s`. +* **name**: the name of the current section. It will only be useful + if you have multiple sections. +* **entry**: when using a custom webpack config, a webpack entry could be given. + It could be a string or an array of strings. + By default, the total size of all entry points will be checked. +* **webpack**: with `false` it will disable webpack. +* **running**: with `false` it will disable calculating running time. +* **gzip**: with `false` it will disable gzip compression. +* **brotli**: with `true` it will use brotli compression and disable gzip compression. +* **config**: a path to a custom webpack config. +* **ignore**: an array of files and dependencies to exclude from + the project size calculation. + +If you use Size Limit to track the size of CSS files, make sure to set +`webpack: false`. Otherwise, you will get wrong numbers, because webpack +inserts `style-loader` runtime (≈2 KB) into the bundle. + +[pattern]: https://github.com/sindresorhus/globby#globbing-patterns + + +## Plugins and Presets + +Plugins: + +* `@size-limit/file` checks the size of files with Gzip, Brotli + or without compression. +* `@size-limit/webpack` adds your library to empty webpack project + and prepares bundle file for `file` plugin. +* `@size-limit/time` uses headless Chrome to track time to execute JS. +* `@size-limit/dual-publish` compiles files to ES modules with [`dual-publish`] + to check size after tree-shaking. + +Plugin presets: + +* `@size-limit/preset-app` contains `file` and `time` plugins. +* `@size-limit/preset-big-lib` contains `webpack`, `file`, and `time` plugins. +* `@size-limit/preset-small-lib` contains `webpack` and `file` plugins. + +[`dual-publish`]: https://github.com/ai/dual-publish + + +## JS API + +```js +const sizeLimit = require('size-limit') +const filePlugin = require('@size-limit/file') +const webpackPlugin = require('@size-limit/webpack') + +sizeLimit([filePlugin, webpackPlugin], [filePath]).then(result => { + result //=> { size: 12480 } +}) +``` \ No newline at end of file diff --git a/ui/src/app/services/api/mock-api.service.ts b/ui/src/app/services/api/mock-api.service.ts new file mode 100644 index 000000000..f25c5b2bd --- /dev/null +++ b/ui/src/app/services/api/mock-api.service.ts @@ -0,0 +1,1086 @@ +import { Injectable } from '@angular/core' +import { AppStatus, AppModel } from '../../models/app-model' +import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo } from '../../models/app-types' +import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model' +import { pauseFor } from '../../util/misc.util' +import { ApiService, ReqRes } from './api.service' +import { ApiServer, Unit as EmptyResponse, Unit } from './api-types' +import { AppMetrics, AppMetricsVersioned, parseMetricsPermissive } from 'src/app/util/metrics.util' +import { mockApiAppAvailableFull, mockApiAppAvailableVersionInfo, mockApiAppInstalledFull, mockAppDependentBreakages, toInstalledPreview } from './mock-app-fixures' + +//@TODO consider moving to test folders. +@Injectable() +export class MockApiService extends ApiService { + constructor ( + private readonly appModel: AppModel, + private readonly serverModel: ServerModel, + ) { + super() + } + + async postLogin () : Promise { + return { } + } + + async postLogout () : Promise { + return { } + } + + async postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> { + await pauseFor(2000) + throw new Error ('some misc backend error ohh we forgot to make this endpoint or something') + // return { config: mockCupsDependentConfig, breakages: [ ] } + } + + async getServer (): Promise { + return mockGetServer() + } + + async getCheckAuth (): Promise { + return { } + } + + async getVersionLatest (): Promise { + return mockGetVersionLatest() + } + + async getServerMetrics (): Promise { + return mockGetServerMetrics() + } + + async getNotifications (page: number, perPage: number): Promise { + return mockGetNotifications() + } + + async deleteNotification (id: string): Promise { + return mockDeleteNotification() + } + + async getExternalDisks (): Promise { + return mockGetExternalDisks() + } + + async updateAgent (thing: any): Promise { + return mockPostUpdateAgent() + } + + async getAvailableApps (): Promise { + return mockGetAvailableApps() + } + + async getAvailableApp (appId: string): Promise { + // throw new Error('Some horrible horrible error message gosh its awful') + return mockGetAvailableApp(appId) + .then(res => { + return { + ...res, + versionViewing: res.versionLatest, + } + }) + } + + async getAvailableAppVersionSpecificInfo (appId: string, versionSpec: string): Promise { + return mockGetAvailableAppVersionInfo() + } + + async getInstalledApp (appId: string): Promise { + return mockGetInstalledApp(appId) + } + + async getAppMetrics (appId: string): Promise { + return mockGetAppMetrics().then(parseMetricsPermissive) + } + + async getInstalledApps (): Promise { + return mockGetInstalledApps() + } + + async getAppConfig (appId: string): Promise { + return mockGetAppConfig() + } + + async getAppLogs (appId: string, params: ReqRes.GetAppLogsReq = { }): Promise { + return mockGetAppLogs() + } + + async installApp (appId: string, version: string, dryRun: boolean): Promise { + return mockInstallApp(appId) + } + + async uninstallApp (appId: string, dryRun: boolean): Promise<{ breakages: DependentBreakage[] }> { + return mockUninstallApp() + } + + async startApp (appId: string): Promise { + console.log('start app mock') + await mockStartApp() + this.appModel.update({ id: appId, status: AppStatus.RUNNING }) + return { } + } + + async stopApp (appId: string, dryRun = false): Promise<{ breakages: DependentBreakage[] }> { + await mockStopApp() + if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPED }) + return mockAppDependentBreakages + } + + async restartApp (appId: string): Promise { + return { } + } + + async createAppBackup (appId: string, logicalname: string, password = ''): Promise { + await mockCreateAppBackup() + this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP }) + return { } + } + + async stopAppBackup (appId: string): Promise { + await mockStopAppBackup() + this.appModel.update({ id: appId, status: AppStatus.STOPPED }) + return { } + } + + async restoreAppBackup (appId: string, logicalname: string, password?: string): Promise { + await mockCreateAppBackup() + this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP }) + return { } + } + + async patchAppConfig (app: AppInstalledPreview, config: object, dryRun?: boolean): Promise<{ breakages: DependentBreakage[] }> { + return mockPatchAppConfig() + } + + async patchServerConfig (attr: string, value: any): Promise { + await mockPatchServerConfig() + this.serverModel.update({ [attr]: value }) + return { } + } + + async wipeAppData (app: AppInstalledPreview): Promise { + return mockWipeAppData() + } + + async addSSHKey (sshKey: string): Promise { + const fingerprint = await mockAddSSHKey() + this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] }) + return { } + } + + async deleteSSHKey (fingerprint: SSHFingerprint): Promise { + await mockDeleteSSHKey() + const ssh = this.serverModel.peek().ssh + this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) }) + return { } + } + + async addWifi (ssid: string, password: string, country: string, connect: boolean): Promise { + return mockAddWifi() + } + + async connectWifi (ssid: string): Promise { + return mockConnectWifi() + } + + async deleteWifi (ssid: string): Promise { + return mockDeleteWifi() + } + + async restartServer (): Promise { + return mockRestartServer() + } + + async shutdownServer (): Promise { + return mockShutdownServer() + } +} + +async function mockGetServer (): Promise { + await pauseFor(1000) + return mockApiServer() +} + +async function mockGetVersionLatest (): Promise { + await pauseFor(1000) + return mockVersionLatest +} + +async function mockGetServerMetrics (): Promise { + await pauseFor(1000) + return mockApiServerMetrics +} + +async function mockGetNotifications (): Promise { + await pauseFor(1000) + function cloneAndChange (arr: S9Notification[], letter: string) { return JSON.parse(JSON.stringify(arr)).map(a => { a.id = a.id + letter; return a }) } + return mockApiNotifications.concat(cloneAndChange(mockApiNotifications, 'a')).concat(cloneAndChange(mockApiNotifications, 'b')) +} + +async function mockDeleteNotification (): Promise { + await pauseFor(1000) + return { } +} + +async function mockGetExternalDisks (): Promise { + await pauseFor(1000) + return mockApiExternalDisks +} + +async function mockPostUpdateAgent (): Promise { + await pauseFor(1000) + return { } +} + +async function mockGetAvailableApp (appId: string): Promise { + await pauseFor(1000) + return mockApiAppAvailableFull[appId] +} + +async function mockGetAvailableApps (): Promise { + await pauseFor(1000) + return Object.values(mockApiAppAvailableFull) +} + +async function mockGetInstalledApp (appId: string): Promise { + await pauseFor(1000) + return { ...mockApiAppInstalledFull[appId], hasFetchedFull: true } +} + +async function mockGetInstalledApps (): Promise { + await pauseFor(1000) + return Object.values(mockApiAppInstalledFull).map(toInstalledPreview).filter(({ versionInstalled}) => !!versionInstalled) +} + +async function mockGetAppLogs (): Promise { + await pauseFor(1000) + return mockApiAppLogs +} + +async function mockGetAppMetrics (): Promise { + await pauseFor(1000) + return mockApiAppMetricsV1 +} + +async function mockGetAvailableAppVersionInfo (): Promise { + await pauseFor(1000) + return mockApiAppAvailableVersionInfo +} + +async function mockGetAppConfig (): Promise { + await pauseFor(1000) + return mockApiAppConfig +} + +async function mockInstallApp (appId: string): Promise { + await pauseFor(1000) + return { ...mockApiAppInstalledFull[appId], hasFetchedFull: true, ...mockAppDependentBreakages } +} + +async function mockUninstallApp (): Promise< { breakages: DependentBreakage[] } > { + await pauseFor(1000) + return mockAppDependentBreakages +} + +async function mockStartApp (): Promise { + await pauseFor(1000) + return { } +} + +async function mockStopApp (): Promise { + await pauseFor(1000) + return { } +} + +async function mockCreateAppBackup (): Promise { + await pauseFor(1000) + return { } +} + +async function mockStopAppBackup (): Promise { + await pauseFor(1000) + return { } +} + + +async function mockPatchAppConfig (): Promise<{ breakages: DependentBreakage[] }> { + await pauseFor(1000) + return mockAppDependentBreakages +} + +async function mockPatchServerConfig (): Promise { + await pauseFor(1000) + return { } +} + +async function mockWipeAppData (): Promise { + await pauseFor(1000) + return { } +} + +async function mockAddSSHKey (): Promise { + await pauseFor(1000) + return mockApiServer().ssh[0] +} + +async function mockDeleteSSHKey (): Promise { + await pauseFor(1000) + return { } +} + +async function mockAddWifi (): Promise { + await pauseFor(1000) + return { } +} + +async function mockConnectWifi (): Promise { + await pauseFor(1000) + return { } +} + +async function mockDeleteWifi (): Promise { + await pauseFor(1000) + return { } +} + +async function mockRestartServer (): Promise { + await pauseFor(1000) + return { } +} + +async function mockShutdownServer (): Promise { + await pauseFor(1000) + return { } +} + +const mockApiNotifications: ReqRes.GetNotificationsRes = [ + { + id: '123e4567-e89b-12d3-a456-426655440000', + appId: 'bitcoind', + createdAt: '2019-12-26T14:20:30.872Z', + code: '101', + title: 'Install Complete', + message: 'Installation of bitcoind has completed successfully.', + }, + { + id: '123e4567-e89b-12d3-a456-426655440001', + appId: 'bitcoind', + createdAt: '2019-12-26T14:20:30.872Z', + code: '201', + title: 'SSH Key Added', + message: 'A new SSH key was added. If you did not do this, shit is bad.', + }, + { + id: '123e4567-e89b-12d3-a456-426655440002', + appId: 'bitcoind', + createdAt: '2019-12-26T14:20:30.872Z', + code: '002', + title: 'SSH Key Removed', + message: 'A SSH key was removed.', + }, + { + id: '123e4567-e89b-12d3-a456-426655440003', + appId: 'bitcoind', + createdAt: '2019-12-26T14:20:30.872Z', + code: '310', + title: 'App Crashed', + message: 'Bitcoind has crashed', + }, +] + +const mockApiServer: () => ReqRes.GetServerRes = () => ({ + serverId: 'start9-mockxyzab', + name: 'Embassy:12345678', + versionInstalled: '0.2.5', + status: ServerStatus.RUNNING, + alternativeRegistryUrl: 'beta-registry.start9labs.com', + specs: { + 'Tor Address': 'nfsnjkcnaskjnlkasnfahj7dh23fdnieqwjdnhjewbfijendiueqwbd.onion', + 'CPU': 'Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz', + 'RAM': '4GB LPDDR4-2400 SDRAM', + 'WiFI': '2.4 GHz and 5.0 GHz IEEE 802.11ac wireless, Bluetooth 5.0, BLE', + 'Ethernet': 'Gigabit', + 'Disk': '512 GB Flash (280 GB available)', + 'Embassy OS Version': '0.1.0.1', + }, + wifi: { + ssids: ['Goosers', 'Atlantic City'], + current: 'Goosers', + }, + ssh: [ + { + alg: 'ed25519', + hash: '28:d2:7e:78:61:b4:bf:g2:de:24:15:96:4e:d4:15:53', + hostname: 'aaron key', + }, + { + alg: 'ed25519', + hash: '12:f8:7e:78:61:b4:bf:e2:de:24:15:96:4e:d4:72:53', + hostname: 'matt macbook pro', + }, + ], +}) + +const mockVersionLatest: ReqRes.GetVersionLatestRes = { + versionLatest: '0.2.5', + canUpdate: true, +} + +const mockApiServerMetrics: ReqRes.GetServerMetricsRes = { + 'Group1': { + 'Metric1': { + value: 22.2, + unit: 'mi/b', + }, + 'Metric2': { + value: 50, + unit: '%', + }, + 'Metric3': { + value: 10.1, + unit: '%', + }, + }, + 'Group2': { + 'Hmmmm1': { + value: 22.2, + unit: 'mi/b', + }, + 'Hmmmm2': { + value: 50, + unit: '%', + }, + 'Hmmmm3': { + value: 10.1, + unit: '%', + }, + }, +} + +const mockApiExternalDisks: DiskInfo[] = [ + { + logicalname: '/dev/sda', + size: '32GB', + description: 'Samsung', + partitions: [ + { + logicalname: 'sdba2', + size: null, + isMounted: false, + label: 'Matt Stuff', + }, + ], + }, + { + logicalname: '/dev/sba', + size: '64GB', + description: 'small USB stick', + partitions: [ + { + logicalname: 'sdba2', + size: '16GB', + isMounted: true, + label: null, + }, + ], + }, + { + logicalname: '/dev/sbd', + size: '128GBGB', + description: 'large USB stick', + partitions: [ + { + logicalname: 'sdba1', + size: '32GB', + isMounted: true, + label: 'Partition 1', + }, + { + logicalname: 'sdba2', + size: null, + isMounted: true, + label: 'Partition 2', + }, + ], + }, +] + +const mockApiAppLogs: string[] = [ + '****** START *****', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:20:30.872Z - Hash: 2b2e5abb3cba2164aea0', + '[ng] 114 unchanged chunks', + '[ng] chunk {app-logs-app-logs-module} app-logs-app-logs-module.js, app-logs-app-logs-module.js.map (app-logs-app-logs-module) 7.86 kB [rendered]', + '[ng] Time: 1244ms', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:21:01.685Z - Hash: bb3f5d0e11f2cd2dd57b', + '[ng] 114 unchanged chunks', + '[ng] chunk {app-logs-app-logs-module} app-logs-app-logs-module.js, app-logs-app-logs-module.js.map (app-logs-app-logs-module) 7.86 kB [rendered]', + '[ng] Time: 1185ms', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:23:13.812Z - Hash: 9342e11e6b8e16ad2f70', + '[ng] 114 unchanged chunks', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:20:30.872Z - Hash: 2b2e5abb3cba2164aea0', + '[ng] 114 unchanged chunks', + '[ng] chunk {app-logs-app-logs-module} app-logs-app-logs-module.js, app-logs-app-logs-module.js.map (app-logs-app-logs-module) 7.86 kB [rendered]', + '[ng] Time: 1244ms', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:21:01.685Z - Hash: bb3f5d0e11f2cd2dd57b', + '[ng] 114 unchanged chunks', + '[ng] chunk {app-logs-app-logs-module} app-logs-app-logs-module.js, app-logs-app-logs-module.js.map (app-logs-app-logs-module) 7.86 kB [rendered]', + '[ng] Time: 1185ms', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:23:13.812Z - Hash: 9342e11e6b8e16ad2f70', + '[ng] 114 unchanged chunks', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:20:30.872Z - Hash: 2b2e5abb3cba2164aea0', + '[ng] 114 unchanged chunks', + '[ng] chunk {app-logs-app-logs-module} app-logs-app-logs-module.js, app-logs-app-logs-module.js.map (app-logs-app-logs-module) 7.86 kB [rendered]', + '[ng] Time: 1244ms', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:21:01.685Z - Hash: bb3f5d0e11f2cd2dd57b', + '[ng] 114 unchanged chunks', + '[ng] chunk {app-logs-app-logs-module} app-logs-app-logs-module.js, app-logs-app-logs-module.js.map (app-logs-app-logs-module) 7.86 kB [rendered]', + '[ng] Time: 1185ms', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:23:13.812Z - Hash: 9342e11e6b8e16ad2f70', + '[ng] 114 unchanged chunks', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:20:30.872Z - Hash: 2b2e5abb3cba2164aea0', + '[ng] 114 unchanged chunks', + '[ng] chunk {app-logs-app-logs-module} app-logs-app-logs-module.js, app-logs-app-logs-module.js.map (app-logs-app-logs-module) 7.86 kB [rendered]', + '[ng] Time: 1244ms', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:21:01.685Z - Hash: bb3f5d0e11f2cd2dd57b', + '[ng] 114 unchanged chunks', + '[ng] chunk {app-logs-app-logs-module} app-logs-app-logs-module.js, app-logs-app-logs-module.js.map (app-logs-app-logs-module) 7.86 kB [rendered]', + '[ng] Time: 1185ms', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:23:13.812Z - Hash: 9342e11e6b8e16ad2f70', + '[ng] 114 unchanged chunks', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:20:30.872Z - Hash: 2b2e5abb3cba2164aea0', + '[ng] 114 unchanged chunks', + '[ng] chunk {app-logs-app-logs-module} app-logs-app-logs-module.js, app-logs-app-logs-module.js.map (app-logs-app-logs-module) 7.86 kB [rendered]', + '[ng] Time: 1244ms', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:21:01.685Z - Hash: bb3f5d0e11f2cd2dd57b', + '[ng] 114 unchanged chunks', + '[ng] chunk {app-logs-app-logs-module} app-logs-app-logs-module.js, app-logs-app-logs-module.js.map (app-logs-app-logs-module) 7.86 kB [rendered]', + '[ng] Time: 1185ms', + '[ng] ℹ 「wdm」: Compiled successfully.', + '[ng] ℹ 「wdm」: Compiling...', + '[ng] Date: 2019-12-26T14:23:13.812Z - Hash: 9342e11e6b8e16ad2f70', + '[ng] 114 unchanged chunks', + '****** FINISH *****', +] + +const mockApiAppMetricsV1: AppMetricsVersioned<2> = { + version: 2, + data: { + 'Test': { + type: 'string', + description: 'This is some information about the thing.', + copyable: true, + qr: true, + masked: false, + value: 'lndconnect://udlyfq2mxa4355pt7cqlrdipnvk2tsl4jtsdw7zaeekenufwcev2wlad.onion:10009?cert=MIICJTCCAcugAwIBAgIRAOyq85fqAiA3U3xOnwhH678wCgYIKoZIzj0EAwIwODEfMB0GAkUEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMB4XDTIwMTAyNjA3MzEyN1oXDTIxMTIyMTA3MzEyN1owODEfMB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKqfhAMMZdY-eFnU5P4bGrQTSx0lo7m8u4V0yYkzUM6jlql_u31_mU2ovLTj56wnZApkEjoPl6fL2yasZA2wiy6OBtTCBsjAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUYQ9uIO6spltnVCx4rLFL5BvBF9IwWwYDVR0RBFQwUoIMNTc0OTkwMzIyYzZlgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhwYWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBKwSAAswCgYIKoZIzj0EAwIDSAAwRQIgVZH2Z2KlyAVY2Q2aIQl0nsvN-OEN49wreFwiBqlxNj4CIQD5_JbpuBFJuf81I5J0FQPtXY-4RppWOPZBb-y6-rkIUQ&macaroon=AgEDbG5kAusBAwoQuA8OUMeQ8Fr2h-f65OdXdRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFAoIbWFjYXJvb24SCGdlbmVyYXRlGhYKB21lc3NhZ2USBHJlYWQSBXdyaXRlGhcKCG9mZmNoYWluEgRyZWFkEgV3cml0ZRoWCgdvbmNoYWluEgRyZWFkEgV3cml0ZRoUCgVwZWVycxIEcmVhZBIFd3JpdGUaGAoGc2lnbmVyEghnZW5lcmF0ZRIEcmVhZAAABiCYsRUoUWuAHAiCSLbBR7b_qULDSl64R8LIU2aqNIyQfA', + }, + 'Nested': { + type: 'object', + description: 'This is a nested thing metric', + value: { + 'Last Name': { + type: 'string', + description: 'The last name of the user', + copyable: true, + qr: true, + masked: false, + value: 'Hill', + }, + 'Age': { + type: 'string', + description: 'The age of the user', + copyable: false, + qr: false, + masked: false, + value: '35', + }, + 'Password': { + type: 'string', + description: 'A secret password', + copyable: true, + qr: false, + masked: true, + value: 'password123', + }, + }, + }, + 'Another Property': { + type: 'string', + description: 'Some more information about the service.', + copyable: false, + qr: true, + masked: false, + value: 'https://guessagain.com', + }, + }, +} + +const mockApiAppConfig: ReqRes.GetAppConfigRes = { + // config spec + spec: { + 'testnet': { + 'name': 'Testnet', + 'type': 'boolean', + 'description': 'determines whether your node is running ontestnet or mainnet', + 'changeWarning': 'Chain will have to resync!', + 'default': false, + }, + 'objectList': { + 'name': 'Object List', + 'type': 'list', + 'subtype': 'object', + 'description': 'This is a list of objects, like users or something', + 'range': '[0,4]', + 'default': [ + { + 'firstName': 'Admin', + 'lastName': 'User', + 'age': 40, + }, + { + 'firstName': 'Admin2', + 'lastName': 'User2', + 'age': 40, + }, + ], + // the outer spec here, at the list level, says that what's inside (the inner spec) pertains to its inner elements. + // it just so happens that ValueSpecObject's have the field { spec: ConfigSpec } + // see 'unionList' below for a different example. + 'spec': { + 'uniqueBy': 'lastName', + 'displayAs': `I'm {{lastName}}, {{firstName}} {{lastName}}`, + 'spec': { + 'firstName': { + 'name': 'First Name', + 'type': 'string', + 'description': 'User first name', + 'nullable': true, + 'default': null, + 'masked': false, + 'copyable': false, + }, + 'lastName': { + 'name': 'Last Name', + 'type': 'string', + 'description': 'User first name', + 'nullable': true, + 'default': { + 'charset': 'a-g,2-9', + 'len': 12, + }, + 'pattern': '^[a-zA-Z]+$', + 'patternDescription': 'must contain only letters.', + 'masked': false, + 'copyable': true, + }, + 'age': { + 'name': 'Age', + 'type': 'number', + 'description': 'The age of the user', + 'nullable': true, + 'default': null, + 'integral': false, + 'changeWarning': 'User must be at least 18.', + 'range': '[18,*)', + }, + }, + }, + }, + 'unionList': { + 'name': 'Union List', + 'type': 'list', + 'subtype': 'union', + 'description': 'This is a sample list of unions', + 'changeWarning': 'If you change this, things may work.', + // a list of union selections. e.g. 'summer', 'winter',... + 'default': [ + 'summer', + ], + 'range': '[0, 2]', + 'spec': { + 'tag': { + 'id': 'preference', + 'name': 'Preferences', + 'variantNames': { + 'summer': 'Summer', + 'winter': 'Winter', + 'other': 'Other', + }, + }, + // this default is used to make a union selection when a new list element is first created + 'default': 'summer', + 'variants': { + 'summer': { + 'favorite-tree': { + 'name': 'Favorite Tree', + 'type': 'string', + 'nullable': false, + 'description': 'What is your favorite tree?', + 'default': 'Maple', + 'masked': false, + 'copyable': false, + }, + 'favorite-flower': { + 'name': 'Favorite Flower', + 'type': 'enum', + 'description': 'Select your favorite flower', + 'valueNames': { + 'none': 'Hate Flowers', + 'red': 'Red', + 'blue': 'Blue', + 'purple': 'Purple', + }, + 'values': [ + 'none', + 'red', + 'blue', + 'purple', + ], + 'default': 'none', + }, + }, + 'winter': { + 'like-snow': { + 'name': 'Like Snow?', + 'type': 'boolean', + 'description': 'Do you like snow or not?', + 'default': true, + }, + }, + }, + 'uniqueBy': 'preference', + }, + }, + 'randomEnum': { + 'name': 'Random Enum', + 'type': 'enum', + 'valueNames': { + 'null': 'Null', + 'option1': 'One 1', + 'option2': 'Two 2', + 'option3': 'Three 3', + }, + 'default': 'null', + 'description': 'This is not even real.', + 'changeWarning': 'Be careful chnaging this!', + 'values': [ + 'null', + 'option1', + 'option2', + 'option3', + ], + }, + 'favoriteNumber': { + 'name': 'Favorite Number', + 'type': 'number', + 'integral': false, + 'description': 'Your favorite number of all time', + 'changeWarning': 'Once you set this number, it can never be changed without severe consequences.', + 'nullable': false, + 'default': 7, + 'range': '(-100,100]', + 'units': 'BTC', + }, + 'secondaryNumbers': { + 'name': 'Unlucky Numbers', + 'type': 'list', + 'subtype': 'number', + 'description': 'Numbers that you like but are not your top favorite.', + 'spec': { + 'integral': false, + 'range': '[-100,200)', + }, + 'range': '[0,10]', + 'default': [ + 2, + 3, + ], + }, + 'rpcsettings': { + 'name': 'RPC Settings', + 'type': 'object', + 'uniqueBy': null, + 'description': 'rpc username and password', + 'changeWarning': 'Adding RPC users gives them special permissions on your node.', + 'nullable': false, + 'nullByDefault': false, + 'spec': { + 'laws': { + 'name': 'Laws', + 'type': 'object', + 'uniqueBy': 'law1', + 'description': 'the law of the realm', + 'nullable': true, + 'nullByDefault': true, + 'spec': { + 'law1': { + 'name': 'First Law', + 'type': 'string', + 'description': 'the first law', + 'nullable': true, + 'masked': false, + 'copyable': true, + }, + 'law2': { + 'name': 'Second Law', + 'type': 'string', + 'description': 'the second law', + 'nullable': true, + 'masked': false, + 'copyable': true, + }, + }, + }, + 'rulemakers': { + 'name': 'Rule Makers', + 'type': 'list', + 'subtype': 'object', + 'description': 'the people who make the rules', + 'range': '[0,2]', + 'default': [], + 'spec': { + 'uniqueBy': null, + 'spec': { + 'rulemakername': { + 'name': 'Rulemaker Name', + 'type': 'string', + 'description': 'the name of the rule maker', + 'nullable': false, + 'default': { + 'charset': 'a-g,2-9', + 'len': 12, + }, + 'masked': false, + 'copyable': false, + }, + 'rulemakerip': { + 'name': 'Rulemaker IP', + 'type': 'string', + 'description': 'the ip of the rule maker', + 'nullable': false, + 'default': '192.168.1.0', + 'pattern': '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', + 'patternDescription': 'may only contain numbers and periods', + 'masked': false, + 'copyable': true, + }, + }, + }, + }, + 'rpcuser': { + 'name': 'RPC Username', + 'type': 'string', + 'description': 'rpc username', + 'nullable': false, + 'default': 'defaultrpcusername', + 'pattern': '^[a-zA-Z]+$', + 'patternDescription': 'must contain only letters.', + 'masked': false, + 'copyable': true, + }, + 'rpcpass': { + 'name': 'RPC User Password', + 'type': 'string', + 'description': 'rpc password', + 'nullable': false, + 'default': { + 'charset': 'a-z,A-Z,2-9', + 'len': 20, + }, + 'masked': true, + 'copyable': true, + }, + }, + }, + 'advanced': { + 'name': 'Advanced', + 'type': 'object', + 'uniqueBy': null, + 'description': 'Advanced settings', + 'nullable': false, + 'nullByDefault': false, + 'spec': { + 'notifications': { + 'name': 'Notification Preferences', + 'type': 'list', + 'subtype': 'enum', + 'description': 'how you want to be notified', + 'range': '[1,3]', + 'default': [ + 'email', + ], + 'spec': { + 'valueNames': { + 'email': 'EEEEmail', + 'text': 'Texxxt', + 'call': 'Ccccall', + 'push': 'PuuuusH', + 'webhook': 'WebHooookkeee', + }, + 'values': [ + 'email', + 'text', + 'call', + 'push', + 'webhook', + ], + }, + }, + }, + }, + 'bitcoinNode': { + 'name': 'Bitcoin Node Settings', + 'type': 'union', + 'uniqueBy': null, + 'description': 'The node settings', + 'default': 'internal', + 'changeWarning': 'Careful changing this', + 'tag': { + 'id': 'type', + 'name': 'Type', + 'variantNames': { + 'internal': 'Internal', + 'external': 'External', + }, + }, + 'variants': { + 'internal': { + 'lan-address': { + 'name': 'LAN Address', + 'type': 'pointer', + 'subtype': 'app', + 'target': 'lan-address', + 'app-id': 'bitcoind', + 'description': 'the lan address', + }, + }, + 'external': { + 'public-domain': { + 'name': 'Public Domain', + 'type': 'string', + 'description': 'the public address of the node', + 'nullable': false, + 'default': 'bitcoinnode.com', + 'pattern': '.*', + 'patternDescription': 'anything', + 'masked': false, + 'copyable': true, + }, + }, + }, + }, + 'port': { + 'name': 'Port', + 'type': 'number', + 'integral': true, + 'description': 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444', + 'nullable': false, + 'default': 8333, + 'range': '[0, 9999]', + }, + 'favoriteSlogan': { + 'name': 'Favorite Slogan', + 'type': 'string', + 'description': 'You most favorite slogan in the whole world, used for paying you.', + 'nullable': true, + 'masked': true, + 'copyable': true, + }, + 'rpcallowip': { + 'name': 'RPC Allowed IPs', + 'type': 'list', + 'subtype': 'string', + 'description': 'external ip addresses that are authorized to access your Bitcoin node', + 'changeWarning': 'Any IP you allow here will have RPC access to your Bitcoin node.', + 'range': '[1,10]', + 'default': [ + '192.168.1.1', + ], + 'spec': { + 'pattern': '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))', + 'patternDescription': 'must be a valid ipv4, ipv6, or domain name', + }, + }, + 'rpcauth': { + 'name': 'RPC Auth', + 'type': 'list', + 'subtype': 'string', + 'description': 'api keys that are authorized to access your Bitcoin node.', + 'range': '[0,*)', + 'default': [], + 'spec': { }, + }, + }, + // actual config + config: { + testnet: undefined, + objectList: undefined, + unionList: undefined, + randomEnum: 'option1', + favoriteNumber: 8, + secondaryNumbers: undefined, + rpcsettings: { + laws: null, + rpcpass: null, + rpcuser: '123', + rulemakers: [], + }, + advanced: { + notifications: ['call'], + }, + bitcoinNode: undefined, + port: 5959, + maxconnections: null, + rpcallowip: undefined, + rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'], + }, + rules: [], +} + +export const mockCupsDependentConfig = { + randomEnum: 'option1', + testnet: false, + favoriteNumber: 8, + secondaryNumbers: [13, 58, 20], + objectList: [], + unionList: [], + rpcsettings: { + laws: null, + rpcpass: null, + rpcuser: '123', + rulemakers: [], + }, + advanced: { + notifications: [], + }, + bitcoinNode: { type: 'internal' }, + port: 5959, + maxconnections: null, + rpcallowip: [], + rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'], +} \ No newline at end of file diff --git a/ui/src/app/services/api/mock-app-fixures.ts b/ui/src/app/services/api/mock-app-fixures.ts new file mode 100644 index 000000000..a65644e91 --- /dev/null +++ b/ui/src/app/services/api/mock-app-fixures.ts @@ -0,0 +1,289 @@ +import { AppStatus } from '../../models/app-model' +import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppDependency, BaseApp, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo } from '../../models/app-types' +export function toAvailablePreview (f: AppAvailableFull): AppAvailablePreview { + return { + id: f.id, + versionInstalled: f.versionInstalled, + status: f.status, + title: f.title, + descriptionShort: f.descriptionShort, + iconURL: f.iconURL, + versionLatest: f.versionLatest, + } +} + +export function toInstalledPreview (f: AppInstalledFull): AppInstalledPreview { + return { + id: f.id, + versionInstalled: f.versionInstalled, + status: f.status, + title: f.title, + iconURL: f.iconURL, + torAddress: f.torAddress, + } +} + +export function toServiceRequirement (f: BaseApp, o: Omit): AppDependency { + return { + id: f.id, + title: f.title, + iconURL: f.iconURL, + ...o, + } +} + +export function toServiceBreakage (f: BaseApp): DependentBreakage { + return { + id: f.id, + title: f.title, + iconURL: f.iconURL, + } +} + +export const bitcoinI: AppInstalledFull = { + id: 'bitcoind', + versionInstalled: '0.18.1', + title: 'Bitcoin Core', + torAddress: 'sample-bitcoin-tor-address-and-some-more-tor-address.onion', + status: AppStatus.STOPPED, + iconURL: 'assets/img/service-icons/bitcoind.png', + instructions: 'some instructions', + lastBackup: new Date().toISOString(), + configuredRequirements: [], + hasFetchedFull: true, +} + +export const lightningI: AppInstalledFull = { + id: 'c-lightning', + status: AppStatus.RUNNING, + title: 'C Lightning', + versionInstalled: '1.0.0', + torAddress: 'sample-bitcoin-tor-address-and-some-more-tor-address.onion', + iconURL: 'assets/img/service-icons/bitwarden.png', + instructions: 'some instructions', + lastBackup: new Date().toISOString(), + configuredRequirements: [ + toServiceRequirement(bitcoinI, + { + optional: 'you don\'t reeeeelly need this', + default: true, + versionSpec: '>= 0.1.2', + description: 'lightning needs bitcoin', + violation: null, + }), + ], + hasFetchedFull: true, +} + +export const cupsI: AppInstalledFull = { + id: 'cups', + versionInstalled: '2.1.0', + title: 'Cups Messenger', + torAddress: 'sample-cups-tor-address.onion', + status: AppStatus.BROKEN_DEPENDENCIES, + iconURL: 'assets/img/service-icons/cups.png', + + instructions: 'some instructions', + lastBackup: new Date().toISOString(), + configuredRequirements: [ + toServiceRequirement(lightningI, + { + optional: 'you don\'t reeeeelly need this', + default: true, + + versionSpec: '>= 0.1.2', + description: 'lightning needs bitcoin', + violation: { name: 'incompatible-version' }, + }), + toServiceRequirement(lightningI, + { + optional: 'you don\'t reeeeelly need this', + default: true, + + versionSpec: '>= 0.1.2', + description: 'lightning needs bitcoin', + violation: { name: 'incompatible-status', status: AppStatus.INSTALLING }, + }), + toServiceRequirement(lightningI, + { + optional: 'you don\'t reeeeelly need this', + default: true, + + versionSpec: '>= 0.1.2', + description: 'lightning needs bitcoin', + violation: { name: 'incompatible-config', ruleViolations: ['bro', 'seriously', 'fix this'] }, + }), + ], + hasFetchedFull: true, +} + +export const bitcoinA: AppAvailableFull = { + id: 'bitcoind', + versionLatest: '0.19.1.1', + versionInstalled: '0.19.0', + status: AppStatus.UNKNOWN, + title: 'Bitcoin Core', + descriptionShort: 'Bitcoin is an innovative payment network and new kind of money.', + iconURL: 'assets/img/service-icons/bitcoind.png', + releaseNotes: 'Bitcoin is an innovative payment network and new kind of money. Bitcoin utilizes a robust p2p network to garner decentralized consensus. Bitcoin is an innovative payment network and new kind of money. Bitcoin utilizes a robust p2p network to garner decentralized consensus. Bitcoin is an innovative payment network and new kind of money. Bitcoin utilizes a robust p2p network to garner decentralized consensus. Segit and more cool things!', + descriptionLong: 'Bitcoin is an innovative payment network and new kind of money. Bitcoin utilizes a robust p2p network to garner decentralized consensus.', + versions: ['0.19.1.1', '0.19.1', '0.19.0', '0.18.1', '0.17.0'], + versionViewing: '0.19.1', + serviceRequirements: [], +} + +export const lightningA: AppAvailableFull = { + id: 'c-lightning', + versionLatest: '1.0.1', + versionInstalled: null, + status: AppStatus.UNKNOWN, + title: 'C Lightning', + descriptionShort: 'Lightning is quick money things.', + iconURL: 'assets/img/service-icons/bitcoind.png', + releaseNotes: 'Finally it works', + descriptionLong: 'Lightning is an innovative payment network and new kind of money. Lightning utilizes a robust p2p network to garner decentralized consensus.', + versions: ['0.0.1', '0.8.0', '0.8.1', '1.0.0', '1.0.1'], + versionViewing: '1.0.1', + serviceRequirements: [ + toServiceRequirement(bitcoinA, { + optional: null, + default: true, + versionSpec: '>=0.19.0', + description: 'Lightning uses bitcoin under the hood', + violation: null, + }), + ], +} + +export const btcPayA: AppAvailableFull = { + id: 'btcPay', + versionLatest: '1.0.1', + versionInstalled: '1.0.1', + status: AppStatus.INSTALLING, + title: 'BTC Pay', + descriptionShort: 'BTC Pay is quick payment money things', + iconURL: 'assets/img/service-icons/bitcoind.png', + releaseNotes: 'Finally pay us Finally pay us Finally pay us Finally pay us Finally pay usFinally pay us', + descriptionLong: 'Btc Pay is an innovative payment network and new kind of money. Btc Pay utilizes a robust p2p network to garner decentralized consensus.', + versions: ['0.8.0', '0.8.1', '1.0.0', '1.0.1'], + versionViewing: '1.0.1', + serviceRequirements: [ + toServiceRequirement(bitcoinA, { + optional: null, + default: true, + versionSpec: '>0.19.0', + description: 'Lightning uses bitcoin under the hood', + violation: { name: 'incompatible-version' }, + }), + ], +} + +export const thunderA: AppAvailableFull = { + id: 'thunder', + versionLatest: '1.0.1', + versionInstalled: null, + status: AppStatus.UNKNOWN, + title: 'Thunder', + descriptionShort: 'Thunder is quick payment money things', + iconURL: 'assets/img/service-icons/bitcoind.png', + releaseNotes: 'Finally pay us', + descriptionLong: 'Thunder is an innovative payment network and new kind of money. Thunder utilizes a robust p2p network to garner decentralized consensus.', + versions: ['0.8.0', '0.8.1', '1.0.0', '1.0.1'], + versionViewing: '1.0.1', + serviceRequirements: [ + toServiceRequirement(bitcoinA, { + optional: null, + default: true, + versionSpec: '>0.19.0', + description: 'Thunder uses bitcoin under the hood', + violation: { name: 'incompatible-version' }, + }), + toServiceRequirement(lightningA, { + optional: null, + default: true, + versionSpec: '>=1.0.1', + description: 'Thunder uses lightning under the hood', + violation: { name: 'incompatible-version' }, + }), + toServiceRequirement(btcPayA, { + optional: 'Can be configured to use chase bank instead', + default: true, + versionSpec: '>=1.0.1', + description: 'Thunder can use btcpay under the hood', + violation: { name: 'missing' }, + }), + toServiceRequirement(btcPayA, { + optional: 'Can be configured to use chase bank instead', + default: true, + versionSpec: '>=1.0.1', + description: 'Thunder can use btcpay under the hood', + violation: { name: 'incompatible-status', status: AppStatus.INSTALLING }, + }), + ], +} + +export const cupsA: AppAvailableFull = { + id: 'cups', + versionLatest: '2.1.0', + versionInstalled: '2.1.0', + status: AppStatus.RUNNING, + title: 'Cups Messenger', + descriptionShort: 'P2P encrypted messaging over Tor.', + iconURL: 'assets/img/service-icons/cups.png', + releaseNotes: 'Segit and more cool things!', + descriptionLong: 'Bitcoin is an innovative payment network and new kind of money. Bitcoin utilizes a robust p2p network to garner decentralized consensus.', + versions: ['0.1.0', '0.1.1', '0.1.2', '1.0.0', '2.0.0', '2.1.0'], + versionViewing: '2.1.0', + serviceRequirements: [], +} + +export const bitwardenA: AppAvailableFull = { + id: 'bitwarden', + versionLatest: '0.1.1', + versionInstalled: null, + status: null, + title: 'Bitwarden', + descriptionShort: `Self-hosted password manager`, + iconURL: 'assets/img/service-icons/bitwarden.png', + releaseNotes: 'Passwords and shite!', + descriptionLong: 'Bitwarden is fun.', + versions: ['0.19.0', '0.18.1', '0.17.0'], + versionViewing: '0.1.1', + serviceRequirements: [ + toServiceRequirement(cupsA, { + optional: 'Can be configured to use chase bank instead', + default: true, + versionSpec: '>=1.0.0', + description: 'cups does great stuff for bitwarden', + violation: { name: 'incompatible-config', ruleViolations: ['change this value to that value', 'change this second value to something better']}, + }), + ], +} + +export const mockApiAppAvailableFull: { [appId: string]: AppAvailableFull; } = { + bitcoind: bitcoinA, + lightning: lightningA, + btcPay: btcPayA, + thunder: thunderA, + cups: cupsA, + bitwarden: bitwardenA, +} + +export const mockApiAppInstalledFull: { [appId: string]: AppInstalledFull; } = { + bitcoind: bitcoinI, + cups: cupsI, + lightning: lightningI, +} + +export const mockApiAppAvailableVersionInfo: AppAvailableVersionSpecificInfo = { + releaseNotes: 'Some older release notes that are not super important anymore.', + serviceRequirements: [], + versionViewing: '0.2.0', +} + +export const mockAppDependentBreakages: { breakages: DependentBreakage[] } = { + breakages: [ + toServiceBreakage(bitcoinI), + toServiceBreakage(cupsA), + ], +} \ No newline at end of file diff --git a/ui/src/app/services/auth.service.ts b/ui/src/app/services/auth.service.ts new file mode 100644 index 000000000..31aed59c1 --- /dev/null +++ b/ui/src/app/services/auth.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@angular/core' +import { BehaviorSubject, Subscription } from 'rxjs' +import { distinctUntilChanged } from 'rxjs/operators' +import { ApiService } from './api/api.service' +import { chill } from '../util/misc.util' +import { isUnauthorized } from '../util/web.util' +import { Storage } from '@ionic/storage' +import { StorageKeys } from '../models/storage-keys' + +export enum AuthState { + UNVERIFIED, + VERIFIED, + INITIALIZING, +} +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private readonly $authState$: BehaviorSubject = new BehaviorSubject(AuthState.INITIALIZING) + + constructor ( + private readonly api: ApiService, + private readonly storage: Storage, + ) { } + + peek (): AuthState { return this.$authState$.getValue() } + listen (callback: Partial<{ [k in AuthState]: () => any }>): Subscription { + return this.$authState$.pipe(distinctUntilChanged()).subscribe(s => { + return (callback[s] || chill)() + }) + } + + async login (password: string) { + try { + await this.api.postLogin(password) + await this.storage.set(StorageKeys.LOGGED_IN_KEY, true) + this.$authState$.next(AuthState.VERIFIED) + } catch (e) { + if (isUnauthorized(e)) { + this.$authState$.next(AuthState.UNVERIFIED) + throw { name: 'invalid', message: 'invalid credentials' } + } + console.error(`Failed login attempt`, e) + throw e + } + } + + async restoreCache (): Promise { + const loggedIn = await this.storage.get(StorageKeys.LOGGED_IN_KEY) + if (loggedIn) { + this.$authState$.next(AuthState.VERIFIED) + return AuthState.VERIFIED + } else { + this.$authState$.next(AuthState.UNVERIFIED) + return AuthState.UNVERIFIED + } + } + + async setAuthStateUnverified (): Promise { + this.$authState$.next(AuthState.UNVERIFIED) + } +} diff --git a/ui/src/app/services/config.service.ts b/ui/src/app/services/config.service.ts new file mode 100644 index 000000000..46d14c46b --- /dev/null +++ b/ui/src/app/services/config.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core' + +@Injectable({ + providedIn: 'root', +}) +export class ConfigService { + origin = removePort(removeProtocol(window.origin)) + version = require('../../../package.json').version + + api = { + useMocks: require('../../../use-mocks.json').useMocks, + url: '/api', + version: '/v0', + root: '', // empty will default to same origin + } + + isConsulateIos = window['platform'] === 'ios' + isConsulateAndroid = window['platform'] === 'android' + + isTor () : boolean { + return this.api.useMocks || this.origin.endsWith('.onion') + } +} + +function removeProtocol (str: string): string { + if (str.startsWith('http://')) return str.slice(7) + if (str.startsWith('https://')) return str.slice(8) + return str +} + +function removePort (str: string): string { + return str.split(':')[0] +} diff --git a/ui/src/app/services/emver.service.ts b/ui/src/app/services/emver.service.ts new file mode 100644 index 000000000..45d2f88a6 --- /dev/null +++ b/ui/src/app/services/emver.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core' + +@Injectable({ + providedIn: 'root', +}) +export class Emver { + private e: typeof import('@start9labs/emver') + constructor () { } + + async init () { + this.e = await import('@start9labs/emver') + } + + compare (lhs: string, rhs: string): number { + return this.e.compare(lhs, rhs) + } + + satisfies (version: string, range: string): boolean { + return this.e.satisfies(version, range) + } +} \ No newline at end of file diff --git a/ui/src/app/services/http.service.ts b/ui/src/app/services/http.service.ts new file mode 100644 index 000000000..3928a5eb4 --- /dev/null +++ b/ui/src/app/services/http.service.ts @@ -0,0 +1,118 @@ +import { Injectable } from '@angular/core' +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' +import { Observable, from, interval, race } from 'rxjs' +import { map, take } from 'rxjs/operators' +import { ConfigService } from './config.service' + +@Injectable({ + providedIn: 'root', +}) +export class HttpService { + constructor ( + private readonly http: HttpClient, + private readonly config: ConfigService, + ) { } + + async serverRequest (options: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise { + options.url = leadingSlash(`${this.config.api.url}${exists(overrides.version) ? overrides.version : this.config.api.version}${options.url}`) + if ( this.config.api.root && this.config.api.root !== '' ) { + options.url = `${this.config.api.root}${options.url}` + } + return this.request(options) + } + + async request (httpOpts: HttpOptions): Promise { + const { url, body, timeout, ...rest} = translateOptions(httpOpts) + let req: Observable<{ body: T }> + switch (httpOpts.method){ + case Method.GET: req = this.http.get(url, rest) as any; break + case Method.POST: req = this.http.post(url, body, rest) as any; break + case Method.PUT: req = this.http.put(url, body, rest) as any; break + case Method.PATCH: req = this.http.patch(url, body, rest) as any; break + case Method.DELETE: req = this.http.delete(url, rest) as any; break + } + + return (timeout ? withTimeout(req, timeout) : req) + .toPromise() + .then(res => res.body) + .catch(e => { console.error(e); throw humanReadableErrorMessage(e)}) + } +} + +function humanReadableErrorMessage (e: any): Error { + // server up, custom backend error + if (e.error && e.error.message) return { ...e, message: e.error.message } + if (e.message) return { ...e, message: e.message } + if (e.status && e.statusText) return { ...e, message: `${e.status} ${e.statusText}` } + return { ...e, message: `Unidentifiable HTTP exception` } +} + +function leadingSlash (url: string): string { + let toReturn = url + toReturn = toReturn.startsWith('/') ? toReturn : '/' + toReturn + toReturn = !toReturn.endsWith('/') ? toReturn : toReturn.slice(0, -1) + return toReturn +} + +export enum Method { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE', +} + +export interface HttpOptions { + withCredentials?: boolean + url: string + method: Method + params?: { + [param: string]: string | string[]; + } + data?: any + headers?: { + [key: string]: string; + } + readTimeout?: number +} + +export interface HttpJsonOptions { + headers?: HttpHeaders | { + [header: string]: string | string[]; + } + observe: 'events' + params?: HttpParams | { + [param: string]: string | string[]; + } + reportProgress?: boolean + responseType?: 'json' + withCredentials?: boolean + body?: any + url: string + timeout: number +} + +function translateOptions (httpOpts: HttpOptions): HttpJsonOptions { + return { + observe: 'events', + responseType: 'json', + reportProgress: false, + withCredentials: true, + headers: httpOpts.headers, + params: httpOpts.params, + body: httpOpts.data || { }, + url: httpOpts.url, + timeout: httpOpts.readTimeout, + } +} + +function withTimeout (req: Observable, timeout: number): Observable { + return race( + from(req.toPromise()), // this guarantees it only emits on completion, intermediary emissions are suppressed. + interval(timeout).pipe(take(1), map(() => { throw new Error('timeout') })), + ) +} + +function exists (str?: string): boolean { + return !!str || str === '' +} \ No newline at end of file diff --git a/ui/src/app/services/loader.service.ts b/ui/src/app/services/loader.service.ts new file mode 100644 index 000000000..11dd2559b --- /dev/null +++ b/ui/src/app/services/loader.service.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@angular/core' +import { concatMap, finalize } from 'rxjs/operators' +import { Observable, from, Subject } from 'rxjs' +import { fromAsync$, fromAsyncP, emitAfter$, fromSync$ } from '../util/rxjs.util' +import { LoadingController } from '@ionic/angular' +import { LoadingOptions } from '@ionic/core' + +@Injectable({ + providedIn: 'root', +}) +export class LoaderService { + private loadingOptions: LoadingOptions = defaultOptions() + constructor (private readonly loadingCtrl: LoadingController) { } + + private loader: HTMLIonLoadingElement + + public get ionLoader (): HTMLIonLoadingElement { + return this.loader + } + + public get ctrl () { + return this.loadingCtrl + } + + private setOptions (l: LoadingOptions): LoaderService { + this.loadingOptions = l + return this + } + + of (overrideOptions: LoadingOptions): LoaderService { + return new LoaderService(this.loadingCtrl).setOptions(Object.assign(defaultOptions(), overrideOptions)) + } + + displayDuring$ (o: Observable): Observable { + let shouldDisplay = true + const displayIfItsBeenAtLeast = 10 // ms + return fromAsync$( + async () => { + this.loader = await this.loadingCtrl.create(this.loadingOptions) + emitAfter$(displayIfItsBeenAtLeast).subscribe(() => { if (shouldDisplay) this.loader.present() }) + }, + ).pipe( + concatMap(() => o), + finalize(() => { + this.loader.dismiss(); shouldDisplay = false; this.loader = undefined + }), + ) + } + + displayDuringP (p: Promise): Promise { + return this.displayDuring$(from(p)).toPromise() + } + + displayDuringAsync (thunk: () => Promise): Promise { + return this.displayDuringP(fromAsyncP(thunk)) + } +} + +export function markAsLoadingDuring$ ($trigger$: Subject, o: Observable): Observable { + let shouldBeOn = true + const displayIfItsBeenAtLeast = 5 // ms + return fromSync$(() => { + emitAfter$(displayIfItsBeenAtLeast).subscribe(() => { if (shouldBeOn) $trigger$.next(true) }) + }).pipe( + concatMap(() => o), + finalize(() => { + $trigger$.next(false) + shouldBeOn = false + }), + ) +} + +export function markAsLoadingDuringP ($trigger$: Subject, p: Promise): Promise { + return markAsLoadingDuring$($trigger$, from(p)).toPromise() +} + +export function markAsLoadingDuringAsync ($trigger$: Subject, thunk: () => Promise): Promise { + return markAsLoadingDuringP($trigger$, fromAsyncP(thunk)) +} + + +const defaultOptions: () => LoadingOptions = () => ({ + spinner: 'lines', + cssClass: 'loader', + backdropDismiss: true, +}) diff --git a/ui/src/app/services/pwa-back.service.ts b/ui/src/app/services/pwa-back.service.ts new file mode 100644 index 000000000..d4681a0ac --- /dev/null +++ b/ui/src/app/services/pwa-back.service.ts @@ -0,0 +1,23 @@ +import { Router } from '@angular/router' +import { Injectable } from '@angular/core' +import { NavController } from '@ionic/angular' + +@Injectable({ + providedIn: 'root', +}) +export class PwaBackService { + constructor ( + private readonly router: Router, + private readonly nav: NavController, + ) { } + + // this will strip an entry from the path on navigation + back () { + return this.nav.back() + // this.router.navigate() + // const path = this.router.url.split('/').filter(a => a !== '') + // path.pop() + // this.router.navigate(['/', ...path], { replaceUrl: false }) + } +} + diff --git a/ui/src/app/services/server-config.service.ts b/ui/src/app/services/server-config.service.ts new file mode 100644 index 000000000..f9067dd36 --- /dev/null +++ b/ui/src/app/services/server-config.service.ts @@ -0,0 +1,114 @@ +import { Injectable } from '@angular/core' +import { ModalController } from '@ionic/angular' +import { AppConfigValuePage } from '../modals/app-config-value/app-config-value.page' +import { ApiService } from './api/api.service' +import { PropertySubject } from '../util/property-subject.util' +import { S9Server, ServerModel } from '../models/server-model' +import { ValueSpec } from '../app-config/config-types' + +@Injectable({ + providedIn: 'root', +}) +export class ServerConfigService { + server: PropertySubject + + constructor ( + private readonly modalCtrl: ModalController, + private readonly apiService: ApiService, + private readonly serverModel: ServerModel, + ) { + this.server = this.serverModel.watch() + } + + async presentModalValueEdit (key: string, add = false) { + const modal = await this.modalCtrl.create({ + backdropDismiss: false, + component: AppConfigValuePage, + presentingElement: await this.modalCtrl.getTop(), + componentProps: { + ...this.getConfigSpec(key), + value: add ? '' : this.server[key].getValue(), + }, + }) + + await modal.present() + } + + private getConfigSpec (key: string): SpecAndSaveFn { + const configSpec: { [key: string]: SpecAndSaveFn } = { + name: { + spec: { + type: 'string', + name: 'Device Name', + description: 'A unique label for this device.', + nullable: false, + // @TODO determine regex + // pattern: '', + patternDescription: 'Must be less than 40 characters', + masked: false, + copyable: true, + }, + saveFn: (val: string) => { + return this.apiService.patchServerConfig('name', val).then(() => this.serverModel.update({ name: val })) + }, + }, + // password: { + // spec: { + // type: 'string', + // name: 'Change Password', + // description: 'The master password for your Embassy. Must contain at least 128 bits of entropy.', + // nullable: false, + // // @TODO figure out how to confirm min entropy + // // pattern: '^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[*.!@#$%^&*\]).{12,32}$', + // patternDescription: 'Password too simple. Password must contain at least 128 bits of entroy.', + // changeWarning: 'Changing your password will have no affect on old backups. In order to restore old backups, you must provide the password that was used to create them.', + // masked: true, + // copyable: true, + // }, + // saveFn: (val: string) => { + // return this.apiService.patchServerConfig('password', val) + // }, + // }, + // alternativeRegistryUrl: { + // spec: { + // type: 'string', + // name: 'Marketplace URL', + // description: 'Used for connecting to an alternative service marketplace.', + // nullable: true, + // // @TODO regex for URL + // // pattern: '', + // patternDescription: 'Must be a valid URL', + // changeWarning: 'Downloading services from an alternative marketplace could result in malicious or harmful code being installed on your device.', + // masked: false, + // copyable: true, + // }, + // saveFn: (val: string) => { + // return this.apiService.patchServerConfig('alternativeRegistryUrl', val).then(() => this.serverModel.update({ alternativeRegistryUrl: val })) + // }, + // }, + ssh: { + spec: { + type: 'string', + name: 'SSH Key', + description: 'Add SSH keys to your Embassy to gain root access from the command line.', + nullable: false, + // @TODO regex for SSH Key + // pattern: '', + patternDescription: 'Must be a valid SSH key', + masked: true, + copyable: true, + }, + saveFn: (val: string) => { + return this.apiService.addSSHKey(val) + }, + }, + } + + return configSpec[key] + } +} + +interface SpecAndSaveFn { + spec: ValueSpec + saveFn: (val: string) => Promise +} diff --git a/ui/src/app/services/split-pane.service.ts b/ui/src/app/services/split-pane.service.ts new file mode 100644 index 000000000..bdc317c3f --- /dev/null +++ b/ui/src/app/services/split-pane.service.ts @@ -0,0 +1,10 @@ +import { BehaviorSubject } from 'rxjs' +import { Injectable } from '@angular/core' + +@Injectable({ + providedIn: 'root', +}) +export class SplitPaneTracker { + $menuFixedOpenOnLeft$: BehaviorSubject = new BehaviorSubject(false) + constructor () { } +} \ No newline at end of file diff --git a/ui/src/app/services/sync.notifier.ts b/ui/src/app/services/sync.notifier.ts new file mode 100644 index 000000000..0cd75fd7b --- /dev/null +++ b/ui/src/app/services/sync.notifier.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core' +import { ToastController, NavController } from '@ionic/angular' +import { ServerModel, S9Server } from '../models/server-model' + +@Injectable({ + providedIn: 'root', +}) +export class SyncNotifier { + constructor ( + private readonly toastCtrl: ToastController, + private readonly navCtrl: NavController, + private readonly serverModel: ServerModel, + ) { } + + async handleNotifications (server: Readonly): Promise { + const count = server.notifications.length + + if (!count) { return } + + let updates = { } as Partial + updates.badge = server.badge + count + updates.notifications = [] + + const toast = await this.toastCtrl.create({ + header: 'Embassy', + message: `${count} new notification${count === 1 ? '' : 's'}`, + position: 'bottom', + duration: 4000, + cssClass: 'notification-toast', + buttons: [ + { + side: 'start', + icon: 'close', + handler: () => { + return true + }, + }, + { + side: 'end', + text: 'View', + handler: () => { + this.navCtrl.navigateForward(['/notifications']) + }, + }, + ], + }) + await toast.present() + this.serverModel.update(updates) + } +} diff --git a/ui/src/app/services/sync.service.ts b/ui/src/app/services/sync.service.ts new file mode 100644 index 000000000..59dbd2d30 --- /dev/null +++ b/ui/src/app/services/sync.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@angular/core' +import { ServerModel } from '../models/server-model' +import { ApiService } from './api/api.service' +import { tryAll, pauseFor } from '../util/misc.util' +import { AppModel } from '../models/app-model' +import { SyncNotifier } from './sync.notifier' +import { BehaviorSubject, Observable, of, from, Subject, EMPTY } from 'rxjs' +import { switchMap, concatMap, catchError, delay, tap } from 'rxjs/operators' + +@Injectable({ + providedIn: 'root', +}) +export class SyncDaemon { + private readonly syncInterval = 5000 + private readonly $sync$ = new BehaviorSubject(false) + + // emits on every successful sync + private readonly $synced$ = new Subject() + + constructor ( + private readonly apiService: ApiService, + private readonly serverModel: ServerModel, + private readonly appModel: AppModel, + private readonly syncNotifier: SyncNotifier, + ) { + this.$sync$.pipe( + switchMap(go => go + ? this.sync().pipe(delay(this.syncInterval), tap(() => this.$sync$.next(true))) + : EMPTY, + ), + ).subscribe() + } + + start () { this.$sync$.next(true) } + stop () { this.$sync$.next(false) } + sync (): Observable { + return from(this.getServerAndApps()).pipe( + concatMap(() => this.syncNotifier.handleNotifications(this.serverModel.peek())), + tap(() => this.$synced$.next()), + catchError(e => of(console.error(`Exception in sync service`, e))), + ) + } + + watchSynced (): Observable { + return this.$synced$.asObservable() + } + + private async getServerAndApps (): Promise { + const now = new Date() + const [serverRes, appsRes] = await tryAll([ + this.apiService.getServer(), + pauseFor(250).then(() => this.apiService.getInstalledApps()), + ]) + + switch (serverRes.result) { + case 'resolve': { + this.serverModel.update(serverRes.value, now) + break + } + case 'reject': { + console.error(`get server request rejected with`, serverRes.value) + this.serverModel.markUnreachable() + break + } + } + + switch (appsRes.result) { + case 'resolve': { + this.appModel.syncCache(appsRes.value, now) + break + } + case 'reject': { + console.error(`get apps request rejected with`, appsRes.value) + this.appModel.markAppsUnreachable() + break + } + } + } +} \ No newline at end of file diff --git a/ui/src/app/services/tracking-modal-controller.service.ts b/ui/src/app/services/tracking-modal-controller.service.ts new file mode 100644 index 000000000..51feab9c5 --- /dev/null +++ b/ui/src/app/services/tracking-modal-controller.service.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable } from '@angular/core' +import { Observable, Subject } from 'rxjs' +import { ModalController } from '@ionic/angular' +import { ModalOptions } from '@ionic/core' +import { APP_CONFIG_COMPONENT_MAPPING } from '../modals/app-config-injectable/modal-injectable-token' +import { AppConfigComponentMapping } from '../modals/app-config-injectable/modal-injectable-type' +import { ValueSpec } from '../app-config/config-types' + +@Injectable({ + providedIn: 'root', +}) +export class TrackingModalController { + private modals: { [modalId: string] : HTMLIonModalElement} = { } + + private readonly $onDismiss$ = new Subject() + private readonly $onCreate$ = new Subject() + + constructor ( + private readonly modalCtrl: ModalController, + @Inject(APP_CONFIG_COMPONENT_MAPPING) private readonly appConfigComponentMapping: AppConfigComponentMapping, + ) { } + + async createConfigModal (o: Omit, type: ValueSpec['type']) { + const component = this.appConfigComponentMapping[type] + return this.create({ ...o, component }) + } + + async create (a: ModalOptions): Promise { + const modal = await this.modalCtrl.create(a) + this.modals[modal.id] = modal + this.$onCreate$.next(modal.id) + + modal.onWillDismiss().then(() => { + delete this.modals[modal.id] + this.$onDismiss$.next(modal.id) + }) + return modal + } + + dismissAll (): Promise { + return Promise.all( + Object.values(this.modals).map(m => m.dismiss()), + ) + } + + + dismiss (val?: any): Promise { + return this.modalCtrl.dismiss(val) + } + + onCreateAny$ (): Observable { + return this.$onCreate$.asObservable() + } + + onDismissAny$ (): Observable { + return this.$onDismiss$.asObservable() + } + + async getTop (): Promise { + return this.modalCtrl.getTop() + } + + get anyModals (): boolean { + return Object.keys(this.modals).length !== 0 + } + + get modalCount (): number { + return Object.keys(this.modals).length + } +} diff --git a/ui/src/app/util/cleanup.ts b/ui/src/app/util/cleanup.ts new file mode 100644 index 000000000..254b7a82d --- /dev/null +++ b/ui/src/app/util/cleanup.ts @@ -0,0 +1,15 @@ +import { Injectable, OnDestroy } from '@angular/core' +import { Subscription } from 'rxjs' + +@Injectable() +export abstract class Cleanup implements OnDestroy { + private toCleanup: Subscription[] = [] + + ngOnDestroy () { + this.toCleanup.forEach(s => s.unsubscribe()) + } + + cleanup (...s: Subscription[]) { + this.toCleanup.push(...s) + } +} diff --git a/ui/src/app/util/countries.json b/ui/src/app/util/countries.json new file mode 100644 index 000000000..e2c91e3d7 --- /dev/null +++ b/ui/src/app/util/countries.json @@ -0,0 +1,252 @@ +{ + "AD": "Andorra", + "AE": "United Arab Emirates", + "AF": "Afghanistan", + "AG": "Antigua and Barbuda", + "AI": "Anguilla", + "AL": "Albania", + "AM": "Armenia", + "AO": "Angola", + "AQ": "Antarctica", + "AR": "Argentina", + "AS": "American Samoa", + "AT": "Austria", + "AU": "Australia", + "AW": "Aruba", + "AX": "Aland Islands", + "AZ": "Azerbaijan", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BL": "Saint Barthelemy", + "BM": "Bermuda", + "BN": "Brunei", + "BO": "Bolivia", + "BQ": "Bonaire, Saint Eustatius and Saba ", + "BR": "Brazil", + "BS": "Bahamas", + "BT": "Bhutan", + "BV": "Bouvet Island", + "BW": "Botswana", + "BY": "Belarus", + "BZ": "Belize", + "CA": "Canada", + "CC": "Cocos Islands", + "CD": "Democratic Republic of the Congo", + "CF": "Central African Republic", + "CG": "Republic of the Congo", + "CH": "Switzerland", + "CI": "Ivory Coast", + "CK": "Cook Islands", + "CL": "Chile", + "CM": "Cameroon", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "CU": "Cuba", + "CV": "Cape Verde", + "CW": "Curacao", + "CX": "Christmas Island", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DE": "Germany", + "DJ": "Djibouti", + "DK": "Denmark", + "DM": "Dominica", + "DO": "Dominican Republic", + "DZ": "Algeria", + "EC": "Ecuador", + "EE": "Estonia", + "EG": "Egypt", + "EH": "Western Sahara", + "ER": "Eritrea", + "ES": "Spain", + "ET": "Ethiopia", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "FR": "France", + "GA": "Gabon", + "GB": "United Kingdom", + "GD": "Grenada", + "GE": "Georgia", + "GF": "French Guiana", + "GG": "Guernsey", + "GH": "Ghana", + "GI": "Gibraltar", + "GL": "Greenland", + "GM": "Gambia", + "GN": "Guinea", + "GP": "Guadeloupe", + "GQ": "Equatorial Guinea", + "GR": "Greece", + "GS": "South Georgia and the South Sandwich Islands", + "GT": "Guatemala", + "GU": "Guam", + "GW": "Guinea-Bissau", + "GY": "Guyana", + "HK": "Hong Kong", + "HM": "Heard Island and McDonald Islands", + "HN": "Honduras", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IM": "Isle of Man", + "IN": "India", + "IO": "British Indian Ocean Territory", + "IQ": "Iraq", + "IR": "Iran", + "IS": "Iceland", + "IT": "Italy", + "JE": "Jersey", + "JM": "Jamaica", + "JO": "Jordan", + "JP": "Japan", + "KE": "Kenya", + "KG": "Kyrgyzstan", + "KH": "Cambodia", + "KI": "Kiribati", + "KM": "Comoros", + "KN": "Saint Kitts and Nevis", + "KP": "North Korea", + "KR": "South Korea", + "KW": "Kuwait", + "KY": "Cayman Islands", + "KZ": "Kazakhstan", + "LA": "Laos", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LI": "Liechtenstein", + "LK": "Sri Lanka", + "LR": "Liberia", + "LS": "Lesotho", + "LT": "Lithuania", + "LU": "Luxembourg", + "LV": "Latvia", + "LY": "Libya", + "MA": "Morocco", + "MC": "Monaco", + "MD": "Moldova", + "ME": "Montenegro", + "MF": "Saint Martin", + "MG": "Madagascar", + "MH": "Marshall Islands", + "MK": "Macedonia", + "ML": "Mali", + "MM": "Myanmar", + "MN": "Mongolia", + "MO": "Macao", + "MP": "Northern Mariana Islands", + "MQ": "Martinique", + "MR": "Mauritania", + "MS": "Montserrat", + "MT": "Malta", + "MU": "Mauritius", + "MV": "Maldives", + "MW": "Malawi", + "MX": "Mexico", + "MY": "Malaysia", + "MZ": "Mozambique", + "NA": "Namibia", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PH": "Philippines", + "PK": "Pakistan", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "PN": "Pitcairn", + "PR": "Puerto Rico", + "PS": "Palestinian Territory", + "PT": "Portugal", + "PW": "Palau", + "PY": "Paraguay", + "QA": "Qatar", + "RE": "Reunion", + "RO": "Romania", + "RS": "Serbia", + "RU": "Russia", + "RW": "Rwanda", + "SA": "Saudi Arabia", + "SB": "Solomon Islands", + "SC": "Seychelles", + "SD": "Sudan", + "SE": "Sweden", + "SG": "Singapore", + "SH": "Saint Helena", + "SI": "Slovenia", + "SJ": "Svalbard and Jan Mayen", + "SK": "Slovakia", + "SL": "Sierra Leone", + "SM": "San Marino", + "SN": "Senegal", + "SO": "Somalia", + "SR": "Suriname", + "SS": "South Sudan", + "ST": "Sao Tome and Principe", + "SV": "El Salvador", + "SX": "Sint Maarten", + "SY": "Syria", + "SZ": "Swaziland", + "TC": "Turks and Caicos Islands", + "TD": "Chad", + "TF": "French Southern Territories", + "TG": "Togo", + "TH": "Thailand", + "TJ": "Tajikistan", + "TK": "Tokelau", + "TL": "East Timor", + "TM": "Turkmenistan", + "TN": "Tunisia", + "TO": "Tonga", + "TR": "Turkey", + "TT": "Trinidad and Tobago", + "TV": "Tuvalu", + "TW": "Taiwan", + "TZ": "Tanzania", + "UA": "Ukraine", + "UG": "Uganda", + "UM": "United States Minor Outlying Islands", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VA": "Vatican", + "VC": "Saint Vincent and the Grenadines", + "VE": "Venezuela", + "VG": "British Virgin Islands", + "VI": "U.S. Virgin Islands", + "VN": "Vietnam", + "VU": "Vanuatu", + "WF": "Wallis and Futuna", + "WS": "Samoa", + "XK": "Kosovo", + "YE": "Yemen", + "YT": "Mayotte", + "ZA": "South Africa", + "ZM": "Zambia", + "ZW": "Zimbabwe" +} \ No newline at end of file diff --git a/ui/src/app/util/map-subject.util.ts b/ui/src/app/util/map-subject.util.ts new file mode 100644 index 000000000..ae449fb87 --- /dev/null +++ b/ui/src/app/util/map-subject.util.ts @@ -0,0 +1,82 @@ +import { Subject, BehaviorSubject } from 'rxjs' +import { PropertySubject, initPropertySubject, complete, peekProperties, PropertySubjectId } from './property-subject.util' +import { NgZone } from '@angular/core' +import { both, diff } from './misc.util' + +export type Update = Partial & { + id: string +} +export type Delta = { action: 'add' | 'delete', id: string } | { action: 'update', id: string, effectedFields: Partial } + +export class MapSubject { + contents: { [id: string]: PropertySubject } = { } + $delta$ = new Subject>() + + constructor ( + private readonly zone: NgZone = new NgZone({ shouldCoalesceEventChangeDetection: true }), + ) { } + + get ids () : string[] { return Object.keys(this.contents) } + get all () : T[] { return this.ids.map(id => this.peek(id) as T) } + + getContents () : PropertySubjectId[] { + return Object.entries(this.contents).map( ([k, v]) => ({ id: k, subject: v })) + } + + add (t: T): void { + this.contents[t.id] = initPropertySubject(t) + this.$delta$.next({ action: 'add', id: t.id }) + } + + delete (id: string): void { + const t$ = this.contents[id] + if (!t$) return + complete(t$) + delete this.contents[id] + this.$delta$.next({ action: 'delete', id }) + } + + update (newValues: Update): void { + const t$ = this.contents[newValues.id] as PropertySubject + + if (!t$) { + this.contents[newValues.id] = initPropertySubject(newValues) as PropertySubject + return + } + + const effectedFields = { } + const oldKeys = Object.keys(t$) + const newKeys = Object.keys(newValues) + + const newKeysInUpdate = diff(newKeys, oldKeys) + newKeysInUpdate.forEach(keyToAdd => { + t$[keyToAdd] = new BehaviorSubject(newValues[keyToAdd]) + effectedFields[keyToAdd] = newValues[keyToAdd] + }) + + const keysToUpdate = both(newKeys, oldKeys) + keysToUpdate.forEach(keyToUpdate => { + const valueToUpdate = newValues[keyToUpdate] + if (JSON.stringify(t$[keyToUpdate].getValue()) !== JSON.stringify(valueToUpdate)) { + this.zone.run(() => t$[keyToUpdate].next(valueToUpdate)) + effectedFields[keyToUpdate] = newValues[keyToUpdate] + } + }) + + if (Object.keys(effectedFields).length) { + this.$delta$.next({ + action: 'update', + id: newValues.id, + effectedFields, + }) + } + } + + watch (id: string): undefined | PropertySubject { + return this.contents[id] + } + + peek (id: string): T | undefined { + return this.contents[id] && peekProperties(this.contents[id]) + } +} diff --git a/ui/src/app/util/metrics.util.ts b/ui/src/app/util/metrics.util.ts new file mode 100644 index 000000000..d93023428 --- /dev/null +++ b/ui/src/app/util/metrics.util.ts @@ -0,0 +1,192 @@ +import * as Ajv from 'ajv' +import { JsonPointer } from 'jsonpointerx' + +const ajv = new Ajv({ jsonPointers: true, allErrors: true, nullable: true }) +const ajvWithDefaults = new Ajv({ jsonPointers: true, allErrors: true, useDefaults: true, nullable: true, removeAdditional: 'failing' }) +const schemaV1 = { + 'type': 'object', + 'properties': { + 'name': { 'type': 'string' }, + 'value': { 'type': 'string' }, + 'description': { 'type': 'string', 'nullable': true, 'default': null }, + 'copyable': { 'type': 'boolean', 'default': false }, + 'qr': { 'type': 'boolean', 'default': false }, + }, + 'required': ['name', 'value', 'copyable', 'qr'], + 'additionalProperties': false, +} +const schemaV1Compiled = ajv.compile(schemaV1) +const schemaV1CompiledWithDefaults = ajvWithDefaults.compile(schemaV1) +const schemaV2 = { + 'anyOf': [ + { + 'type': 'object', + 'properties': { + 'type': { 'type': 'string', 'const': 'string' }, + 'value': { 'type': 'string' }, + 'description': { 'type': 'string', 'nullable': true, 'default': null }, + 'copyable': { 'type': 'boolean', 'default': false }, + 'qr': { 'type': 'boolean', 'default': false }, + 'masked': { 'type': 'boolean', 'default': false }, + }, + 'required': ['type', 'value', 'description', 'copyable', 'qr', 'masked'], + 'additionalProperties': false, + }, + { + 'type': 'object', + 'properties': { + 'type': { 'type': 'string', 'const': 'object' }, + 'value': { + 'type': 'object', + 'patternProperties': { + '^.*$': { + '$ref': '#', + }, + }, + }, + 'description': { 'type': 'string', 'nullable': true, 'default': null }, + + }, + 'required': ['type', 'value', 'description'], + 'additionalProperties': false, + }, + ], +} +const schemaV2Compiled = ajv.compile(schemaV2) +const schemaV2CompiledWithDefaults = ajvWithDefaults.compile(schemaV2) + +export function parseMetricsPermissive (metrics: any, errorCallback: (err: Error) => any = console.warn): AppMetrics { + if (typeof metrics !== 'object' || metrics === null) { + errorCallback(new TypeError(`${metrics} is not an object`)) + return { } + } + if (typeof metrics.version !== 'number' || !metrics.data) { + return Object.entries(metrics) + .filter(([_, value]) => { + if (typeof value === 'string') { + return true + } else { + errorCallback(new TypeError(`${value} is not a string`)) + return false + } + }) + .map(([name, value]) => ({ + name, + value: { + value: String(value), + description: null, + copyable: false, + qr: false, + masked: false, + }, + })) + .reduce((acc, { name, value }) => { + acc[name] = value + return acc + }, { }) + } + const typedMetrics = metrics as AppMetricsVersioned + switch (typedMetrics.version) { + case 1: + return parseMetricsV1Permissive(typedMetrics.data, errorCallback) + case 2: + return parseMetricsV2Permissive(typedMetrics.data, errorCallback) + default: + errorCallback(new Error(`unknown metrics version ${metrics.version}, attempting to parse as v2`)) + return parseMetricsV2Permissive(typedMetrics.data, errorCallback) + } +} + +function parseMetricsV1Permissive (metrics: AppMetricsV1, errorCallback: (err: Error) => any): AppMetrics { + return metrics.reduce((prev: AppMetricsV2, cur: AppMetricV1, idx: number) => { + schemaV1Compiled(cur) + if (schemaV1Compiled.errors) { + for (let err of schemaV1Compiled.errors) { + errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`)) + if (err.dataPath) { + JsonPointer.set(cur, err.dataPath, undefined) + } + } + if (!schemaV1CompiledWithDefaults(cur)) { + for (let err of schemaV1CompiledWithDefaults.errors) { + errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`)) + } + return prev + } + } + prev[cur.name] = { + type: 'string', + value: cur.value, + description: cur.description, + copyable: cur.copyable, + qr: cur.qr, + masked: false, + } + return prev + }, { }) +} + +function parseMetricsV2Permissive (metrics: AppMetricsV2, errorCallback: (err: Error) => any): AppMetrics { + return Object.entries(metrics).reduce((prev, [name, value], idx) => { + schemaV2Compiled(value) + if (schemaV2Compiled.errors) { + for (let err of schemaV2Compiled.errors) { + errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`)) + if (err.dataPath) { + JsonPointer.set(value, err.dataPath, undefined) + } + } + if (!schemaV2CompiledWithDefaults(value)) { + for (let err of schemaV2CompiledWithDefaults.errors) { + errorCallback(new Error(`/data/${idx}${err.dataPath}: ${err.message}`)) + } + return prev + } + } + prev[name] = value + return prev + }, { }) +} + +export type AppMetrics = AppMetricsV2 // chnage this type when updating versions + +export type AppMetricsVersioned = { + version: T, + data: AppMetricsVersionedData +} + +export type AppMetricsVersionedData = T extends 1 ? AppMetricsV1 : + T extends 2 ? AppMetricsV2 : + never + +interface AppMetricV1 { + name: string + value: string + description: string | null + copyable: boolean + qr: boolean +} + +type AppMetricsV1 = AppMetricV1[] + +interface AppMetricsV2 { + [name: string]: AppMetricString | AppMetricObject +} + +interface AppMetricBase { + type: 'string' | 'object' + description: string | null +} + +interface AppMetricString extends AppMetricBase { + type: 'string' + value: string + copyable: boolean + qr: boolean + masked: boolean +} + +interface AppMetricObject extends AppMetricBase { + type: 'object' + value: AppMetricsV2 +} \ No newline at end of file diff --git a/ui/src/app/util/misc.util.ts b/ui/src/app/util/misc.util.ts new file mode 100644 index 000000000..6ac3dcc18 --- /dev/null +++ b/ui/src/app/util/misc.util.ts @@ -0,0 +1,161 @@ +export type Omit = Pick> +export type PromiseRes = { result: 'resolve', value: T } | { result: 'reject', value: Error } + +import { OperatorFunction } from 'rxjs' +import { map } from 'rxjs/operators' + +export function trace (t: T): T { + console.log(`TRACE`, t) + return t +} + +// curried description. This allows e.g somePromise.thentraceDesc('my result')) +export function traceDesc (description: string): (t: T) => T { + return t => { + console.log(`TRACE`, description, t) + return t + } +} + +// for use in observables. This allows e.g. someObservable.pipe(traceM('my result')) +// the practical equivalent of `tap(t => console.log(t, description))` +export function traceWheel (description?: string): OperatorFunction { + return description ? map(traceDesc(description)) : map(trace) +} + +export function traceThrowDesc (description: string, t: T | undefined): T { + if (!t) throw new Error(description) + return t +} + +export function thenReturn (act1 : () => Promise, t: T): Promise { + return act1().then(() => t) +} + +export function modulateTime (ts: Date, count: number, unit: 'days' | 'hours' | 'minutes' | 'seconds' ) { + const ms = inMs(count, unit) + const toReturn = new Date(ts) + toReturn.setMilliseconds( toReturn.getMilliseconds() + ms) + return toReturn +} + +export function inMs ( count: number, unit: 'days' | 'hours' | 'minutes' | 'seconds' ) { + switch (unit){ + case 'seconds' : return count * 1000 + case 'minutes' : return inMs(count * 60, 'seconds') + case 'hours' : return inMs(count * 60, 'minutes') + case 'days' : return inMs(count * 24, 'hours') + } +} + +export async function tryAll ( promises: [Promise, Promise]): Promise<[PromiseRes, PromiseRes]> +export async function tryAll ( promises: Promise[] ): Promise[]> { + return Promise.all(promises.map( + p => p + .then (r => ({ result: 'resolve' as 'resolve', value: r })) + .catch(e => ({ result: 'reject' as 'reject', value: e })), + )) +} + +// arr1 - arr2 +export function diff (arr1: T[], arr2: T[]): T[] { + return arr1.filter(x => !arr2.includes(x)) +} + +// arr1 & arr2 +export function both (arr1: T[], arr2: T[]): T[] { + return arr1.filter(x => arr2.includes(x)) +} + +export async function doForAtLeast (promises: Promise[], minTime: number): Promise { + const returned = await Promise.all(promises.concat(pauseFor(minTime))) + returned.pop() + return returned +} + +export function isEmptyObject (obj: object): boolean { + if (!obj) return true + return Object.keys(obj).length === 0 && obj.constructor === Object +} + +export function pauseFor (ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +export type Valued = { [s: string]: T } + +export function toObject (t: T[], map: (t0: T) => string): Valued { + return t.reduce( (acc, next) => { + acc[map(next)] = next + return acc + }, { } as Valued) +} + +export function toDedupObject (t: T[], t2: T[], map: (t0: T) => string): Valued { + return toObject(t.concat(t2), map) +} + +export function update (t: Valued, u: Valued): Valued { + return { ...t, ...u} +} + +export function fromObject (o : Valued): T[] { + return Object.values(o) +} + +export function deepCloneUnknown (value: T): T { + if (typeof value !== 'object' || value === null) { + return value + } + if (Array.isArray(value)) { + return deepCloneArray(value) + } + return deepCloneObject(value) +} + +export function deepCloneObject (source: T) { + const result = { } + Object.keys(source).forEach(key => { + const value = source[key] + result[key] = deepCloneUnknown(value) + }, { }) + return result as T +} + +export function deepCloneArray (collection: any) { + return collection.map(value => { + return deepCloneUnknown(value) + }) +} + +export function partitionArray (ts: T[], condition: (t: T) => boolean): [T[], T[]] { + const yes = [] as T[] + const no = [] as T[] + ts.forEach(t => { + if (condition(t)) { + yes.push(t) + } else { + no.push(t) + } + }) + return [yes, no] +} + +export const chill = () => { } +export const chillAsync = async () => { } + +export function uniqueBy (ts: T[], uniqueBy: (t: T) => string, prioritize: (t1: T, t2: T) => T) { + return Object.values(ts.reduce((acc, next) => { + const previousValue = acc[uniqueBy(next)] + if (previousValue) { + acc[uniqueBy(next)] = prioritize(acc[uniqueBy(next)], previousValue) + } else { + acc[uniqueBy(next)] = previousValue + } + return acc + }, { })) +} + +export function capitalizeFirstLetter (string: string): string { + return string.charAt(0).toUpperCase() + string.slice(1) +} \ No newline at end of file diff --git a/ui/src/app/util/property-subject.util.ts b/ui/src/app/util/property-subject.util.ts new file mode 100644 index 000000000..51f8431b4 --- /dev/null +++ b/ui/src/app/util/property-subject.util.ts @@ -0,0 +1,49 @@ +import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs' +import { map } from 'rxjs/operators' + +export type PropertySubjectId = { + id: string + subject: PropertySubject +} + +export type PropertySubject = { + [k in keyof T]: BehaviorSubject +} + +// better type information than Object.entries without the return type cast +export function asLabelledList (p : PropertySubject): [string, BehaviorSubject][] { + return Object.entries(p) +} + +export function peekProperties (ps: PropertySubject) : T { + return asLabelledList(ps).reduce( (acc, [key, value]) => { + acc[key] = value.getValue() + return acc + }, { } as T) +} + +export function initPropertySubject (t: T): PropertySubject { + return Object.entries(t).reduce( (acc, [k, v]) => { + acc[k] = new BehaviorSubject(v) + return acc + }, { } as PropertySubject ) +} + +export function withKey (k: string, v: BehaviorSubject): Observable<[string, V]> { + return combineLatest([of(k), v]) +} + +export function toObservable (t: PropertySubject): Observable { + return combineLatest( + asLabelledList(t as any).map(([k, p]) => withKey(k, p)), + ).pipe(map( kvPairs => { + return kvPairs.reduce( (acc, [k, v]) => { + acc[k] = v + return acc + }, { }) as T + })) +} + +export function complete (t: PropertySubject): void { + asLabelledList(t as any).forEach(p => p[1].complete() ) +} diff --git a/ui/src/app/util/rxjs.util.ts b/ui/src/app/util/rxjs.util.ts new file mode 100644 index 000000000..e4e2a9288 --- /dev/null +++ b/ui/src/app/util/rxjs.util.ts @@ -0,0 +1,53 @@ +import { Observable, from, interval, race, OperatorFunction, Observer, BehaviorSubject } from 'rxjs' +import { take, map, switchMap, delay, tap } from 'rxjs/operators' + +export function fromAsync$ (async: (s: S) => Promise, s: S): Observable +export function fromAsync$ (async: () => Promise): Observable +export function fromAsync$ (async: (s: S) => Promise, s?: S): Observable { + return from(async(s as S)) +} + +export function fromAsyncP (async: () => Promise): Promise +export function fromAsyncP (async: (s: S) => Promise, s?: S): Promise { + return async(s as S) +} + +// emits + completes after ms +export function emitAfter$ (ms: number): Observable { + return interval(ms).pipe(take(1)) +} + +export function throwIn (timeout: number): OperatorFunction { + return o => race( + o, + emitAfter$(timeout).pipe(map(() => { throw new Error('timeout') } ))) +} + +export const squash = map(() => { }) + +export function fromSync$ (sync: (s: S) => T, s: S): Observable +export function fromSync$ (sync: () => T): Observable +export function fromSync$ (sync: (s: S) => T, s?: S): Observable { + return new Observable( (subscriber: Observer) => { + try { + subscriber.next(sync(s as S)) + subscriber.complete() + } catch (e) { + subscriber.error(e) + } + }) +} + +export function onCooldown (cooldown: number, o: () => Observable): Observable { + + const $trigger$ = new BehaviorSubject(true) + $trigger$.subscribe(t => console.log('triggering', t)) + return $trigger$.pipe( + switchMap(_ => + o().pipe( + delay(cooldown), + tap(() => $trigger$.next(true)), + ), + ), + ) +} diff --git a/ui/src/app/util/status-rendering.ts b/ui/src/app/util/status-rendering.ts new file mode 100644 index 000000000..fe00a0bf6 --- /dev/null +++ b/ui/src/app/util/status-rendering.ts @@ -0,0 +1,31 @@ +import { AppStatus } from 'src/app/models/app-model' +import { ServerStatus } from 'src/app/models/server-model' + +export const ServerStatusRendering: { + [k in ServerStatus]: { display: string; color: string; showDots: boolean; } +} = { + [ServerStatus.UNKNOWN]: { display: 'Connecting', color: 'dark', showDots: true }, + [ServerStatus.UNREACHABLE]: { display: 'Unreachable', color: 'danger', showDots: false }, + [ServerStatus.NEEDS_CONFIG]: { display: 'Needs Config', color: 'warning', showDots: false }, + [ServerStatus.RUNNING]: { display: 'Connected', color: 'success', showDots: false }, + [ServerStatus.UPDATING]: { display: 'Updating', color: 'primary', showDots: true }, +} + +export const AppStatusRendering: { + [k in AppStatus]: { display: string; color: string; showDots: boolean; style?: string; } +} = { + [AppStatus.UNKNOWN]: { display: 'Connecting', color: 'dark', showDots: true }, + [AppStatus.REMOVING]: { display: 'Removing', color: 'dark', showDots: true }, + [AppStatus.CRASHED]: { display: 'Crashing', color: 'danger', showDots: true }, + [AppStatus.NEEDS_CONFIG]: { display: 'Needs Config', color: 'warning', showDots: false }, + [AppStatus.RUNNING]: { display: 'Running', color: 'success', showDots: false }, + [AppStatus.UNREACHABLE]: { display: 'Unreachable', color: 'danger', showDots: false }, + [AppStatus.STOPPED]: { display: 'Not Running', color: 'medium', showDots: false }, + [AppStatus.CREATING_BACKUP]: { display: 'Backing Up', color: 'dark', showDots: true }, + [AppStatus.RESTORING_BACKUP]: { display: 'Restoring', color: 'dark', showDots: true }, + [AppStatus.INSTALLING]: { display: 'Installing', color: 'primary', showDots: true }, + [AppStatus.DEAD]: { display: 'Dead', color: 'danger', showDots: false }, + [AppStatus.BROKEN_DEPENDENCIES]: { display: 'Dependency Issue', color: 'warning', showDots: false }, + [AppStatus.STOPPING]: { display: 'Stopping', color: 'dark', showDots: true }, + [AppStatus.RESTARTING]: { display: 'Restarting', color: 'dark', showDots: true }, +} diff --git a/ui/src/app/util/types.util.ts b/ui/src/app/util/types.util.ts new file mode 100644 index 000000000..745ec5f6e --- /dev/null +++ b/ui/src/app/util/types.util.ts @@ -0,0 +1 @@ +export type Replace = Omit & { [k in withKey]: T[key] } diff --git a/ui/src/app/util/web.util.ts b/ui/src/app/util/web.util.ts new file mode 100644 index 000000000..c98d72a69 --- /dev/null +++ b/ui/src/app/util/web.util.ts @@ -0,0 +1,32 @@ +import { HttpErrorResponse } from '@angular/common/http' + +export async function copyToClipboard (str: string): Promise { + if (window.isSecureContext) { + return navigator.clipboard.writeText(str) + .then(() => { + return true + }) + .catch(err => { + return false + }) + } else { + const el = document.createElement('textarea') + el.value = str + el.setAttribute('readonly', '') + el.style.position = 'absolute' + el.style.left = '-9999px' + document.body.appendChild(el) + el.select() + const copy = document.execCommand('copy') + document.body.removeChild(el) + return copy + } +} + +export function isUnauthorized (e: HttpErrorResponse): boolean { + return !!e.status && 401 === e.status +} + +export function isBadRequest (e: HttpErrorResponse): boolean { + return !!e.status && 400 === e.status +} diff --git a/ui/src/app/util/webview.context.ts b/ui/src/app/util/webview.context.ts new file mode 100644 index 000000000..bb4987dc1 --- /dev/null +++ b/ui/src/app/util/webview.context.ts @@ -0,0 +1,8 @@ +import WebviewContext from '@start9labs/ambassador-sdk/dist/webview-context' + +export const webviewContext = new WebviewContext(async (method: string, data: any) => { + throw new Error (`${method} UNIMPLEMENTED`) + // switch(method){ + // case 'getConfigValue': throw new Error ('getConfigValue UNIMPLEMENTED') + // } +}) \ No newline at end of file diff --git a/ui/src/assets/favicon.ico b/ui/src/assets/favicon.ico new file mode 100644 index 000000000..db0b75111 Binary files /dev/null and b/ui/src/assets/favicon.ico differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-Black.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-Black.ttf new file mode 100644 index 000000000..437b1157c Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-Black.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-BlackItalic.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-BlackItalic.ttf new file mode 100644 index 000000000..52348354c Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-BlackItalic.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-Bold.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-Bold.ttf new file mode 100644 index 000000000..221819bca Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-Bold.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-BoldItalic.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-BoldItalic.ttf new file mode 100644 index 000000000..9ae2bd240 Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-BoldItalic.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-ExtraBold.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-ExtraBold.ttf new file mode 100644 index 000000000..80ea8061b Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-ExtraBold.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf new file mode 100644 index 000000000..6c961e1cc Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-ExtraLight.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-ExtraLight.ttf new file mode 100644 index 000000000..ca0bbb656 Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-ExtraLight.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf new file mode 100644 index 000000000..f3c1559ec Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-Italic.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-Italic.ttf new file mode 100644 index 000000000..eb4232a0c Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-Italic.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-Light.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-Light.ttf new file mode 100644 index 000000000..990857de8 Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-Light.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-LightItalic.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-LightItalic.ttf new file mode 100644 index 000000000..209604046 Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-LightItalic.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-Medium.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-Medium.ttf new file mode 100644 index 000000000..6e079f698 Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-Medium.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-MediumItalic.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-MediumItalic.ttf new file mode 100644 index 000000000..0dc3ac9c2 Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-MediumItalic.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-Regular.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-Regular.ttf new file mode 100644 index 000000000..8d443d5d5 Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-Regular.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-SemiBold.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-SemiBold.ttf new file mode 100644 index 000000000..f8a43f2b2 Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-SemiBold.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf new file mode 100644 index 000000000..336c56ec0 Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-Thin.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-Thin.ttf new file mode 100644 index 000000000..b9858757e Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-Thin.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/Montserrat-ThinItalic.ttf b/ui/src/assets/fonts/Montserrat/Montserrat-ThinItalic.ttf new file mode 100644 index 000000000..e488998ec Binary files /dev/null and b/ui/src/assets/fonts/Montserrat/Montserrat-ThinItalic.ttf differ diff --git a/ui/src/assets/fonts/Montserrat/OFL.txt b/ui/src/assets/fonts/Montserrat/OFL.txt new file mode 100644 index 000000000..7881887b7 --- /dev/null +++ b/ui/src/assets/fonts/Montserrat/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/ui/src/assets/fonts/Open_Sans/LICENSE.txt b/ui/src/assets/fonts/Open_Sans/LICENSE.txt new file mode 100644 index 000000000..75b52484e --- /dev/null +++ b/ui/src/assets/fonts/Open_Sans/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ui/src/assets/fonts/Open_Sans/OpenSans-Bold.ttf b/ui/src/assets/fonts/Open_Sans/OpenSans-Bold.ttf new file mode 100644 index 000000000..efdd5e84a Binary files /dev/null and b/ui/src/assets/fonts/Open_Sans/OpenSans-Bold.ttf differ diff --git a/ui/src/assets/fonts/Open_Sans/OpenSans-BoldItalic.ttf b/ui/src/assets/fonts/Open_Sans/OpenSans-BoldItalic.ttf new file mode 100644 index 000000000..9bf9b4e97 Binary files /dev/null and b/ui/src/assets/fonts/Open_Sans/OpenSans-BoldItalic.ttf differ diff --git a/ui/src/assets/fonts/Open_Sans/OpenSans-ExtraBold.ttf b/ui/src/assets/fonts/Open_Sans/OpenSans-ExtraBold.ttf new file mode 100644 index 000000000..67fcf0fb2 Binary files /dev/null and b/ui/src/assets/fonts/Open_Sans/OpenSans-ExtraBold.ttf differ diff --git a/ui/src/assets/fonts/Open_Sans/OpenSans-ExtraBoldItalic.ttf b/ui/src/assets/fonts/Open_Sans/OpenSans-ExtraBoldItalic.ttf new file mode 100644 index 000000000..086722809 Binary files /dev/null and b/ui/src/assets/fonts/Open_Sans/OpenSans-ExtraBoldItalic.ttf differ diff --git a/ui/src/assets/fonts/Open_Sans/OpenSans-Italic.ttf b/ui/src/assets/fonts/Open_Sans/OpenSans-Italic.ttf new file mode 100644 index 000000000..117856707 Binary files /dev/null and b/ui/src/assets/fonts/Open_Sans/OpenSans-Italic.ttf differ diff --git a/ui/src/assets/fonts/Open_Sans/OpenSans-Light.ttf b/ui/src/assets/fonts/Open_Sans/OpenSans-Light.ttf new file mode 100644 index 000000000..6580d3a16 Binary files /dev/null and b/ui/src/assets/fonts/Open_Sans/OpenSans-Light.ttf differ diff --git a/ui/src/assets/fonts/Open_Sans/OpenSans-LightItalic.ttf b/ui/src/assets/fonts/Open_Sans/OpenSans-LightItalic.ttf new file mode 100644 index 000000000..1e0c33198 Binary files /dev/null and b/ui/src/assets/fonts/Open_Sans/OpenSans-LightItalic.ttf differ diff --git a/ui/src/assets/fonts/Open_Sans/OpenSans-Regular.ttf b/ui/src/assets/fonts/Open_Sans/OpenSans-Regular.ttf new file mode 100644 index 000000000..29bfd35a2 Binary files /dev/null and b/ui/src/assets/fonts/Open_Sans/OpenSans-Regular.ttf differ diff --git a/ui/src/assets/fonts/Open_Sans/OpenSans-SemiBold.ttf b/ui/src/assets/fonts/Open_Sans/OpenSans-SemiBold.ttf new file mode 100644 index 000000000..54e7059cf Binary files /dev/null and b/ui/src/assets/fonts/Open_Sans/OpenSans-SemiBold.ttf differ diff --git a/ui/src/assets/fonts/Open_Sans/OpenSans-SemiBoldItalic.ttf b/ui/src/assets/fonts/Open_Sans/OpenSans-SemiBoldItalic.ttf new file mode 100644 index 000000000..aebcf1421 Binary files /dev/null and b/ui/src/assets/fonts/Open_Sans/OpenSans-SemiBoldItalic.ttf differ diff --git a/ui/src/assets/img/issue-bulb.png b/ui/src/assets/img/issue-bulb.png new file mode 100644 index 000000000..28ee73514 Binary files /dev/null and b/ui/src/assets/img/issue-bulb.png differ diff --git a/ui/src/assets/img/off-bulb.png b/ui/src/assets/img/off-bulb.png new file mode 100644 index 000000000..338f43878 Binary files /dev/null and b/ui/src/assets/img/off-bulb.png differ diff --git a/ui/src/assets/img/running-bulb.png b/ui/src/assets/img/running-bulb.png new file mode 100644 index 000000000..4f23b21d4 Binary files /dev/null and b/ui/src/assets/img/running-bulb.png differ diff --git a/ui/src/assets/img/service-icons/bitcoind.png b/ui/src/assets/img/service-icons/bitcoind.png new file mode 100644 index 000000000..26ab11e1d Binary files /dev/null and b/ui/src/assets/img/service-icons/bitcoind.png differ diff --git a/ui/src/assets/img/service-icons/bitwarden.png b/ui/src/assets/img/service-icons/bitwarden.png new file mode 100644 index 000000000..04d8a9052 Binary files /dev/null and b/ui/src/assets/img/service-icons/bitwarden.png differ diff --git a/ui/src/assets/img/service-icons/btc-rpc-proxy.png b/ui/src/assets/img/service-icons/btc-rpc-proxy.png new file mode 100644 index 000000000..78e57d65e Binary files /dev/null and b/ui/src/assets/img/service-icons/btc-rpc-proxy.png differ diff --git a/ui/src/assets/img/service-icons/c-lightning.png b/ui/src/assets/img/service-icons/c-lightning.png new file mode 100644 index 000000000..9b53c1a5c Binary files /dev/null and b/ui/src/assets/img/service-icons/c-lightning.png differ diff --git a/ui/src/assets/img/service-icons/cups.png b/ui/src/assets/img/service-icons/cups.png new file mode 100644 index 000000000..3b1f72bb0 Binary files /dev/null and b/ui/src/assets/img/service-icons/cups.png differ diff --git a/ui/src/assets/img/service-icons/filebrowser.png b/ui/src/assets/img/service-icons/filebrowser.png new file mode 100644 index 000000000..6728fb3c6 Binary files /dev/null and b/ui/src/assets/img/service-icons/filebrowser.png differ diff --git a/ui/src/assets/img/service-icons/lightning-terminal.png b/ui/src/assets/img/service-icons/lightning-terminal.png new file mode 100644 index 000000000..8187bb96a Binary files /dev/null and b/ui/src/assets/img/service-icons/lightning-terminal.png differ diff --git a/ui/src/assets/img/service-icons/lnd.png b/ui/src/assets/img/service-icons/lnd.png new file mode 100644 index 000000000..37a0ffceb Binary files /dev/null and b/ui/src/assets/img/service-icons/lnd.png differ diff --git a/ui/src/assets/img/service-icons/pastebin.png b/ui/src/assets/img/service-icons/pastebin.png new file mode 100644 index 000000000..876352955 Binary files /dev/null and b/ui/src/assets/img/service-icons/pastebin.png differ diff --git a/ui/src/assets/img/service-icons/ride-the-lightning.png b/ui/src/assets/img/service-icons/ride-the-lightning.png new file mode 100644 index 000000000..f77e30852 Binary files /dev/null and b/ui/src/assets/img/service-icons/ride-the-lightning.png differ diff --git a/ui/src/assets/img/warning-bulb.png b/ui/src/assets/img/warning-bulb.png new file mode 100644 index 000000000..394f9ea72 Binary files /dev/null and b/ui/src/assets/img/warning-bulb.png differ diff --git a/ui/src/assets/logo-full.png b/ui/src/assets/logo-full.png new file mode 100644 index 000000000..d693f7259 Binary files /dev/null and b/ui/src/assets/logo-full.png differ diff --git a/ui/src/assets/logo.png b/ui/src/assets/logo.png new file mode 100644 index 000000000..5364a7dc5 Binary files /dev/null and b/ui/src/assets/logo.png differ diff --git a/ui/src/environments/environment.prod.ts b/ui/src/environments/environment.prod.ts new file mode 100644 index 000000000..970e25bd7 --- /dev/null +++ b/ui/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true, +} diff --git a/ui/src/environments/environment.ts b/ui/src/environments/environment.ts new file mode 100644 index 000000000..5c68c17ab --- /dev/null +++ b/ui/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false, +} + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/ui/src/global.scss b/ui/src/global.scss new file mode 100644 index 000000000..98e9be390 --- /dev/null +++ b/ui/src/global.scss @@ -0,0 +1,280 @@ +/* + * App Global CSS + * ---------------------------------------------------------------------------- + * Put style rules here that you want to apply globally. These styles are for + * the entire app and not just one component. Additionally, this file can be + * used as an entry point to import other CSS/Sass files to be included in the + * output CSS. + * For more information on global stylesheets, visit the documentation: + * https://ionicframework.com/docs/layout/global-stylesheets + */ + +/* Core CSS required for Ionic components to work properly */ +@import "~@ionic/angular/css/core.css"; + +/* Basic CSS for apps built with Ionic */ +@import "~@ionic/angular/css/normalize.css"; +@import "~@ionic/angular/css/structure.css"; +@import "~@ionic/angular/css/typography.css"; +@import '~@ionic/angular/css/display.css'; + +/* Optional CSS utils that can be commented out */ +@import "~@ionic/angular/css/padding.css"; +@import "~@ionic/angular/css/float-elements.css"; +@import "~@ionic/angular/css/text-alignment.css"; +@import "~@ionic/angular/css/text-transformation.css"; +@import "~@ionic/angular/css/flex-utils.css"; + +.ios ion-title { + padding-inline-start: 60px; + padding-inline-end: 60px; +} + +.select-change-warning .alert-sub-title { + color: var(--ion-color-warning) +} + +.break-all { + word-break: break-all; +} + +.loader { + --spinner-color: var(--ion-color-warning) !important; +} + +.loader-ontop-of-all { + --spinner-color: var(--ion-color-warning) !important; + z-index: 40000 !important; +} + +.alert-danger { + color: var(--ion-color-danger) !important; + & ion-icon { + color: var(--ion-color-danger) !important; + } +} + +.borderless { + border-style: none; +} + +.fab-button { + margin: 20px; + --background: transparent; + --border-color: var(--ion-color-primary); + --border-style: solid; + --border-width: 2px; +} + +.help-button { + margin: 0 8px 0 0; +} + +.item-interactive { + --highlight-background: transparent !important; +} + +.alert-config-value { + .alert-message.sc-ion-alert-md { + color: var(--ion-color-danger) !important; + } +} + +.center { + display: block; + margin: auto; + height: 100%; +} + +.notification-toast { + --background: var(--ion-color-light); + --button-color: var(--ion-color-primary); + --border-color: var(--ion-color-warning); + --border-style: solid; + --border-width: 2px; + --color: white; +} + +.sublist-spinner { + text-align: center; + margin-top: 40px; +} + +.alert-radio-label.sc-ion-alert-md { + white-space: normal !important; +} + +.alert-radio-label.sc-ion-alert-ios { + white-space: normal !important; +} + +ion-title { + font-family: 'Montserrat'; + font-weight: unset; +} + +ion-item { + --border-width: 0; +} + +ion-textarea { + margin-top: 0; + padding-left: 12px; +} + +ion-note { + max-width: 140px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +ion-badge { + font-weight: bold; +} + +ion-item-divider { + --background: transparent; +} + +ion-infinite-scroll ion-infinite-scroll-content { + --color: var(--ion-color-warning) !important; +} + +ion-action-sheet { + --backdrop-opacity: 0.75 !important; +} + +ion-alert { + --backdrop-opacity: 0.75 !important; +} + +ion-loading { + --backdrop-opacity: 0.75 !important; +} + +* { + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +ion-popover { + --background: var(--ion-color-warning) !important; + + ion-backdrop { + --backdrop-opacity: 0.45 !important; + } +} + +.text-ellipses { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.modal-wrapper { + position: absolute; + height: 90%!important; + top: 5%; + width: 90%!important; + left: 5%; + display: block; +} + +@media (min-width:1000px) { + .modal-wrapper { + position: absolute; + height: 80% !important; + top: 10%; + width: 60% !important; + left: 20%; + display: block; + } +} + +ion-slides { + .slider-wrapper { + height: 100%; + width: 100% + } +} + +.qr-popover { + --width: auto; + --background: transparent !important; +} + +ion-loading { + z-index: 100 !important; +} + +ion-modal { + --box-shadow: 3px 4px 14px 3px rgba(255,255,255,0.3) !important; +} + +ion-modal:first-of-type { + --backdrop-opacity: 0.75 !important; +} + + +.swiper-pagination { + position: fixed; + bottom: 0px; + padding-bottom: 3px; +} + +ion-avatar { + --border-radius: var(--icon-border-radius); +} + +.no-white-space { + --white-space: 0; + --box-shadow: 3px 3px 10px var(--ion-color-primary); +} + +.notifier-item { + margin: 12px; + margin-top: 0px; + border-radius: 12px; + // kills the lines + --border-width: 0; + --inner-border-width: 0; +} + +.full-page-spinner { + min-height: 80vh; + align-items: center; + color: white; + margin: 0px 30px; + display: grid; + grid-template-rows: 35% 15%; +} + +.recommendation-item { + margin: 10px; + border-style: solid; + border-width: 1px; + border-style: groove; + border-color: dimgrey; + border-radius: 10px; + box-shadow: 4px 4px 16px var(--ion-color-primary); + --padding-start: 10px; +} + +// .divider { +// margin-top: 15px; +// color: var(--ion-color-medium); +// font-size: medium; +// padding-left: 10px; +// font-weight: unset; +// } + +ion-item-divider { + margin-top: 15px; + color: var(--ion-color-medium); + font-size: medium; + padding-left: 10px; + font-weight: unset; +} \ No newline at end of file diff --git a/ui/src/globals.d.ts b/ui/src/globals.d.ts new file mode 100644 index 000000000..5172a75b6 --- /dev/null +++ b/ui/src/globals.d.ts @@ -0,0 +1 @@ +declare module '*.md' \ No newline at end of file diff --git a/ui/src/index.html b/ui/src/index.html new file mode 100644 index 000000000..d81986a53 --- /dev/null +++ b/ui/src/index.html @@ -0,0 +1,22 @@ + + + + + + Embassy + + + + + + + + + + + + + + + + diff --git a/ui/src/main.ts b/ui/src/main.ts new file mode 100644 index 000000000..3d8855b64 --- /dev/null +++ b/ui/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core' +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' + +import { AppModule } from './app/app.module' +import { environment } from './environments/environment' + +if (environment.production) { + enableProdMode() +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)) diff --git a/ui/src/polyfills.ts b/ui/src/polyfills.ts new file mode 100644 index 000000000..034407983 --- /dev/null +++ b/ui/src/polyfills.ts @@ -0,0 +1,70 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags.ts'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +(window as any).global = window +global.Buffer = global.Buffer || require('buffer').Buffer; +(window as any).process = { env: { DEBUG: undefined }, browser: true } + +import './zone-flags' + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ + +import 'zone.js/dist/zone' // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/ui/src/theme/variables.scss b/ui/src/theme/variables.scss new file mode 100644 index 000000000..d710b5ad7 --- /dev/null +++ b/ui/src/theme/variables.scss @@ -0,0 +1,273 @@ +// Ionic Variables and Theming. For more info, please see: +// http://ionicframework.com/docs/theming/ + +/** Ionic CSS Variables **/ +:root { + --ion-font-family: 'Open Sans'; + /** primary **/ + --ion-color-primary: #3880ff; + --ion-color-primary-rgb: 56, 128, 255; + --ion-color-primary-contrast: #ffffff; + --ion-color-primary-contrast-rgb: 255, 255, 255; + --ion-color-primary-shade: #3171e0; + --ion-color-primary-tint: #4c8dff; + + /** secondary **/ + --ion-color-secondary: #3dc2ff; + --ion-color-secondary-rgb: 61, 194, 255; + --ion-color-secondary-contrast: #ffffff; + --ion-color-secondary-contrast-rgb: 255, 255, 255; + --ion-color-secondary-shade: #36abe0; + --ion-color-secondary-tint: #50c8ff; + + /** tertiary **/ + --ion-color-tertiary: #5260ff; + --ion-color-tertiary-rgb: 82, 96, 255; + --ion-color-tertiary-contrast: #ffffff; + --ion-color-tertiary-contrast-rgb: 255, 255, 255; + --ion-color-tertiary-shade: #4854e0; + --ion-color-tertiary-tint: #6370ff; + + /** success **/ + --ion-color-success: #2dd36f; + --ion-color-success-rgb: 45, 211, 111; + --ion-color-success-contrast: #ffffff; + --ion-color-success-contrast-rgb: 255, 255, 255; + --ion-color-success-shade: #28ba62; + --ion-color-success-tint: #42d77d; + + /** warning **/ + --ion-color-warning: #ffc409; + --ion-color-warning-rgb: 255, 196, 9; + --ion-color-warning-contrast: #000000; + --ion-color-warning-contrast-rgb: 0, 0, 0; + --ion-color-warning-shade: #e0ac08; + --ion-color-warning-tint: #ffca22; + + /** danger **/ + --ion-color-danger: #eb445a; + --ion-color-danger-rgb: 235, 68, 90; + --ion-color-danger-contrast: #ffffff; + --ion-color-danger-contrast-rgb: 255, 255, 255; + --ion-color-danger-shade: #cf3c4f; + --ion-color-danger-tint: #ed576b; + + /** dark **/ + --ion-color-dark: #222428; + --ion-color-dark-rgb: 34, 36, 40; + --ion-color-dark-contrast: #ffffff; + --ion-color-dark-contrast-rgb: 255, 255, 255; + --ion-color-dark-shade: #1e2023; + --ion-color-dark-tint: #383a3e; + + /** medium **/ + --ion-color-medium: #92949c; + --ion-color-medium-rgb: 146, 148, 156; + --ion-color-medium-contrast: #ffffff; + --ion-color-medium-contrast-rgb: 255, 255, 255; + --ion-color-medium-shade: #808289; + --ion-color-medium-tint: #9d9fa6; + + /** light **/ + --ion-color-light: #f4f5f8; + --ion-color-light-rgb: 244, 245, 248; + --ion-color-light-contrast: #000000; + --ion-color-light-contrast-rgb: 0, 0, 0; + --ion-color-light-shade: #d7d8da; + --ion-color-light-tint: #f5f6f9; + + --icon-border-radius: 10px +} + +body.dark { + --ion-color-primary: #428cff; + --ion-color-primary-rgb: 66,140,255; + --ion-color-primary-contrast: #ffffff; + --ion-color-primary-contrast-rgb: 255,255,255; + --ion-color-primary-shade: #3a7be0; + --ion-color-primary-tint: #5598ff; + + --ion-color-secondary: #50c8ff; + --ion-color-secondary-rgb: 80,200,255; + --ion-color-secondary-contrast: #ffffff; + --ion-color-secondary-contrast-rgb: 255,255,255; + --ion-color-secondary-shade: #46b0e0; + --ion-color-secondary-tint: #62ceff; + + --ion-color-tertiary: #6a64ff; + --ion-color-tertiary-rgb: 106,100,255; + --ion-color-tertiary-contrast: #ffffff; + --ion-color-tertiary-contrast-rgb: 255,255,255; + --ion-color-tertiary-shade: #5d58e0; + --ion-color-tertiary-tint: #7974ff; + + --ion-color-success: #2fdf75; + --ion-color-success-rgb: 47,223,117; + --ion-color-success-contrast: #000000; + --ion-color-success-contrast-rgb: 0,0,0; + --ion-color-success-shade: #29c467; + --ion-color-success-tint: #44e283; + + --ion-color-warning: #ffd534; + --ion-color-warning-rgb: 255,213,52; + --ion-color-warning-contrast: #000000; + --ion-color-warning-contrast-rgb: 0,0,0; + --ion-color-warning-shade: #e0bb2e; + --ion-color-warning-tint: #ffd948; + + --ion-color-danger: #ff4961; + --ion-color-danger-rgb: 255,73,97; + --ion-color-danger-contrast: #ffffff; + --ion-color-danger-contrast-rgb: 255,255,255; + --ion-color-danger-shade: #e04055; + --ion-color-danger-tint: #ff5b71; + + --ion-color-dark: #f4f5f8; + --ion-color-dark-rgb: 244,245,248; + --ion-color-dark-contrast: #000000; + --ion-color-dark-contrast-rgb: 0,0,0; + --ion-color-dark-shade: #d7d8da; + --ion-color-dark-tint: #f5f6f9; + + --ion-color-medium: #989aa2; + --ion-color-medium-rgb: 152,154,162; + --ion-color-medium-contrast: #000000; + --ion-color-medium-contrast-rgb: 0,0,0; + --ion-color-medium-shade: #86888f; + --ion-color-medium-tint: #a2a4ab; + + --ion-color-light: #222428; + --ion-color-light-rgb: 34,36,40; + --ion-color-light-contrast: #ffffff; + --ion-color-light-contrast-rgb: 255,255,255; + --ion-color-light-shade: #1e2023; + --ion-color-light-tint: #383a3e; + + --ion-color-start9: #FF4960; +} + +/* + * iOS Dark Theme + * ------------------------------------------- + */ + +.ios body.dark { + --ion-background-color: #000000; + --ion-background-color-rgb: 0,0,0; + + --ion-text-color: #ffffff; + --ion-text-color-rgb: 255,255,255; + + --ion-color-step-50: #0d0d0d; + --ion-color-step-100: #1a1a1a; + --ion-color-step-150: #262626; + --ion-color-step-200: #333333; + --ion-color-step-250: #404040; + --ion-color-step-300: #4d4d4d; + --ion-color-step-350: #595959; + --ion-color-step-400: #666666; + --ion-color-step-450: #737373; + --ion-color-step-500: #808080; + --ion-color-step-550: #8c8c8c; + --ion-color-step-600: #999999; + --ion-color-step-650: #a6a6a6; + --ion-color-step-700: #b3b3b3; + --ion-color-step-750: #bfbfbf; + --ion-color-step-800: #cccccc; + --ion-color-step-850: #d9d9d9; + --ion-color-step-900: #e6e6e6; + --ion-color-step-950: #f2f2f2; + + --ion-item-background: #1e1e1e; + + --ion-toolbar-background: #1f1f1f; +} + + +/* + * Material Design Dark Theme + * ------------------------------------------- + */ + +.md body.dark { + --ion-background-color: #121212; + --ion-background-color-rgb: 18,18,18; + + --ion-text-color: #ffffff; + --ion-text-color-rgb: 255,255,255; + + --ion-border-color: #222222; + + --ion-color-step-50: #1e1e1e; + --ion-color-step-100: #2a2a2a; + --ion-color-step-150: #363636; + --ion-color-step-200: #414141; + --ion-color-step-250: #4d4d4d; + --ion-color-step-300: #595959; + --ion-color-step-350: #656565; + --ion-color-step-400: #717171; + --ion-color-step-450: #7d7d7d; + --ion-color-step-500: #898989; + --ion-color-step-550: #949494; + --ion-color-step-600: #a0a0a0; + --ion-color-step-650: #acacac; + --ion-color-step-700: #b8b8b8; + --ion-color-step-750: #c4c4c4; + --ion-color-step-800: #d0d0d0; + --ion-color-step-850: #dbdbdb; + --ion-color-step-900: #e7e7e7; + --ion-color-step-950: #f3f3f3; + + --ion-item-background: #1e1e1e; + + --ion-toolbar-background: #1f1f1f; +} + +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: normal; + src: url('../assets/fonts/Montserrat/Montserrat-Regular.ttf'); +} + +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: bold; + src: url('../assets/fonts/Montserrat/Montserrat-Bold.ttf'); +} + +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: thin; + src: url('../assets/fonts/Montserrat/Montserrat-Light.ttf'); +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: normal; + src: url('../assets/fonts/Open_Sans/OpenSans-Regular.ttf'); +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: bold; + src: url('../assets/fonts/Open_Sans/OpenSans-Bold.ttf'); +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 500; + src: url('../assets/fonts/Open_Sans/OpenSans-SemiBold.ttf'); +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: thin; + src: url('../assets/fonts/Open_Sans/OpenSans-Light.ttf'); +} diff --git a/ui/src/zone-flags.ts b/ui/src/zone-flags.ts new file mode 100644 index 000000000..07aca7176 --- /dev/null +++ b/ui/src/zone-flags.ts @@ -0,0 +1,5 @@ +/** + * Prevents Angular change detection from + * running with certain Web Component callbacks + */ +(window as any).__Zone_disable_customElements = true diff --git a/ui/test/assets/bitcoind.png b/ui/test/assets/bitcoind.png new file mode 100644 index 000000000..26ab11e1d Binary files /dev/null and b/ui/test/assets/bitcoind.png differ diff --git a/ui/test/assets/bitwarden.png b/ui/test/assets/bitwarden.png new file mode 100644 index 000000000..04d8a9052 Binary files /dev/null and b/ui/test/assets/bitwarden.png differ diff --git a/ui/test/assets/btc-rpc-proxy.png b/ui/test/assets/btc-rpc-proxy.png new file mode 100644 index 000000000..78e57d65e Binary files /dev/null and b/ui/test/assets/btc-rpc-proxy.png differ diff --git a/ui/test/assets/c-lightning.png b/ui/test/assets/c-lightning.png new file mode 100644 index 000000000..9b53c1a5c Binary files /dev/null and b/ui/test/assets/c-lightning.png differ diff --git a/ui/test/assets/cups.png b/ui/test/assets/cups.png new file mode 100644 index 000000000..3b1f72bb0 Binary files /dev/null and b/ui/test/assets/cups.png differ diff --git a/ui/test/assets/filebrowser.png b/ui/test/assets/filebrowser.png new file mode 100644 index 000000000..6728fb3c6 Binary files /dev/null and b/ui/test/assets/filebrowser.png differ diff --git a/ui/test/assets/lightning-terminal.png b/ui/test/assets/lightning-terminal.png new file mode 100644 index 000000000..8187bb96a Binary files /dev/null and b/ui/test/assets/lightning-terminal.png differ diff --git a/ui/test/assets/lnd.png b/ui/test/assets/lnd.png new file mode 100644 index 000000000..37a0ffceb Binary files /dev/null and b/ui/test/assets/lnd.png differ diff --git a/ui/test/assets/pastebin.png b/ui/test/assets/pastebin.png new file mode 100644 index 000000000..876352955 Binary files /dev/null and b/ui/test/assets/pastebin.png differ diff --git a/ui/test/assets/ride-the-lightning.png b/ui/test/assets/ride-the-lightning.png new file mode 100644 index 000000000..f77e30852 Binary files /dev/null and b/ui/test/assets/ride-the-lightning.png differ diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 000000000..b55f72a21 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./out-tsc/app", + "sourceMap": true, + "declaration": false, + "module": "esnext", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "strictNullChecks": false, + "target": "es2015", + "paths": { + "stream": ["./node_modules/stream-browserify"], + "crypto": ["./node_modules/crypto-browserify"], + "vm": ["./node_modules/vm-browserify"] + }, + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2018", + "dom" + ] + }, + "angularCompilerOptions": { + "strictInjectionParameters": true + }, + "files": [ + "src/main.ts", + "src/polyfills.ts", + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/ui/tslint.json b/ui/tslint.json new file mode 100644 index 000000000..6f53dafb6 --- /dev/null +++ b/ui/tslint.json @@ -0,0 +1,47 @@ +{ + "rules": { + "no-unused-variable": true, + "no-unused-expression": true, + "space-before-function-paren": true, + "semicolon": [ + true, + "never" + ], + "no-trailing-whitespace": true, + "indent": [ + true, + "spaces", + 2 + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-module", + "check-operator", + "check-separator", + "check-rest-spread", + "check-type", + "check-typecast", + "check-type-operator", + "check-preblock", + "check-postbrace" + ], + "trailing-comma": [ + true, + { + "multiline": { + "objects": "always", + "arrays": "always", + "functions": "always", + "typeLiterals": "never" + }, + "singleline": "never" + } + ], + "quotemark": [ + true, + "single" + ] + } +} diff --git a/ui/use-mocks.json b/ui/use-mocks.json new file mode 100644 index 000000000..3425efc8a --- /dev/null +++ b/ui/use-mocks.json @@ -0,0 +1,3 @@ +{ + "useMocks": false +}

{{error.moreInfo.buttonText}}