diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 000000000..e80d8f04b --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,4 @@ +# We use Electron v18. +# This is the version of Chrome it requires: +# https://github.com/electron/releases#releases +chrome >= 100 diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..ee1bee2bc --- /dev/null +++ b/.clang-format @@ -0,0 +1,2 @@ +BasedOnStyle: Google +ColumnLimit: 100 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..b512c09d4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..417d89a93 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..d01fa2835 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +/build/ +node_modules/ diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..5f7d214f6 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,114 @@ +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:import/typescript" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint", "compat", "import"], + "rules": { + "compat/compat": "error", + "import/no-restricted-paths": [ + "error", + { + "zones": [ + // this means that in the src/shadowbox/infrastructure folder, + // you can't import any code, + // with the exception of other files within the src/shadowbox/infrastructure folder + { + "target": "./src/shadowbox/infrastructure", + "from": ".", + "except": ["./src/shadowbox/infrastructure", "./node_modules"] + }, + { + "target": "./src/server_manager/infrastructure", + "from": ".", + "except": ["./src/server_manager/infrastructure", "./node_modules"] + }, + { + "target": "./src/metrics_server/infrastructure", + "from": ".", + "except": ["./src/metrics_server/infrastructure", "./node_modules"] + }, + // similar to above but for src/shadowbox/model, but you can use files from both the + // src/shadowbox/model and src/shadowbox/infrastructure paths + { + "target": "./src/shadowbox/model", + "from": ".", + "except": ["./src/shadowbox/model", "./src/shadowbox/infrastructure", "./node_modules"] + }, + { + "target": "./src/server_manager/model", + "from": ".", + "except": [ + "./src/server_manager/model", + "./src/server_manager/infrastructure", + "./node_modules" + ] + } + // TODO(daniellacosse): fix ui_component-specific import violations + // { + // "target": "./src/server_manager/web_app/ui_components", + // "from": "./src/server_manager/model" + // }, + // { + // "target": "./src/server_manager/web_app/ui_components", + // "from": "./src/server_manager/web_app", + // "except": ["./ui_components"] + // } + ] + } + ], + "no-prototype-builtins": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_" + } + ] + }, + "overrides": [ + { + "files": [ + "check-version-tracker.js", + "rollup-common.js", + "rollup.config.js", + "web-test-runner.config.js" + ], + "env": { + "node": true + } + }, + { + "files": ["packages/lit-html/src/test/version-stability_test.js"], + "env": { + "mocha": true + } + }, + { + "files": [ + "*_test.ts", + "packages/labs/ssr/custom_typings/node.d.ts", + "packages/labs/ssr/src/test/integration/tests/**", + "packages/labs/ssr/src/lib/util/parse5-utils.ts" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } + } + ] +} diff --git a/.gitallowed b/.gitallowed new file mode 100644 index 000000000..1fefcd38b --- /dev/null +++ b/.gitallowed @@ -0,0 +1 @@ +946220775492-a5v6bsdin6o7ncnqn34snuatmrp7dqh0.apps.googleusercontent.com diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..3b66b7db4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +* @Jigsaw-Code/outline-dev + +/src/server_manager/model/ @fortuna +/src/shadowbox/ @fortuna diff --git a/.github/codeql/config.yml b/.github/codeql/config.yml new file mode 100644 index 000000000..ab510459c --- /dev/null +++ b/.github/codeql/config.yml @@ -0,0 +1,5 @@ +query-filters: + - exclude: + id: js/disabling-certificate-validation + - exclude: + id: js/missing-rate-limiting diff --git a/.github/workflows/build_and_test_debug.yml b/.github/workflows/build_and_test_debug.yml new file mode 100644 index 000000000..eabbdadb9 --- /dev/null +++ b/.github/workflows/build_and_test_debug.yml @@ -0,0 +1,208 @@ +name: Build and Test + +concurrency: + group: ${{ github.head_ref || github.ref }} + cancel-in-progress: true + +on: + pull_request: + types: + - opened + - synchronize + push: + branches: + - master + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + + - name: Install NPM Dependencies + run: npm ci + + - name: Lint + run: npm run lint + + shadowbox: + name: Shadowbox + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + + - name: Install NPM Dependencies + run: npm ci + + - name: Shadowbox Debug Build + run: npm run action shadowbox/server/build + + - name: Shadowbox Unit Test + run: npm run action shadowbox/test + + - name: Shadowbox Integration Test + run: npm run action shadowbox/integration_test/run + + manual-install-script: + name: Manual Install Script + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install Outline Server + run: ./src/server_manager/install_scripts/install_server.sh --hostname localhost + + - name: Test API + run: 'curl --silent --fail --insecure $(grep "apiUrl" /opt/outline/access.txt | cut -d: -f 2-)/server' + + metrics-server: + name: Metrics Server + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + + - name: Install NPM Dependencies + run: npm ci + + - name: Metrics Server Debug Build + run: npm run action metrics_server/build + + - name: Metrics Server Test + run: npm run action metrics_server/test + + sentry-webhook: + name: Sentry Webhook + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + + - name: Install NPM Dependencies + run: npm ci + + - name: Sentry Webhook Debug Build + run: npm run action sentry_webhook/build + + - name: Sentry Webhook Test + run: npm run action sentry_webhook/test + + manager-web-test: + name: Manager Web Test + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + + - name: Install NPM Dependencies + run: npm ci + + - name: Manager Web Test + run: npm run action server_manager/test + + manager-linux-debug-build: + name: Manager Linux Debug Build + runs-on: ubuntu-latest + needs: manager-web-test + env: + SENTRY_DSN: debug + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + + - name: Install NPM Dependencies + run: npm ci + + - name: Build Linux Manager + run: npm run action server_manager/electron_app/build linux + + manager-windows-debug-build: + name: Manager Windows Debug Build + runs-on: windows-2019 + needs: manager-web-test + env: + SENTRY_DSN: debug + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + + - name: Install NPM Dependencies + run: npm ci + + - name: Build Windows Manager + run: npm run action server_manager/electron_app/build windows + + manager-mac-debug-build: + name: Manager MacOS Debug Build + runs-on: macos-11 + needs: manager-web-test + env: + SENTRY_DSN: debug + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: npm + + - name: Install NPM Dependencies + run: npm ci + + - name: Set XCode Version + run: sudo xcode-select -switch /Applications/Xcode_13.2.app + + - name: Build MacOS Manager + run: npm run action server_manager/electron_app/build macos diff --git a/.github/workflows/codeql_vulnerability_analysis.yml b/.github/workflows/codeql_vulnerability_analysis.yml new file mode 100644 index 000000000..637e96830 --- /dev/null +++ b/.github/workflows/codeql_vulnerability_analysis.yml @@ -0,0 +1,39 @@ +name: "CodeQL analysis" + +on: + pull_request: + types: + - opened + - edited + - synchronize + push: + branches: + - master + schedule: + - cron: '0 0 * * *' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: javascript + queries: +security-and-quality + config-file: ./.github/codeql/config.yml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/pull_request_checks.yml b/.github/workflows/pull_request_checks.yml new file mode 100644 index 000000000..cc3b535b1 --- /dev/null +++ b/.github/workflows/pull_request_checks.yml @@ -0,0 +1,71 @@ +name: Pull Request Checks + +on: + pull_request: + types: + - opened + + # This `edited` flag is why we need a separate workflow - + # specifying edited here causes this job to be re-run whenever someone edits the PR title/description. + + # If we had the debug builds in this file, they would run unnecessarily, costing resources. + - edited + + - synchronize + +jobs: + name_check: + name: Pull Request Name Check + runs-on: ubuntu-latest + permissions: + pull-requests: read + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Clone Repository + uses: actions/checkout@v2 + + - name: Install Node + uses: actions/setup-node@v2.2.0 + with: + node-version: 18 + cache: npm + + - name: Install NPM Dependencies + run: npm ci + + - name: Ensure Commitizen Format + uses: JulienKode/pull-request-name-linter-action@98794a8b815ec05560813c42e55fd8d32d3fd248 + + size_label: + name: Change Size Label + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + # size-label-action fails to work when coming from a fork: + # https://github.com/pascalgn/size-label-action/issues/10 + - if: ${{ !github.event.pull_request.head.repo.fork }} + name: Apply Size Label + uses: pascalgn/size-label-action@a4655c448bb838e8d73b81e97fd0831bb4cbda1e + env: + IGNORED: | + LICENSE + package-lock.json + resources/* + src/server_manager/messages/* + src/server_manager/images/* + third_party/* + with: + sizes: > + { + "0": "XS", + "64": "S", + "128": "M", + "256": "L", + "512": "XL", + "1024": "XXL" + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4b647d935 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/build/ +node_modules/ +/src/server_manager/install_scripts/do_install_script.ts +/src/server_manager/install_scripts/gcp_install_script.ts +.vscode/ +.idea/ +third_party/shellcheck/download/ +macos-signing-certificate.p12 \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..35d740d17 --- /dev/null +++ b/.npmrc @@ -0,0 +1,6 @@ +; Enforces that the user is `npm install`ing with the correct node version. +engine-strict=true + +; Workaround for conflict between the default location(s) of node-forge and the +; location expected by Typescript, Jasmine, and Electron. +prefer-dedupe=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..a77793ecc --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +lts/hydrogen diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..9e0427c45 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +/build/ +node_modules/ +/src/server_manager/messages/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..de41e34cb --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "bracketSpacing": false, + "printWidth": 100 +} diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 000000000..be0ef9573 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,10 @@ +# Configuration for `npx shellcheck` and IDEs + +# Enable relative path references +source-path=SCRIPTDIR + +# The style guide says "quote your variables" +enable=quote-safe-variables + +# The style guide says 'prefer "${var}" over "$var"' +enable=require-variable-braces diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..ae319c70a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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/README.md b/README.md new file mode 100644 index 000000000..4ba82d345 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# Outline Server + +![Build and Test](https://github.com/Jigsaw-Code/outline-server/actions/workflows/build_and_test_debug.yml/badge.svg?branch=master) [![Mattermost](https://badgen.net/badge/Mattermost/Outline%20Community/blue)](https://community.internetfreedomfestival.org/community/channels/outline-community) [![Reddit](https://badgen.net/badge/Reddit/r%2Foutlinevpn/orange)](https://www.reddit.com/r/outlinevpn/) + +This repository has all the code needed to create and manage Outline servers on +DigitalOcean. An Outline server runs instances of Shadowsocks proxies and +provides an API used by the Outline Manager application. + +Go to https://getoutline.org for ready-to-use versions of the software. **To join our Outline Community, [sign up for the IFF Mattermost](https://internetfreedomfestival.org/wiki/index.php/IFF_Mattermost).** + +## Components + +The system comprises the following components: + +- **Outline Server**: a proxy server that runs a Shadowsocks instance for each + access key and a REST API to manage the access keys. The Outline Server runs + in a Docker container in the host machine. + + See [`src/shadowbox`](src/shadowbox) + +- **Outline Manager:** an [Electron](https://electronjs.org/) application that + can create Outline Servers on the cloud and talks to their access key + management API to manage who has access to the server. + + See [`src/server_manager`](src/server_manager) + +- **Metrics Server:** a REST service that the Outline Server talks to + if the user opts-in to anonymous metrics sharing. + + See [`src/metrics_server`](src/metrics_server) + +## Code Prerequisites + +In order to build and run the code, you need the following installed: + +- [Node](https://nodejs.org/en/download/) LTS (`lts/hydrogen`, version `18.16.0`) +- [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) (version `9.5.1`) +- Manager-specific + - [Wine](https://www.winehq.org/download), if you would like to generate binaries for Windows. +- Server-specific + - [Docker](https://docs.docker.com/engine/install/), to build the Docker image and to run the integration test. + - [docker-compose](https://docs.docker.com/compose/install/), to run the integration test. + +> 💡 NOTE: if you have `nvm` installed, run `nvm use` to switch to the correct node version! + +Install dependencies with: + +```sh +npm install +``` + +This project uses [NPM workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces/). + +## Build System + +We have a very simple build system based on package.json scripts that are called using `npm run` +and a thin wrapper for what we call build "actions". + +We've defined a package.json script called `action` whose parameter is a relative path: + +```shell +npm run action $ACTION +``` + +This command will define a `run_action()` function and call `${ACTION}.action.sh`, which must exist. +The called action script can use `run_action` to call its dependencies. The $ACTION parameter is +always resolved from the project root, regardless of the caller location. + +The idea of `run_action` is to keep the build logic next to where the relevant code is. +It also defines two environmental variables: + +- ROOT_DIR: the root directory of the project, as an absolute path. +- BUILD_DIR: where the build output should go, as an absolute path. + +> ⚠️ To find all the actions in this project, run `npm run action:list` + +### Build output + +Building creates the following directories under `build/`: + +- `web_app/`: The Manager web app. + - `static/`: The standalone web app static files. This is what one deploys to a web server or runs with Electron. +- `electron_app/`: The launcher desktop Electron app + - `static/`: The Manager Electron app to run with the electron command-line + - `bundled/`: The Electron app bundled to run standalone on each platform + - `packaged/`: The Electron app bundles packaged as single files for distribution +- `invite_page`: the Invite Page + - `static`: The standalone static files to be deployed +- `shadowbox`: The Proxy Server + +The directories have subdirectories for intermediate output: + +- `ts/`: Autogenerated Typescript files +- `js/`: The output from compiling Typescript code +- `browserified/`: The output of browserifying the JavaScript code + +To clean up: + +``` +npm run clean +``` + +## Shadowsocks Resistance Against Detection and Blocking + +Shadowsocks used to be blocked in some countries, and because Outline uses Shadowsocks, there has been skepticism about Outline working in those countries. In fact, people have tried Outline in the past and had their servers blocked. + +However, since the second half of 2020 things have changed. The Outline team and Shadowsocks community made a number of improvements that strengthened Shadowsocks beyond the censor's current capabilities. + +As shown in the research [How China Detects and Blocks Shadowsocks](https://gfw.report/talks/imc20/en/), the censor uses active probing to detect Shadowsocks servers. The probing may be triggered by packet sniffing, but that's not how the servers are detected. + +Even though Shadowsocks is a standard, it leaves a lot of room for choices on how it's implemented and deployed. + +First of all, you **must use AEAD ciphers**. The old stream ciphers are easy to break and manipulate, exposing you to simple detection and decryption attacks. Outline has banned all stream ciphers, since people copy old examples to set up their servers. The Outline Manager goes further and picks the cipher for you, since users don't usually know how to choose a cipher, and it generates a long random secret, so you are not vulnerable to dictionary-based attacks. + +Second, you need **probing resistance**. Both shadowsocks-libev and Outline have added that. The research [Detecting Probe-resistant Proxies](https://www.ndss-symposium.org/ndss-paper/detecting-probe-resistant-proxies/) showed that, in the past, an invalid byte would trigger different behaviors whether it was inserted in positions 49, 50 or 51 of the stream, which is very telling. That behavior is now gone, and the censor can no longer rely on that. + +Third, you need **protection against replayed data**. Both shadowsocks-libev and Outline have added such protection, which you may need to enable explicitly on ss-libev, but it's the default on Outline. + +Fourth, Outline and clients using shadowsocks-libev now **merge the SOCKS address and the initial data** in the same initial encrypted frame, making the size of the first packet variable. Before the first packet only had the SOCKS address, with a fixed size, and that was a giveaway. + +The censors used to block Shadowsocks, but Shadowsocks has evolved, and in 2021, it was ahead again in the cat and mouse game. + +In 2022 China started blocking seemingly random traffic ([report](https://www.opentech.fund/news/exposing-the-great-firewalls-dynamic-blocking-of-fully-encrypted-traffic/)). While there is no evidence they could detect Shadowsocks, the protocol ended up blocked. + +As a reponse, we [added a feature to the Outline Client](https://github.com/Jigsaw-Code/outline-client/pull/1454) that allows service managers to specify in the access key a prefix to be used in the Shadowsocks initialization, which can be used to bypass the blocking in China. + +Shadowsocks remains our protocol of choice because it's simple, well understood and very performant. Furthermore, it has an enthusiastic community of very smart people behind it. diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 000000000..1a9ee2c17 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,20 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'scope-enum': [ + 2, + 'always', + [ + 'devtools', + 'devtools/build', + 'docs', + 'manager', + 'manager/electron', + 'manager/web', + 'metrics_server', + 'sentry_webhook', + 'server', + ], + ], + }, +}; diff --git a/jasmine.json b/jasmine.json new file mode 100644 index 000000000..e35b03761 --- /dev/null +++ b/jasmine.json @@ -0,0 +1,6 @@ +{ + "spec_dir": ".", + "spec_files": ["build/js/**/*.spec.js"], + "stopSpecOnExpectationFailure": false, + "random": false +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..054ae1f2b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,33644 @@ +{ + "name": "outline-server", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "outline-server", + "workspaces": [ + "src/*" + ], + "dependencies": { + "node-fetch": "^2.6.7" + }, + "devDependencies": { + "@commitlint/config-conventional": "^17.0.0", + "@types/jasmine": "^3.5.10", + "@types/node-fetch": "^2.6.2", + "@typescript-eslint/eslint-plugin": "^5.14.0", + "@typescript-eslint/parser": "^5.14.0", + "@webpack-cli/serve": "^2.0.5", + "browserslist": "^4.20.3", + "eslint": "^8.10.0", + "eslint-import-resolver-typescript": "^2.7.1", + "eslint-plugin-compat": "^4.0.2", + "eslint-plugin-import": "^2.26.0", + "generate-license-file": "^1.2.0", + "husky": "^1.3.1", + "jasmine": "^3.5.0", + "prettier": "^2.4.1", + "pretty-quick": "^3.1.1", + "typescript": "^4" + }, + "engines": { + "node": "18.x.x" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "17.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-changelog-conventionalcommits": "^4.3.1" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.14.1.tgz", + "integrity": "sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^9.6.0", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=8.6" + }, + "optionalDependencies": { + "global-agent": "^3.0.0", + "global-tunnel-ng": "^2.7.1" + } + }, + "node_modules/@electron/universal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", + "dev": true, + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "asar": "^3.1.0", + "debug": "^4.3.1", + "dir-compare": "^2.4.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.1", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@formatjs/intl-unified-numberformat": { + "version": "3.3.7", + "license": "MIT", + "dependencies": { + "@formatjs/intl-utils": "^2.3.0" + } + }, + "node_modules/@formatjs/intl-utils": { + "version": "2.3.0", + "license": "MIT" + }, + "node_modules/@google-cloud/bigquery": { + "version": "5.12.0", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/common": "^3.9.0", + "@google-cloud/paginator": "^3.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "big.js": "^6.0.0", + "duplexify": "^4.0.0", + "extend": "^3.0.2", + "is": "^3.3.0", + "p-event": "^4.1.0", + "readable-stream": "^3.6.0", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/bigquery/node_modules/big.js": { + "version": "6.1.1", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bigjs" + } + }, + "node_modules/@google-cloud/bigquery/node_modules/duplexify": { + "version": "4.1.2", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/@google-cloud/bigquery/node_modules/p-event": { + "version": "4.2.0", + "license": "MIT", + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@google-cloud/bigquery/node_modules/p-timeout": { + "version": "3.2.0", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@google-cloud/bigquery/node_modules/readable-stream": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@google-cloud/bigquery/node_modules/uuid": { + "version": "8.3.2", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@google-cloud/common": { + "version": "3.10.0", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^7.14.0", + "retry-request": "^4.2.2", + "teeny-request": "^7.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/common/node_modules/duplexify": { + "version": "4.1.2", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/@google-cloud/common/node_modules/gaxios": { + "version": "4.3.3", + "license": "Apache-2.0", + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/common/node_modules/gcp-metadata": { + "version": "4.3.1", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/common/node_modules/google-auth-library": { + "version": "7.14.1", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/common/node_modules/lru-cache": { + "version": "6.0.0", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/common/node_modules/readable-stream": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@google-cloud/common/node_modules/yallist": { + "version": "4.0.0", + "license": "ISC" + }, + "node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "2.1.1", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "2.0.4", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage": { + "version": "5.19.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "abort-controller": "^3.0.0", + "arrify": "^2.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "configstore": "^5.0.0", + "date-and-time": "^2.0.0", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "get-stream": "^6.0.0", + "google-auth-library": "^7.14.1", + "hash-stream-validation": "^0.2.2", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "pumpify": "^2.0.0", + "retry-request": "^4.2.2", + "snakeize": "^0.1.0", + "stream-events": "^1.0.4", + "teeny-request": "^7.1.3", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/duplexify": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/gaxios": { + "version": "4.3.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { + "version": "4.3.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-auth-library": { + "version": "7.14.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@google-cloud/storage/node_modules/pumpify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/readable-stream": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@google-cloud/storage/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@hapi/address": { + "version": "2.1.4", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/formula": { + "version": "1.2.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "8.5.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/joi": { + "version": "16.1.8", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^2.1.2", + "@hapi/formula": "^1.2.0", + "@hapi/hoek": "^8.2.4", + "@hapi/pinpoint": "^1.0.2", + "@hapi/topo": "^3.1.3" + } + }, + "node_modules/@hapi/pinpoint": { + "version": "1.0.2", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "3.1.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^8.3.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.5", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@mdn/browser-compat-data": { + "version": "4.2.1", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@netflix/nerror": { + "version": "1.1.3", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "extsprintf": "^1.4.0", + "lodash": "^4.17.15" + } + }, + "node_modules/@netflix/nerror/node_modules/extsprintf": { + "version": "1.4.1", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polymer/app-layout": { + "version": "3.1.0", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-media-query": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-scroll-target-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/app-localize-behavior": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-ajax": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0", + "intl-messageformat": "^2.2.0" + } + }, + "node_modules/@polymer/app-localize-behavior/node_modules/intl-messageformat": { + "version": "2.2.0", + "license": "BSD-3-Clause", + "dependencies": { + "intl-messageformat-parser": "1.4.0" + } + }, + "node_modules/@polymer/app-localize-behavior/node_modules/intl-messageformat-parser": { + "version": "1.4.0", + "license": "BSD-3-Clause" + }, + "node_modules/@polymer/font-roboto": { + "version": "3.0.2", + "license": "BSD-3-Clause" + }, + "node_modules/@polymer/iron-a11y-announcer": { + "version": "3.2.0", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-a11y-keys-behavior": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-ajax": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-autogrow-textarea": { + "version": "3.0.3", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-behaviors": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-checked-element-behavior": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-collapse": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-dropdown": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-fit-behavior": { + "version": "3.1.0", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-flex-layout": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-form-element-behavior": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-icon": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-icons": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-iconset-svg": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-iconset-svg": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-input": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-announcer": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-media-query": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-menu-behavior": { + "version": "3.0.2", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-selector": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-meta": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-overlay-behavior": { + "version": "3.0.3", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-pages": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-selector": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-range-behavior": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-resizable-behavior": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-scroll-target-behavior": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-selector": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/iron-validatable-behavior": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/neon-animation": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-selector": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-behaviors": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-checked-element-behavior": "^3.0.0-pre.26", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-button": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-checkbox": { + "version": "3.1.0", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-checked-element-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-dialog": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/paper-dialog-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-dialog-behavior": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-dialog-scrollable": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/paper-dialog-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-dropdown-menu": { + "version": "3.2.0", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-iconset-svg": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-input": "^3.1.0", + "@polymer/paper-menu-button": "^3.1.0", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.3.1" + } + }, + "node_modules/@polymer/paper-icon-button": { + "version": "3.0.2", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-input": { + "version": "3.2.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-autogrow-textarea": "^3.0.0-pre.26", + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-input": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-item": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-listbox": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-menu-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-menu-button": { + "version": "3.1.0", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-dropdown": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.1.0", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-progress": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-range-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-ripple": { + "version": "3.0.2", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-styles": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/font-roboto": "^3.0.1", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-tabs": { + "version": "3.1.0", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-iconset-svg": "^3.0.0-pre.26", + "@polymer/iron-menu-behavior": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-icon-button": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-toast": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/iron-a11y-announcer": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.0.0-pre.26", + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/paper-tooltip": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "node_modules/@polymer/polymer": { + "version": "3.4.1", + "license": "BSD-3-Clause", + "dependencies": { + "@webcomponents/shadycss": "^1.9.1" + } + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.74.0.tgz", + "integrity": "sha512-JK6IRGgdtZjswGfaGIHNWIThffhOHzVIIaGmglui+VFIzOsOqePjoxaDV0MEvzafxXZD7eWqGE5RGuZ0n6HFVg==", + "dependencies": { + "@sentry/core": "7.74.0", + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0", + "tslib": "^2.4.1 || ^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.74.0.tgz", + "integrity": "sha512-Njr8216Z1dFUcl6NqBOk20dssK9SjoVddY74Xq+Q4p3NfXBG3lkMcACXor7SFoJRZXq8CZWGS13Cc5KwViRw4g==", + "dependencies": { + "@sentry-internal/tracing": "7.74.0", + "@sentry/core": "7.74.0", + "@sentry/replay": "7.74.0", + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0", + "tslib": "^2.4.1 || ^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.74.0.tgz", + "integrity": "sha512-83NRuqn7nDZkSVBN5yJQqcpXDG4yMYiB7TkYUKrGTzBpRy6KUOrkCdybuKk0oraTIGiGSe5WEwCFySiNgR9FzA==", + "dependencies": { + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0", + "tslib": "^2.4.1 || ^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/electron": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@sentry/electron/-/electron-4.14.0.tgz", + "integrity": "sha512-5laPImINGd86osNUu9UyGWB9dfK9O6hmydSTFWsHWHgFsd/2YKtOgjdXgMu+znyU+sy+lU9z+wwEq/h4yivPZQ==", + "dependencies": { + "@sentry/browser": "7.74.0", + "@sentry/core": "7.74.0", + "@sentry/node": "7.74.0", + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0", + "deepmerge": "4.3.0", + "lru_map": "^0.3.3", + "tslib": "^2.5.0" + } + }, + "node_modules/@sentry/node": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.74.0.tgz", + "integrity": "sha512-uBmW2/z0cz/WFIG74ZF7lSipO0XNzMf9yrdqnZXnGDYsUZE4I4QiqDN0hNi6fkTgf9MYRC8uFem2OkAvyPJ74Q==", + "dependencies": { + "@sentry-internal/tracing": "7.74.0", + "@sentry/core": "7.74.0", + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0", + "cookie": "^0.5.0", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^2.4.1 || ^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sentry/replay": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.74.0.tgz", + "integrity": "sha512-GoYa3cHTTFVI/J1cnZ0i4X128mf/JljaswO3PWNTe2k3lSHq/LM5aV0keClRvwM0W8hlix8oOTT06nnenOUmmw==", + "dependencies": { + "@sentry/core": "7.74.0", + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry/types": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.74.0.tgz", + "integrity": "sha512-rI5eIRbUycWjn6s6o3yAjjWtIvYSxZDdnKv5je2EZINfLKcMPj1dkl6wQd2F4y7gLfD/N6Y0wZYIXC3DUdJQQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.74.0.tgz", + "integrity": "sha512-k3np8nuTPtx5KDODPtULfFln4UXdE56MZCcF19Jv6Ljxf+YN/Ady1+0Oi3e0XoSvFpWNyWnglauT7M65qCE6kg==", + "dependencies": { + "@sentry/types": "7.74.0", + "tslib": "^2.4.1 || ^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bunyan": { + "version": "1.8.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/component-emitter": { + "version": "1.2.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.4", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.4.tgz", + "integrity": "sha512-lOzjyfY/D9QR4hY9oblZ76B90MYTB3RrQ4z2vBIJKj9ROCRqdkYl2gSUx1x1a4IWPjKJZLL4Aw1Zfay7eMnmnA==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", + "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", + "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", + "dev": true + }, + "node_modules/@types/expect": { + "version": "1.20.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.24", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/formidable": { + "version": "1.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/hapi__joi": { + "version": "17.1.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "3.10.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "3.12.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.11.34", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/plist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", + "integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/polymer": { + "version": "1.2.11", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webcomponents.js": "*" + } + }, + "node_modules/@types/puppeteer": { + "version": "5.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/randomstring": { + "version": "1.1.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/request": { + "version": "2.48.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@types/restify": { + "version": "8.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bunyan": "*", + "@types/formidable": "*", + "@types/node": "*", + "@types/spdy": "*" + } + }, + "node_modules/@types/restify-cors-middleware": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/restify": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "5.5.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/spdy": { + "version": "3.4.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/tmp": { + "version": "0.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/verror": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", + "integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==", + "dev": true, + "optional": true + }, + "node_modules/@types/vinyl": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/expect": "^1.20.4", + "@types/node": "*" + } + }, + "node_modules/@types/webcomponents.js": { + "version": "0.6.37", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.7.tgz", + "integrity": "sha512-6UrLjiDUvn40CMrAubXuIVtj2PEfKDffJS7ychvnPU44j+KVeXmdHHTgqcM/dxLUTHxlXHiFM8Skmb8ozGdTnQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz", + "integrity": "sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.9.2", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "5.14.0", + "@typescript-eslint/type-utils": "5.14.0", + "@typescript-eslint/utils": "5.14.0", + "debug": "^4.3.2", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.2.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.3.7", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.14.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.14.0", + "@typescript-eslint/types": "5.14.0", + "@typescript-eslint/typescript-estree": "5.14.0", + "debug": "^4.3.2" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.14.0", + "@typescript-eslint/visitor-keys": "5.14.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "5.14.0", + "debug": "^4.3.2", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.14.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.14.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.14.0", + "@typescript-eslint/visitor-keys": "5.14.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.3.7", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.14.0", + "@typescript-eslint/types": "5.14.0", + "@typescript-eslint/typescript-estree": "5.14.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.14.0", + "eslint-visitor-keys": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webcomponents/shadycss": { + "version": "1.11.0", + "license": "BSD-3-Clause" + }, + "node_modules/@webcomponents/webcomponentsjs": { + "version": "2.6.0", + "license": "BSD-3-Clause" + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@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 + }, + "node_modules/@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 + }, + "node_modules/7zip-bin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz", + "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv-formats/node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-gray": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-wrap": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true + }, + "node_modules/app-builder-lib": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.6.0.tgz", + "integrity": "sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA==", + "dev": true, + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/universal": "1.2.1", + "@malept/flatpak-bundler": "^0.4.0", + "7zip-bin": "~5.1.1", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.7", + "electron-osx-sign": "^0.6.0", + "electron-publish": "23.6.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^4.0.10", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^3.1.2", + "read-config-file": "6.2.0", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.7", + "tar": "^6.1.11", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", + "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/app-builder-lib/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/app-builder-lib/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/append-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/archive-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^4.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/archive-type/node_modules/file-type": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/args": { + "version": "2.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "4.1.0", + "chalk": "1.1.3", + "minimist": "1.2.0", + "pkginfo": "0.4.0", + "string-similarity": "1.1.0" + }, + "engines": { + "node": ">= 6.6.0" + } + }, + "node_modules/args/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/args/node_modules/ansi-styles": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/args/node_modules/camelcase": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/args/node_modules/chalk": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/args/node_modules/strip-ansi": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/args/node_modules/supports-color": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-filter": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-map": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-differ": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-each": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/array-ify": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-initial": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-slice": "^1.0.0", + "is-number": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-initial/node_modules/is-number": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-last": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-last/node_modules/is-number": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-sort": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/asar": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/asar/-/asar-3.2.0.tgz", + "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", + "deprecated": "Please use @electron/asar moving forward. There is no API change, just a package name change", + "dev": true, + "dependencies": { + "chromium-pickle-js": "^0.2.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + }, + "optionalDependencies": { + "@types/glob": "^7.1.1" + } + }, + "node_modules/asn1": { + "version": "0.2.4", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ast-metadata-inferer": { + "version": "0.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdn/browser-compat-data": "^3.3.14" + } + }, + "node_modules/ast-metadata-inferer/node_modules/@mdn/browser-compat-data": { + "version": "3.3.14", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/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, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/async-done": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/async-each": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/async-settle": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "async-done": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/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, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "license": "MIT" + }, + "node_modules/bach": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/base": { + "version": "0.11.2", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "dev": true, + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bignumber.js": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bin-build": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress": "^4.0.0", + "download": "^6.2.2", + "execa": "^0.7.0", + "p-map-series": "^1.0.0", + "tempfile": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/cross-spawn": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/bin-build/node_modules/download": { + "version": "6.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "caw": "^2.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.0.0", + "ext-name": "^5.0.0", + "file-type": "5.2.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^7.0.0", + "make-dir": "^1.0.0", + "p-event": "^1.0.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/execa": { + "version": "0.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.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" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/file-type": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/filenamify": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/get-stream": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/got": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-response": "^3.2.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-plain-obj": "^1.1.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "p-cancelable": "^0.3.0", + "p-timeout": "^1.1.1", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "url-parse-lax": "^1.0.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-build/node_modules/lru-cache": { + "version": "4.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/bin-build/node_modules/make-dir": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/p-cancelable": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/p-event": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^1.1.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/p-timeout": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/prepend-http": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-build/node_modules/shebang-command": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-build/node_modules/shebang-regex": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-build/node_modules/temp-dir": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/tempfile": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "temp-dir": "^1.0.0", + "uuid": "^3.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/url-parse-lax": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prepend-http": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-build/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/bin-build/node_modules/yallist": { + "version": "2.1.2", + "dev": true, + "license": "ISC" + }, + "node_modules/binary-extensions": { + "version": "1.13.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bintrees": { + "version": "1.0.1" + }, + "node_modules/bl": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/bmp-js": { + "version": "0.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.0", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/destroy": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/body-parser/node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bonjour-service": { + "version": "1.0.12", + "dev": true, + "license": "MIT", + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.4" + } + }, + "node_modules/bonjour-service/node_modules/array-flatten": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "dev": true, + "optional": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.6.0.tgz", + "integrity": "sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ==", + "dev": true, + "dependencies": { + "@types/debug": "^4.1.6", + "@types/fs-extra": "^9.0.11", + "7zip-bin": "~5.1.1", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", + "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/builder-util/node_modules/ci-info": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", + "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/builder-util/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase-keys": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001546", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz", + "integrity": "sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "license": "Apache-2.0" + }, + "node_modules/caw": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-proxy": "^2.0.0", + "isurl": "^1.0.0-alpha5", + "tunnel-agent": "^0.6.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "2.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "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" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "dev": true, + "license": "ISC" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true + }, + "node_modules/ci-info": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/circle-flags": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/HatScripts/circle-flags.git#a21fc224b3079631993b3b8189c490fa0899ea9f", + "license": "MIT" + }, + "node_modules/class-utils": { + "version": "0.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.6.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/clipboard-polyfill": { + "version": "2.8.6", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep/node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-map": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.16", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/configstore": { + "version": "5.0.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/configstore/node_modules/make-dir": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/connect": { + "version": "3.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/content-type": { + "version": "1.0.4", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "4.6.3", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "license": "MIT" + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-props": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "each-props": "^1.3.2", + "is-plain-object": "^5.0.0" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/copy-webpack-plugin/node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/core-js": { + "version": "3.19.0", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-loader/node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-loader/node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/css-loader/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/css-select/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/css-select/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/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, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csv": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.3.5.tgz", + "integrity": "sha512-Y+KTCAUljtq2JaGP42ZL1bymqlU5BkfnFpZhxRczGFDZox2VXhlRHnG5DRshyUrwQzmCdEiLjSqNldCfm1OVCA==", + "dependencies": { + "csv-generate": "^4.3.0", + "csv-parse": "^5.5.2", + "csv-stringify": "^6.4.4", + "stream-transform": "^3.2.10" + }, + "engines": { + "node": ">= 0.1.90" + } + }, + "node_modules/csv-generate": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.3.0.tgz", + "integrity": "sha512-7KdVId/2RgwPIKfWHaHtjBq7I9mgdi8ICzsUyIhP8is6UwpwVGGSC/aPnrZ8/SkgBcCP20lXrdPuP64Irs1VBg==" + }, + "node_modules/csv-parse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.2.tgz", + "integrity": "sha512-YRVtvdtUNXZCMyK5zd5Wty1W6dNTpGKdqQd4EQ8tl/c6KW1aMBB1Kg1ppky5FONKmEqGJ/8WjLlTNLPne4ioVA==" + }, + "node_modules/csv-stringify": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.4.tgz", + "integrity": "sha512-NDshLupGa7gp4UG4sSNIqwYJqgSwvds0SvENntxoVoVvTzXcrHvd5gG2MWpbRpSNvk59dlmIe1IwNvSxN4IVmg==" + }, + "node_modules/currently-unhandled": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/d": { + "version": "1.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/date-and-time": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/date-format": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debuglog": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress": { + "version": "4.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar/node_modules/file-type": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/file-type": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress/node_modules/make-dir": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", + "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-compare": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^5.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-gateway/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/default-gateway/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-resolution": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/defaults": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/arrify": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/globby": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/devtools-protocol": { + "version": "0.0.981744", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/dezalgo": { + "version": "1.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/di": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", + "integrity": "sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA==", + "dev": true, + "dependencies": { + "buffer-equal": "1.0.0", + "colors": "1.0.3", + "commander": "2.9.0", + "minimatch": "3.0.4" + }, + "bin": { + "dircompare": "src/cli/dircompare.js" + } + }, + "node_modules/dir-compare/node_modules/commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", + "dev": true, + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/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, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dmg-builder": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.6.0.tgz", + "integrity": "sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA==", + "dev": true, + "dependencies": { + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "fs-extra": "^10.0.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.3.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "dev": true + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "2.4.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.7.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "8.2.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "node_modules/download": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "archive-type": "^4.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.2.1", + "ext-name": "^5.0.0", + "file-type": "^11.1.0", + "filenamify": "^3.0.0", + "get-stream": "^4.1.0", + "got": "^8.3.1", + "make-dir": "^2.1.0", + "p-event": "^2.1.0", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/download/node_modules/@sindresorhus/is": { + "version": "0.7.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/cacheable-request": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + } + }, + "node_modules/download/node_modules/cacheable-request/node_modules/get-stream": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/file-type": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/download/node_modules/got": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/got/node_modules/get-stream": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/got/node_modules/pify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/http-cache-semantics": { + "version": "3.8.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/download/node_modules/lowercase-keys": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/download/node_modules/normalize-url": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/p-cancelable": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/pify": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/download/node_modules/sort-keys": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "nan": "^2.14.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/duplexer3": { + "version": "0.1.4", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/duplexify": { + "version": "3.7.1", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/each-props": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + } + }, + "node_modules/each-props/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/electron/-/electron-19.1.9.tgz", + "integrity": "sha512-XT5LkTzIHB+ZtD3dTmNnKjVBWrDWReCKt9G1uAFLz6uJMEVcIUiYO+fph5pLXETiBw/QZBx8egduMEfIccLx+g==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@electron/get": "^1.14.1", + "@types/node": "^16.11.26", + "extract-zip": "^1.0.3" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 8.6" + } + }, + "node_modules/electron-builder": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.6.0.tgz", + "integrity": "sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw==", + "dev": true, + "dependencies": { + "@types/yargs": "^17.0.1", + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "dmg-builder": "23.6.0", + "fs-extra": "^10.0.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.2.0", + "simple-update-notifier": "^1.0.7", + "yargs": "^17.5.1" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder/node_modules/ci-info": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.1.tgz", + "integrity": "sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg==", + "dev": true + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-builder/node_modules/yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-icon-maker": { + "version": "0.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "args": "^2.3.0", + "icon-gen": "1.0.7", + "jimp": "^0.2.27" + }, + "bin": { + "electron-icon-maker": "index.js" + } + }, + "node_modules/electron-notarize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.1.tgz", + "integrity": "sha512-u/ECWhIrhkSQpZM4cJzVZ5TsmkaqrRo5LDC/KMbGF0sPkm53Ng59+M0zp8QVaql0obfJy9vlVT+4iOkAi2UDlA==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-notarize/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-notarize/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-osx-sign": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", + "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==", + "deprecated": "Please use @electron/osx-sign moving forward. Be aware the API is slightly different", + "dev": true, + "dependencies": { + "bluebird": "^3.5.0", + "compare-version": "^0.1.2", + "debug": "^2.6.8", + "isbinaryfile": "^3.0.2", + "minimist": "^1.2.0", + "plist": "^3.0.1" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/electron-osx-sign/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/electron-osx-sign/node_modules/isbinaryfile": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "dependencies": { + "buffer-alloc": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/electron-osx-sign/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/electron-publish": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.6.0.tgz", + "integrity": "sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "fs-extra": "^10.0.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.545", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.545.tgz", + "integrity": "sha512-G1HKumUw+y5yxMjewGfKz0XrqG6O+Tb4zrlC/Vs1+9riRXBuFlO0hOEXP3xeI+ltlJkbVUuLkYdmjHYH6Jkiow==", + "dev": true + }, + "node_modules/electron-updater": { + "version": "4.3.9", + "license": "MIT", + "dependencies": { + "@types/semver": "^7.3.5", + "builder-util-runtime": "8.7.5", + "fs-extra": "^10.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.4", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.3.5" + } + }, + "node_modules/electron-updater/node_modules/@types/semver": { + "version": "7.3.9", + "license": "MIT" + }, + "node_modules/electron-updater/node_modules/argparse": { + "version": "2.0.1", + "license": "Python-2.0" + }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "8.7.5", + "license": "MIT", + "dependencies": { + "debug": "^4.3.2", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/js-yaml": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.1.0", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/lru-cache": { + "version": "6.0.0", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.3.7", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-updater/node_modules/yallist": { + "version": "4.0.0", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/base64-arraybuffer": "~1.0.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "license": "MIT" + }, + "node_modules/entities": { + "version": "1.1.2", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "dev": true + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.53", + "dev": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "optional": true + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-regexp-component": { + "version": "1.0.2" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/eslintrc": "^1.2.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", + "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "glob": "^7.2.0", + "is-glob": "^4.0.3", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", + "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-compat": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdn/browser-compat-data": "^4.1.5", + "ast-metadata-inferer": "^0.7.0", + "browserslist": "^4.16.8", + "caniuse-lite": "^1.0.30001304", + "core-js": "^3.16.2", + "find-up": "^5.0.0", + "lodash.memoize": "4.1.2", + "semver": "7.3.5" + }, + "engines": { + "node": ">=9.x" + }, + "peerDependencies": { + "eslint": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-compat/node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-compat/node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-compat/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-compat/node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-compat/node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-compat/node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-plugin-compat/node_modules/semver": { + "version": "7.3.5", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-compat/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/espree": { + "version": "9.3.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/ewma": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "dev": true + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express": { + "version": "4.18.1", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.5.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/express/node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ext": { + "version": "1.6.0", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.5.0" + } + }, + "node_modules/ext-list": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.6.0", + "dev": true, + "license": "ISC" + }, + "node_modules/extend": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "1.7.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fancy-log": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-glob/node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/fast-glob/node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/fast-glob/node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-text-encoding": { + "version": "1.0.3", + "license": "Apache-2.0" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.13.0", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "3.9.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/file-url": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "meow": "^3.7.0" + }, + "bin": { + "file-url": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", + "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/fill-range": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/find-my-way": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.7.0.tgz", + "integrity": "sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/findup": { + "version": "0.1.5", + "dev": true, + "dependencies": { + "colors": "~0.6.0-1", + "commander": "~2.1.0" + }, + "bin": { + "findup": "bin/findup.js" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/findup-sync": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/findup/node_modules/colors": { + "version": "0.6.2", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/findup/node_modules/commander": { + "version": "2.1.0", + "dev": true, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/fined": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fined/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/flagged-respawn": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "dev": true, + "license": "ISC" + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "1.2.2", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/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, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-mkdirp-stream": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fs-mkdirp-stream/node_modules/through2": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "1.2.13", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "5.0.0", + "license": "Apache-2.0", + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.0.0", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/generate-license-file": { + "version": "1.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "arg": "^4.1.3", + "cli-spinners": "^2.6.0", + "enquirer": "^2.3.6", + "esm": "^3.2.25", + "glob": "^7.1.7", + "inquirer": "^7.3.3", + "license-checker": "^25.0.1", + "ora": "^4.1.1" + }, + "bin": { + "generate-license-file": "bin/generate-license-file" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proxy": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "npm-conf": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-stdin": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "3.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-stream": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/glob-watcher": { + "version": "5.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "normalize-path": "^3.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/global": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/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, + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-agent/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "optional": true + }, + "node_modules/global-modules": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/global-tunnel-ng": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", + "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", + "dev": true, + "optional": true, + "dependencies": { + "encodeurl": "^1.0.2", + "lodash": "^4.17.10", + "npm-conf": "^1.1.3", + "tunnel": "^0.0.6" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/globals": { + "version": "13.12.1", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "optional": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glogg": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "sparkles": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/google-auth-library": { + "version": "8.0.2", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^5.3.2", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-auth-library/node_modules/lru-cache": { + "version": "6.0.0", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-auth-library/node_modules/yallist": { + "version": "4.0.0", + "license": "ISC" + }, + "node_modules/google-p12-pem": { + "version": "3.1.4", + "license": "MIT", + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", + "dev": true + }, + "node_modules/gtoken": { + "version": "5.3.2", + "license": "MIT", + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gtoken/node_modules/gaxios": { + "version": "4.3.3", + "license": "Apache-2.0", + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gulp": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-cli": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.4.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.2.0", + "yargs": "^7.1.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-cli/node_modules/ansi-colors": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/camelcase": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/cliui": { + "version": "3.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/gulp-cli/node_modules/get-caller-file": { + "version": "1.0.3", + "dev": true, + "license": "ISC" + }, + "node_modules/gulp-cli/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/string-width": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/strip-ansi": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/wrap-ansi": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-cli/node_modules/y18n": { + "version": "3.2.2", + "dev": true, + "license": "ISC" + }, + "node_modules/gulp-cli/node_modules/yargs": { + "version": "7.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.1" + } + }, + "node_modules/gulp-cli/node_modules/yargs-parser": { + "version": "5.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" + } + }, + "node_modules/gulp-posthtml": { + "version": "3.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "plugin-error": "^1.0.1", + "posthtml": "^0.11.6", + "posthtml-load-config": "^1.0.0", + "through2": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/gulp-replace": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^14.14.41", + "@types/vinyl": "^2.0.4", + "istextorbinary": "^3.0.0", + "replacestream": "^4.0.3", + "yargs-parser": ">=5.0.0-security.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gulp-replace/node_modules/@types/node": { + "version": "14.18.17", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-replace/node_modules/binaryextensions": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/gulp-replace/node_modules/istextorbinary": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binaryextensions": "^2.2.0", + "textextensions": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/gulp-replace/node_modules/textextensions": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/gulplog": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "glogg": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "license": "MIT" + }, + "node_modules/har-schema": { + "version": "2.0.0", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbol-support-x": { + "version": "1.4.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-to-string-tag-x": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbol-support-x": "^1.4.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-stream-validation": { + "version": "0.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/hasha": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^1.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hasha/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "bin": { + "he": "bin/he" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "dev": true, + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz", + "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "webpack": "^5.20.0" + } + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + } + }, + "node_modules/htmlparser2/node_modules/readable-stream": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-middleware/node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-middleware/node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-proxy-middleware/node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/http-proxy-middleware/node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/husky": { + "version": "1.3.1", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^5.0.7", + "execa": "^1.0.0", + "find-up": "^3.0.0", + "get-stdin": "^6.0.0", + "is-ci": "^2.0.0", + "pkg-dir": "^3.0.0", + "please-upgrade-node": "^3.1.1", + "read-pkg": "^4.0.1", + "run-node": "^1.0.0", + "slash": "^2.0.0" + }, + "bin": { + "husky-upgrade": "lib/upgrader/bin.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/husky/node_modules/slash": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/icon-gen": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "del": "^2.2.2", + "mkdirp": "^0.5.1", + "pngjs": "^3.0.0", + "svg2png": "4.1.0", + "uuid": "^3.0.0" + }, + "bin": { + "icon-gen": "bin/main.js" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/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, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/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, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/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, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/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, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "repeating": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "dev": true, + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "7.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/intl-format-cache": { + "version": "4.3.1", + "license": "BSD-3-Clause" + }, + "node_modules/intl-messageformat": { + "version": "7.8.4", + "license": "BSD-3-Clause", + "dependencies": { + "intl-format-cache": "^4.2.21", + "intl-messageformat-parser": "^3.6.4" + } + }, + "node_modules/intl-messageformat-parser": { + "version": "3.6.4", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/intl-unified-numberformat": "^3.2.0" + } + }, + "node_modules/into-stream": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/invert-kv": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is": { + "version": "3.3.0", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-descriptor/node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-function": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-invalid-path": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-invalid-path/node_modules/is-extglob": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-invalid-path/node_modules/is-glob": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-object": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-cwd": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-in-cwd": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-path-inside": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-is-inside": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-valid-path": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-invalid-path": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "license": "MIT" + }, + "node_modules/isurl": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jasmine": { + "version": "3.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.1.6", + "jasmine-core": "~3.10.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "3.10.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jimp": { + "version": "0.2.28", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^2.1.0", + "bmp-js": "0.0.3", + "es6-promise": "^3.0.2", + "exif-parser": "^0.1.9", + "file-type": "^3.1.0", + "jpeg-js": "^0.2.0", + "load-bmfont": "^1.2.3", + "mime": "^1.3.4", + "mkdirp": "0.5.1", + "pixelmatch": "^4.0.0", + "pngjs": "^3.0.0", + "read-chunk": "^1.0.1", + "request": "^2.65.0", + "stream-to-buffer": "^0.1.0", + "tinycolor2": "^1.1.2", + "url-regex": "^3.0.0" + } + }, + "node_modules/jimp/node_modules/mime": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jimp/node_modules/minimist": { + "version": "0.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/jimp/node_modules/mkdirp": { + "version": "0.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/jpeg-js": { + "version": "0.2.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-base64": { + "version": "3.7.2", + "license": "BSD-3-Clause" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "license": "MIT" + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-bigint/node_modules/bignumber.js": { + "version": "9.0.2", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "license": "ISC" + }, + "node_modules/json5": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonic": { + "version": "0.3.1", + "license": "MIT" + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/just-debounce": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/karma": { + "version": "6.3.17", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.2.0", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/karma-jasmine": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^3.6.0" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "karma": "*" + } + }, + "node_modules/karma-webpack": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.0.tgz", + "integrity": "sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "webpack-merge": "^4.1.5" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/karma-webpack/node_modules/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, + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/karma/node_modules/anymatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/karma/node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/chokidar": { + "version": "3.5.3", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/karma/node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/fsevents": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/karma/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/karma/node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/karma/node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/karma/node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma/node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/kew": { + "version": "0.7.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/keyv": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/kind-of": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klaw": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/last-run": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/launch-editor/node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lcid": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "invert-kv": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lead": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flush-write-stream": "^1.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-checker": { + "version": "25.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, + "node_modules/license-checker/node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/license-checker/node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/license-checker/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/license-checker/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/license-checker/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/liftoff": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/liftoff/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lit-element": { + "version": "2.5.1", + "license": "BSD-3-Clause", + "dependencies": { + "lit-html": "^1.1.1" + } + }, + "node_modules/lit-html": { + "version": "1.4.1", + "license": "BSD-3-Clause" + }, + "node_modules/load-bmfont": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^2.9.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "node_modules/load-bmfont/node_modules/buffer-equal": { + "version": "0.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/load-bmfont/node_modules/mime": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/lodash.assign": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log4js": { + "version": "6.4.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.5", + "debug": "^4.3.3", + "flatted": "^3.2.5", + "rfdc": "^1.3.0", + "streamroller": "^3.0.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/loud-rejection": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" + }, + "node_modules/make-dir": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/pify": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/make-iterator": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/make-iterator/node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-obj": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/matchdep/node_modules/findup-sync": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/matchdep/node_modules/is-glob": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.4.1", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/meow": { + "version": "3.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/meow/node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, + "node_modules/meow/node_modules/normalize-package-data": { + "version": "2.5.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/meow/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "3.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/extend-shallow": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-extendable": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-document": { + "version": "2.19.0", + "dev": true, + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp/node_modules/minimist": { + "version": "1.2.6", + "dev": true, + "license": "MIT" + }, + "node_modules/mri": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multimatch": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/multimatch/node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mute-stdout": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "dev": true, + "license": "ISC" + }, + "node_modules/nan": { + "version": "2.15.0", + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/extend-shallow": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/is-extendable": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/next-tick": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-jq": { + "version": "1.12.0", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@hapi/joi": "^16.1.7", + "@types/hapi__joi": "^17.1.0", + "bin-build": "^3.0.0", + "download": "^8.0.0", + "is-valid-path": "^0.1.1", + "strip-eof": "^1.0.0", + "strip-final-newline": "^2.0.0", + "tempfile": "^3.0.0" + }, + "engines": { + "npm": ">=6.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/nopt": { + "version": "4.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/now-and-later": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.3.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/npm-conf": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.map": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.reduce": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optionator/node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/ora": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.2.0", + "is-interactive": "^1.0.0", + "log-symbols": "^3.0.0", + "mute-stream": "0.0.8", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ordered-read-streams": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-locale": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "lcid": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/outline-manager": { + "resolved": "src/server_manager", + "link": true + }, + "node_modules/outline-metrics-server": { + "resolved": "src/metrics_server", + "link": true + }, + "node_modules/outline-server": { + "resolved": "src/shadowbox", + "link": true + }, + "node_modules/outline-shadowsocksconfig": { + "version": "0.2.0", + "resolved": "git+ssh://git@github.com/Jigsaw-Code/outline-shadowsocksconfig.git#add590ed57277653d02dd2031ae301500ae881e1", + "license": "Apache-2.0", + "dependencies": { + "ipaddr.js": "^2.0.0", + "js-base64": "^3.5.2", + "punycode": "^1.4.1" + } + }, + "node_modules/outline-shadowsocksconfig/node_modules/ipaddr.js": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-event": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-map-series": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-reduce": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-reduce": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module/node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.4.5" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-headers": { + "version": "2.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-root": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "license": "MIT" + }, + "node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/phantomjs-prebuilt": { + "version": "2.1.16", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "es6-promise": "^4.0.3", + "extract-zip": "^1.6.5", + "fs-extra": "^1.0.0", + "hasha": "^2.2.0", + "kew": "^0.7.0", + "progress": "^1.1.8", + "request": "^2.81.0", + "request-progress": "^2.0.1", + "which": "^1.2.10" + }, + "bin": { + "phantomjs": "bin/phantomjs" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/es6-promise": { + "version": "4.2.8", + "dev": true, + "license": "MIT" + }, + "node_modules/phantomjs-prebuilt/node_modules/fs-extra": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/jsonfile": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/progress": { + "version": "1.1.8", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/phin": { + "version": "2.9.3", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "0.2.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pidusage/node_modules/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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/pify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pino": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.0.tgz", + "integrity": "sha512-UUmvQ/7KTZt/vHjhRrnyS7h+J7qPBQnpG80V56xmIC+o9IqYmQOw/UIny9S9zYDfRBR0ClouCr464EkBMIT7Fw==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/pino-abstract-transport/node_modules/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==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkginfo": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/plist": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", + "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", + "dev": true, + "dependencies": { + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/plist/node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/plugin-error": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/plugin-error/node_modules/ansi-colors": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/plugin-error/node_modules/extend-shallow": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/plugin-error/node_modules/is-extendable": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/plugin-error/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pn": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "3.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "7.0.39", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-rtl": { + "version": "1.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "rtlcss": "2.5.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/posthtml": { + "version": "0.11.6", + "dev": true, + "license": "MIT", + "dependencies": { + "posthtml-parser": "^0.4.1", + "posthtml-render": "^1.1.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/posthtml-load-config": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^2.1.0", + "posthtml-load-options": "^1.0.0" + }, + "engines": { + "node": ">=4", + "npm": ">=3" + } + }, + "node_modules/posthtml-load-config/node_modules/cosmiconfig": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-directory": "^0.3.1", + "js-yaml": "^3.4.3", + "minimist": "^1.2.0", + "object-assign": "^4.1.0", + "os-homedir": "^1.0.1", + "parse-json": "^2.2.0", + "require-from-string": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/posthtml-load-config/node_modules/parse-json": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/posthtml-load-options": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^2.1.0" + }, + "engines": { + "node": ">=4", + "npm": ">=3" + } + }, + "node_modules/posthtml-load-options/node_modules/cosmiconfig": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-directory": "^0.3.1", + "js-yaml": "^3.4.3", + "minimist": "^1.2.0", + "object-assign": "^4.1.0", + "os-homedir": "^1.0.1", + "parse-json": "^2.2.0", + "require-from-string": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/posthtml-load-options/node_modules/parse-json": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/posthtml-parser": { + "version": "0.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^3.9.2" + } + }, + "node_modules/posthtml-postcss": { + "version": "0.2.6", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss": "^6.0.14" + } + }, + "node_modules/posthtml-postcss/node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/posthtml-postcss/node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/posthtml-postcss/node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/posthtml-postcss/node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/posthtml-postcss/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/posthtml-postcss/node_modules/postcss": { + "version": "6.0.23", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/posthtml-postcss/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/posthtml-render": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pretty-quick": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^3.0.0", + "execa": "^4.0.0", + "find-up": "^4.1.0", + "ignore": "^5.1.4", + "mri": "^1.1.5", + "multimatch": "^4.0.0" + }, + "bin": { + "pretty-quick": "bin/pretty-quick.js" + }, + "engines": { + "node": ">=10.13" + }, + "peerDependencies": { + "prettier": ">=2.0.0" + } + }, + "node_modules/pretty-quick/node_modules/chalk": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-quick/node_modules/execa": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/pretty-quick/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-quick/node_modules/get-stream": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-quick/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-quick/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-quick/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-quick/node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/process": { + "version": "0.11.10", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.2.0.tgz", + "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" + }, + "node_modules/progress": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prom-client": { + "version": "11.5.3", + "license": "Apache-2.0", + "dependencies": { + "tdigest": "^0.1.1" + }, + "engines": { + "node": ">=6.1" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "dev": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/psl": { + "version": "1.8.0", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "license": "MIT" + }, + "node_modules/puppeteer": { + "version": "13.6.0", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.981744", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.0", + "pkg-dir": "4.2.0", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.5.0" + }, + "engines": { + "node": ">=10.18.1" + } + }, + "node_modules/puppeteer/node_modules/extract-zip": { + "version": "2.0.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/puppeteer/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer/node_modules/get-stream": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/puppeteer/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer/node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer/node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer/node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/q": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.10.3", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/randombytes": { + "version": "2.0.3", + "license": "MIT" + }, + "node_modules/randomstring": { + "version": "1.2.1", + "license": "MIT", + "dependencies": { + "array-uniq": "1.0.2", + "randombytes": "2.0.3" + }, + "bin": { + "randomstring": "bin/randomstring" + }, + "engines": { + "node": "*" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-chunk": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-config-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.2.0.tgz", + "integrity": "sha512-gx7Pgr5I56JtYz+WuqEbQHj/xWo+5Vwua2jhb1VwM4Wid5PqYmZ4i00ZB0YEGIfkVBsCv9UrjgyqCiQfS/Oosg==", + "dev": true, + "dependencies": { + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/read-config-file/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/read-config-file/node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/read-config-file/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/read-config-file/node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-installed": { + "version": "4.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/read-installed/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-package-json": { + "version": "2.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "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" + } + }, + "node_modules/read-package-json/node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, + "node_modules/read-package-json/node_modules/normalize-package-data": { + "version": "2.5.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-package-json/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-pkg-up": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg-up/node_modules/normalize-package-data": { + "version": "2.5.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/path-type": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/read-pkg": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "license": "MIT", + "dependencies": { + "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" + } + }, + "node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "node_modules/readdirp": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redent": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not/node_modules/extend-shallow": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not/node_modules/is-extendable": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-bom-buffer": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remove-bom-stream": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-bom-stream/node_modules/through2": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/repeating": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-finite": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/replace-ext": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/replace-homedir": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/replacestream": { + "version": "4.0.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "escape-string-regexp": "^1.0.3", + "object-assign": "^4.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/request": { + "version": "2.88.2", + "license": "Apache-2.0", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-progress": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-options": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "value-or-function": "^3.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/restify": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/restify/-/restify-11.1.0.tgz", + "integrity": "sha512-ng7uBlj4wpIpshhAjNNSd6JG5Eg32+zgync2gG8OlF4e2xzIflZo54GJ/qLs765OtQaVU+uJPcNOL5Atm2F/dg==", + "dependencies": { + "assert-plus": "^1.0.0", + "csv": "^6.2.2", + "escape-regexp-component": "^1.0.2", + "ewma": "^2.0.1", + "find-my-way": "^7.2.0", + "formidable": "^1.2.1", + "http-signature": "^1.3.6", + "lodash": "^4.17.11", + "lru-cache": "^7.14.1", + "mime": "^3.0.0", + "negotiator": "^0.6.2", + "once": "^1.4.0", + "pidusage": "^3.0.2", + "pino": "^8.7.0", + "qs": "^6.7.0", + "restify-errors": "^8.0.2", + "semver": "^7.3.8", + "send": "^0.18.0", + "spdy": "^4.0.0", + "uuid": "^9.0.0", + "vasync": "^2.2.0" + }, + "bin": { + "report-latency": "bin/report-latency" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "dtrace-provider": "~0.8" + } + }, + "node_modules/restify-cors-middleware2": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/restify-cors-middleware2/-/restify-cors-middleware2-2.2.1.tgz", + "integrity": "sha512-j7Hvufd5dv699OeVuSk6YUH/HMga6a4A1zriGc6En/SDE3kzeDvQF82a3LMcVa+GaUfXxQa+TpVP8LRtcdIJWg==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "peerDependencies": { + "restify": "2.6.x - 11.x.x" + } + }, + "node_modules/restify-errors": { + "version": "8.0.2", + "license": "MIT", + "dependencies": { + "@netflix/nerror": "^1.0.0", + "assert-plus": "^1.0.0", + "lodash": "^4.17.15" + }, + "optionalDependencies": { + "safe-json-stringify": "^1.0.4" + } + }, + "node_modules/restify/node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/restify/node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/restify/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/restify/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/restify/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/restify/node_modules/semver/node_modules/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==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/restify/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/restify/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "4.2.2", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "2.7.1", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/roarr/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true, + "optional": true + }, + "node_modules/rtlcss": { + "version": "2.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "findup": "^0.1.5", + "mkdirp": "^0.5.1", + "postcss": "^6.0.23", + "strip-json-comments": "^2.0.0" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + } + }, + "node_modules/rtlcss/node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/rtlcss/node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/rtlcss/node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/rtlcss/node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/rtlcss/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rtlcss/node_modules/postcss": { + "version": "6.0.23", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/rtlcss/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-node": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "bin": { + "run-node": "run-node" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/safe-json-stringify": { + "version": "1.2.0", + "license": "MIT", + "optional": true + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/safe-regex2/node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "license": "ISC" + }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT" + }, + "node_modules/select-hose": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dev": true, + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/semver-greatest-satisfied-range": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "sver-compat": "^1.5.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/send/node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/destroy": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/sentry_webhook": { + "resolved": "src/sentry_webhook", + "link": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serialize-javascript/node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/set-value": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.5", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/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, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "dev": true, + "license": "ISC", + "engines": { + "node": "*" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/snakeize": { + "version": "0.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/socket.io": { + "version": "4.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.3.3", + "dev": true, + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.5.tgz", + "integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==", + "dev": true, + "dependencies": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/sonic-boom": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + } + }, + "node_modules/source-map-support": { + "version": "0.5.20", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/sparkles": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.10", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "dev": true, + "license": "(MIT AND CC-BY-3.0)" + }, + "node_modules/spdx-satisfies": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "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" + } + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/extend-shallow": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/is-extendable": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "license": "BSD-3-Clause" + }, + "node_modules/sshpk": { + "version": "1.16.1", + "license": "MIT", + "dependencies": { + "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" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-exhaust": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/stream-to": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/stream-to-buffer": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "stream-to": "~0.2.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-transform": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.10.tgz", + "integrity": "sha512-Yu+x7zcWbWdyB0Td8dFzHt2JEyD6694CNq2lqh1rbuEBVxPtjb/GZ7xDnZcdYiU5E/RtufM54ClSEOzZDeWguA==" + }, + "node_modules/streamroller": { + "version": "3.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.5", + "debug": "^4.3.3", + "fs-extra": "^10.0.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-similarity": { + "version": "1.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "lodash": "^4.13.1" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-stdin": "^4.0.1" + }, + "bin": { + "strip-indent": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-indent/node_modules/get-stdin": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "license": "MIT" + }, + "node_modules/style-loader": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", + "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sver-compat": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/svg2png": { + "version": "4.1.0", + "dev": true, + "license": "WTFPL", + "dependencies": { + "file-url": "^1.1.0", + "phantomjs-prebuilt": "^2.1.10", + "pn": "^1.0.0", + "yargs": "^5.0.0" + }, + "bin": { + "svg2png": "bin/svg2png-cli.js" + } + }, + "node_modules/svg2png/node_modules/ansi-regex": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/camelcase": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/cliui": { + "version": "3.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/svg2png/node_modules/get-caller-file": { + "version": "1.0.3", + "dev": true, + "license": "ISC" + }, + "node_modules/svg2png/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/string-width": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/strip-ansi": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/wrap-ansi": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/y18n": { + "version": "3.2.2", + "dev": true, + "license": "ISC" + }, + "node_modules/svg2png/node_modules/yargs": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "lodash.assign": "^4.2.0", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "window-size": "^0.2.0", + "y18n": "^3.2.1", + "yargs-parser": "^3.2.0" + } + }, + "node_modules/svg2png/node_modules/yargs-parser": { + "version": "3.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^3.0.0", + "lodash.assign": "^4.1.0" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.12.tgz", + "integrity": "sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw==", + "dev": true, + "dependencies": { + "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" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/bl": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/tar-fs/node_modules/readable-stream": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream": { + "version": "1.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/tdigest": { + "version": "0.1.1", + "license": "MIT", + "dependencies": { + "bintrees": "1.0.1" + } + }, + "node_modules/teeny-request": { + "version": "7.2.0", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "8.3.2", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tempfile": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "temp-dir": "^2.0.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.21.0.tgz", + "integrity": "sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/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 + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/throttleit": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "dev": true, + "license": "MIT" + }, + "node_modules/through2": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/through2-filter": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "node_modules/through2-filter/node_modules/through2": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/time-stamp": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/timed-out": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinycolor2": { + "version": "1.4.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tmp/node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/to-absolute-glob": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-buffer": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex/node_modules/extend-shallow": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex/node_modules/is-extendable": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex/node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-through": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/to-through/node_modules/through2": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tough-cookie/node_modules/punycode": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/treeify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/trim-newlines": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-loader": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.0.tgz", + "integrity": "sha512-LLlB/pkB4q9mW2yLdFMnK3dEHbrBjeZTYguaaIfusyojBgAGf5kF+O6KcWqiGzWqHk0LBsoolrp4VftEURhybg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/ts-loader/node_modules/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, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ts-loader/node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "license": "Unlicense" + }, + "node_modules/type": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.4.4", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", + "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undertaker": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "fast-levenshtein": "^1.0.0", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/undertaker-registry": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/union-value": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-stream": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-browserslist-db/node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/url": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + } + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/url-regex": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-regex": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/url-regex/node_modules/ip-regex": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/url-to-options": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/url/node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/use": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", + "dev": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/util-extend": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/v8flags": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/value-or-function": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vasync": { + "version": "2.2.0", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "verror": "1.10.0" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vinyl": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-fs": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-fs/node_modules/through2": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-sourcemap/node_modules/normalize-path": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-animations-js": { + "version": "2.3.2", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/webpack": { + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-cli/node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-cli/node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/anymatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/webpack-dev-server/node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-server/node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.5.3", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-server/node_modules/fsevents": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-server/node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-server/node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server/node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.1", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/window-size": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "bin": { + "window-size": "cli.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "8.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/xhr": { + "version": "2.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.4.23", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "src/metrics_server": { + "name": "outline-metrics-server", + "version": "1.0.1", + "license": "Apache", + "dependencies": { + "@google-cloud/bigquery": "^5.12.0", + "express": "^4.17.1" + }, + "devDependencies": { + "@google-cloud/storage": "^5.19.4", + "@types/express": "^4.17.12" + } + }, + "src/sentry_webhook": { + "version": "0.1.0", + "license": "Apache", + "devDependencies": { + "@sentry/types": "^4.4.1", + "@types/express": "^4.17.12", + "@types/jasmine": "^5.1.0", + "https-browserify": "^1.0.0", + "jasmine": "^5.1.0", + "stream-http": "^3.2.0", + "url": "^0.11.3" + } + }, + "src/sentry_webhook/node_modules/@sentry/types": { + "version": "4.5.3", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "src/sentry_webhook/node_modules/@types/jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.0.tgz", + "integrity": "sha512-XOV0KsqXNX2gUSqk05RWeolIMgaAQ7+l/ozOBoQ8NGwLg+E7J9vgagODtNgfim4jCzEUP0oJ3gnXeC+Zv+Xi1A==", + "dev": true + }, + "src/sentry_webhook/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "src/sentry_webhook/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "src/sentry_webhook/node_modules/jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.1.0.tgz", + "integrity": "sha512-prmJlC1dbLhti4nE4XAPDWmfJesYO15sjGXVp7Cs7Ym5I9Xtwa/hUHxxJXjnpfLO72+ySttA0Ztf8g/RiVnUKw==", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "jasmine-core": "~5.1.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "src/sentry_webhook/node_modules/jasmine-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.1.tgz", + "integrity": "sha512-UrzO3fL7nnxlQXlvTynNAenL+21oUQRlzqQFsA2U11ryb4+NLOCOePZ70PTojEaUKhiFugh7dG0Q+I58xlPdWg==", + "dev": true + }, + "src/sentry_webhook/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "src/sentry_webhook/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "src/server_manager": { + "name": "outline-manager", + "version": "0.0.0-debug", + "license": "Apache", + "dependencies": { + "@polymer/app-layout": "^3.0.0", + "@polymer/app-localize-behavior": "^3.0.0", + "@polymer/font-roboto": "^3.0.0", + "@polymer/iron-autogrow-textarea": "^3.0.0", + "@polymer/iron-collapse": "^3.0.0", + "@polymer/iron-fit-behavior": "^3.0.0", + "@polymer/iron-icon": "^3.0.1", + "@polymer/iron-icons": "^3.0.0", + "@polymer/iron-pages": "^3.0.0", + "@polymer/paper-button": "^3.0.0", + "@polymer/paper-checkbox": "^3.0.0", + "@polymer/paper-dialog": "^3.0.0", + "@polymer/paper-dialog-scrollable": "^3.0.0", + "@polymer/paper-dropdown-menu": "^3.0.0", + "@polymer/paper-icon-button": "^3.0.0", + "@polymer/paper-input": "^3.0.0", + "@polymer/paper-item": "^3.0.0", + "@polymer/paper-listbox": "^3.0.0", + "@polymer/paper-progress": "^3.0.0", + "@polymer/paper-tabs": "^3.0.0", + "@polymer/paper-toast": "^3.0.0", + "@polymer/paper-tooltip": "^3.0.0", + "@sentry/electron": "^4.14.0", + "@webcomponents/webcomponentsjs": "^2.0.0", + "circle-flags": "git+ssh://git@github.com/HatScripts/circle-flags.git#a21fc224b3079631993b3b8189c490fa0899ea9f", + "clipboard-polyfill": "^2.4.6", + "dotenv": "~8.2.0", + "electron-updater": "^4.1.2", + "express": "^4.17.1", + "google-auth-library": "^8.0.2", + "intl-messageformat": "^7", + "jsonic": "^0.3.1", + "lit-element": "^2.3.1", + "node-forge": "^1.3.1", + "request": "^2.87.0", + "web-animations-js": "^2.3.1" + }, + "devDependencies": { + "@types/node": "^16.11.29", + "@types/node-forge": "^1.0.2", + "@types/polymer": "^1.2.9", + "@types/puppeteer": "^5.4.2", + "@types/request": "^2.47.1", + "@types/semver": "^5.5.0", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "electron": "19.1.9", + "electron-builder": "^23.6.0", + "electron-icon-maker": "^0.0.4", + "electron-notarize": "^1.2.1", + "electron-to-chromium": "^1.4.328", + "gulp": "^4.0.0", + "gulp-posthtml": "^3.0.4", + "gulp-replace": "^1.0.0", + "html-webpack-plugin": "^5.5.3", + "karma": "^6.3.16", + "karma-chrome-launcher": "^3.1.0", + "karma-jasmine": "^4.0.1", + "karma-webpack": "^5.0.0", + "node-jq": "^1.11.2", + "postcss": "^7.0.29", + "postcss-rtl": "^1.7.3", + "posthtml-postcss": "^0.2.6", + "puppeteer": "^13.6.0", + "style-loader": "^3.3.3", + "ts-loader": "^9.5.0", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1", + "webpack-node-externals": "^3.0.0" + } + }, + "src/shadowbox": { + "name": "outline-server", + "version": "1.7.2", + "license": "Apache", + "dependencies": { + "ip-regex": "^4.1.0", + "js-yaml": "^3.12.0", + "outline-shadowsocksconfig": "git+ssh://git@github.com/Jigsaw-Code/outline-shadowsocksconfig.git#add590ed57277653d02dd2031ae301500ae881e1", + "prom-client": "^11.1.3", + "randomstring": "^1.1.5", + "restify": "^11.1.0", + "restify-cors-middleware2": "^2.2.1", + "restify-errors": "^8.0.2", + "uuid": "^3.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^3.11.2", + "@types/node": "^12", + "@types/randomstring": "^1.1.6", + "@types/restify": "^8.4.2", + "@types/restify-cors-middleware": "^1.0.1", + "@types/tmp": "^0.2.1", + "tmp": "^0.2.1", + "ts-loader": "^9.5.0", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + } + }, + "src/shadowbox/node_modules/@types/node": { + "version": "12.20.51", + "dev": true, + "license": "MIT" + } + }, + "dependencies": { + "@colors/colors": { + "version": "1.5.0", + "dev": true + }, + "@commitlint/config-conventional": { + "version": "17.0.0", + "dev": true, + "requires": { + "conventional-changelog-conventionalcommits": "^4.3.1" + } + }, + "@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@electron/get": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.14.1.tgz", + "integrity": "sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "global-agent": "^3.0.0", + "global-tunnel-ng": "^2.7.1", + "got": "^9.6.0", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + } + }, + "@electron/universal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", + "dev": true, + "requires": { + "@malept/cross-spawn-promise": "^1.1.0", + "asar": "^3.1.0", + "debug": "^4.3.1", + "dir-compare": "^2.4.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "dependencies": { + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "@eslint/eslintrc": { + "version": "1.2.0", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.1", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "resolve-from": { + "version": "4.0.0", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "dev": true + } + } + }, + "@formatjs/intl-unified-numberformat": { + "version": "3.3.7", + "requires": { + "@formatjs/intl-utils": "^2.3.0" + } + }, + "@formatjs/intl-utils": { + "version": "2.3.0" + }, + "@google-cloud/bigquery": { + "version": "5.12.0", + "requires": { + "@google-cloud/common": "^3.9.0", + "@google-cloud/paginator": "^3.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "big.js": "^6.0.0", + "duplexify": "^4.0.0", + "extend": "^3.0.2", + "is": "^3.3.0", + "p-event": "^4.1.0", + "readable-stream": "^3.6.0", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + }, + "dependencies": { + "big.js": { + "version": "6.1.1" + }, + "duplexify": { + "version": "4.1.2", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "p-event": { + "version": "4.2.0", + "requires": { + "p-timeout": "^3.1.0" + } + }, + "p-timeout": { + "version": "3.2.0", + "requires": { + "p-finally": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "uuid": { + "version": "8.3.2" + } + } + }, + "@google-cloud/common": { + "version": "3.10.0", + "requires": { + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^7.14.0", + "retry-request": "^4.2.2", + "teeny-request": "^7.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.2", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "gaxios": { + "version": "4.3.3", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "4.3.1", + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "7.14.1", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "requires": { + "yallist": "^4.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "yallist": { + "version": "4.0.0" + } + } + }, + "@google-cloud/paginator": { + "version": "3.0.7", + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "2.1.1" + }, + "@google-cloud/promisify": { + "version": "2.0.4" + }, + "@google-cloud/storage": { + "version": "5.19.4", + "dev": true, + "requires": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "abort-controller": "^3.0.0", + "arrify": "^2.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "configstore": "^5.0.0", + "date-and-time": "^2.0.0", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "get-stream": "^6.0.0", + "google-auth-library": "^7.14.1", + "hash-stream-validation": "^0.2.2", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "pumpify": "^2.0.0", + "retry-request": "^4.2.2", + "snakeize": "^0.1.0", + "stream-events": "^1.0.4", + "teeny-request": "^7.1.3", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.2", + "dev": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "gaxios": { + "version": "4.3.3", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "4.3.1", + "dev": true, + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "dev": true + }, + "google-auth-library": { + "version": "7.14.1", + "dev": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "mime": { + "version": "3.0.0", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "pumpify": { + "version": "2.0.1", + "dev": true, + "requires": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "yallist": { + "version": "4.0.0", + "dev": true + } + } + }, + "@hapi/address": { + "version": "2.1.4", + "dev": true + }, + "@hapi/formula": { + "version": "1.2.0", + "dev": true + }, + "@hapi/hoek": { + "version": "8.5.1", + "dev": true + }, + "@hapi/joi": { + "version": "16.1.8", + "dev": true, + "requires": { + "@hapi/address": "^2.1.2", + "@hapi/formula": "^1.2.0", + "@hapi/hoek": "^8.2.4", + "@hapi/pinpoint": "^1.0.2", + "@hapi/topo": "^3.1.3" + } + }, + "@hapi/pinpoint": { + "version": "1.0.2", + "dev": true + }, + "@hapi/topo": { + "version": "3.1.6", + "dev": true, + "requires": { + "@hapi/hoek": "^8.3.0" + } + }, + "@humanwhocodes/config-array": { + "version": "0.9.5", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "dev": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@leichtgewicht/ip-codec": { + "version": "2.0.4", + "dev": true + }, + "@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, + "@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "dependencies": { + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "@mdn/browser-compat-data": { + "version": "4.2.1", + "dev": true + }, + "@netflix/nerror": { + "version": "1.1.3", + "requires": { + "assert-plus": "^1.0.0", + "extsprintf": "^1.4.0", + "lodash": "^4.17.15" + }, + "dependencies": { + "extsprintf": { + "version": "1.4.1" + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@polymer/app-layout": { + "version": "3.1.0", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-media-query": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-scroll-target-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/app-localize-behavior": { + "version": "3.0.1", + "requires": { + "@polymer/iron-ajax": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0", + "intl-messageformat": "^2.2.0" + }, + "dependencies": { + "intl-messageformat": { + "version": "2.2.0", + "requires": { + "intl-messageformat-parser": "1.4.0" + } + }, + "intl-messageformat-parser": { + "version": "1.4.0" + } + } + }, + "@polymer/font-roboto": { + "version": "3.0.2" + }, + "@polymer/iron-a11y-announcer": { + "version": "3.2.0", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-a11y-keys-behavior": { + "version": "3.0.1", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-ajax": { + "version": "3.0.1", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-autogrow-textarea": { + "version": "3.0.3", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-behaviors": { + "version": "3.0.1", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-checked-element-behavior": { + "version": "3.0.1", + "requires": { + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-collapse": { + "version": "3.0.1", + "requires": { + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-dropdown": { + "version": "3.0.1", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-fit-behavior": { + "version": "3.1.0", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-flex-layout": { + "version": "3.0.1", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-form-element-behavior": { + "version": "3.0.1", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-icon": { + "version": "3.0.1", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-icons": { + "version": "3.0.1", + "requires": { + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-iconset-svg": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-iconset-svg": { + "version": "3.0.1", + "requires": { + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-input": { + "version": "3.0.1", + "requires": { + "@polymer/iron-a11y-announcer": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-media-query": { + "version": "3.0.1", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-menu-behavior": { + "version": "3.0.2", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-selector": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-meta": { + "version": "3.0.1", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-overlay-behavior": { + "version": "3.0.3", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-pages": { + "version": "3.0.1", + "requires": { + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-selector": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-range-behavior": { + "version": "3.0.1", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-resizable-behavior": { + "version": "3.0.1", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-scroll-target-behavior": { + "version": "3.0.1", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-selector": { + "version": "3.0.1", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/iron-validatable-behavior": { + "version": "3.0.1", + "requires": { + "@polymer/iron-meta": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/neon-animation": { + "version": "3.0.1", + "requires": { + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/iron-selector": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-behaviors": { + "version": "3.0.1", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-checked-element-behavior": "^3.0.0-pre.26", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-button": { + "version": "3.0.1", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-checkbox": { + "version": "3.1.0", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-checked-element-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-dialog": { + "version": "3.0.1", + "requires": { + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/paper-dialog-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-dialog-behavior": { + "version": "3.0.1", + "requires": { + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-dialog-scrollable": { + "version": "3.0.1", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/paper-dialog-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-dropdown-menu": { + "version": "3.2.0", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-iconset-svg": "^3.0.0-pre.26", + "@polymer/iron-validatable-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-input": "^3.1.0", + "@polymer/paper-menu-button": "^3.1.0", + "@polymer/paper-ripple": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.3.1" + } + }, + "@polymer/paper-icon-button": { + "version": "3.0.2", + "requires": { + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-input": { + "version": "3.2.1", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-autogrow-textarea": "^3.0.0-pre.26", + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-form-element-behavior": "^3.0.0-pre.26", + "@polymer/iron-input": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-item": { + "version": "3.0.1", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-listbox": { + "version": "3.0.1", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-menu-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-menu-button": { + "version": "3.1.0", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-dropdown": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.1.0", + "@polymer/neon-animation": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-progress": { + "version": "3.0.1", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-range-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-ripple": { + "version": "3.0.2", + "requires": { + "@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-styles": { + "version": "3.0.1", + "requires": { + "@polymer/font-roboto": "^3.0.1", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-tabs": { + "version": "3.1.0", + "requires": { + "@polymer/iron-behaviors": "^3.0.0-pre.26", + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-icon": "^3.0.0-pre.26", + "@polymer/iron-iconset-svg": "^3.0.0-pre.26", + "@polymer/iron-menu-behavior": "^3.0.0-pre.26", + "@polymer/iron-resizable-behavior": "^3.0.0-pre.26", + "@polymer/paper-behaviors": "^3.0.0-pre.27", + "@polymer/paper-icon-button": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-toast": { + "version": "3.0.1", + "requires": { + "@polymer/iron-a11y-announcer": "^3.0.0-pre.26", + "@polymer/iron-fit-behavior": "^3.0.0-pre.26", + "@polymer/iron-overlay-behavior": "^3.0.0-pre.27", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/paper-tooltip": { + "version": "3.0.1", + "requires": { + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, + "@polymer/polymer": { + "version": "3.4.1", + "requires": { + "@webcomponents/shadycss": "^1.9.1" + } + }, + "@sentry-internal/tracing": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.74.0.tgz", + "integrity": "sha512-JK6IRGgdtZjswGfaGIHNWIThffhOHzVIIaGmglui+VFIzOsOqePjoxaDV0MEvzafxXZD7eWqGE5RGuZ0n6HFVg==", + "requires": { + "@sentry/core": "7.74.0", + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0", + "tslib": "^2.4.1 || ^1.9.3" + } + }, + "@sentry/browser": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.74.0.tgz", + "integrity": "sha512-Njr8216Z1dFUcl6NqBOk20dssK9SjoVddY74Xq+Q4p3NfXBG3lkMcACXor7SFoJRZXq8CZWGS13Cc5KwViRw4g==", + "requires": { + "@sentry-internal/tracing": "7.74.0", + "@sentry/core": "7.74.0", + "@sentry/replay": "7.74.0", + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0", + "tslib": "^2.4.1 || ^1.9.3" + } + }, + "@sentry/core": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.74.0.tgz", + "integrity": "sha512-83NRuqn7nDZkSVBN5yJQqcpXDG4yMYiB7TkYUKrGTzBpRy6KUOrkCdybuKk0oraTIGiGSe5WEwCFySiNgR9FzA==", + "requires": { + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0", + "tslib": "^2.4.1 || ^1.9.3" + } + }, + "@sentry/electron": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@sentry/electron/-/electron-4.14.0.tgz", + "integrity": "sha512-5laPImINGd86osNUu9UyGWB9dfK9O6hmydSTFWsHWHgFsd/2YKtOgjdXgMu+znyU+sy+lU9z+wwEq/h4yivPZQ==", + "requires": { + "@sentry/browser": "7.74.0", + "@sentry/core": "7.74.0", + "@sentry/node": "7.74.0", + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0", + "deepmerge": "4.3.0", + "lru_map": "^0.3.3", + "tslib": "^2.5.0" + } + }, + "@sentry/node": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.74.0.tgz", + "integrity": "sha512-uBmW2/z0cz/WFIG74ZF7lSipO0XNzMf9yrdqnZXnGDYsUZE4I4QiqDN0hNi6fkTgf9MYRC8uFem2OkAvyPJ74Q==", + "requires": { + "@sentry-internal/tracing": "7.74.0", + "@sentry/core": "7.74.0", + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0", + "cookie": "^0.5.0", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^2.4.1 || ^1.9.3" + }, + "dependencies": { + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + } + } + }, + "@sentry/replay": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.74.0.tgz", + "integrity": "sha512-GoYa3cHTTFVI/J1cnZ0i4X128mf/JljaswO3PWNTe2k3lSHq/LM5aV0keClRvwM0W8hlix8oOTT06nnenOUmmw==", + "requires": { + "@sentry/core": "7.74.0", + "@sentry/types": "7.74.0", + "@sentry/utils": "7.74.0" + } + }, + "@sentry/types": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.74.0.tgz", + "integrity": "sha512-rI5eIRbUycWjn6s6o3yAjjWtIvYSxZDdnKv5je2EZINfLKcMPj1dkl6wQd2F4y7gLfD/N6Y0wZYIXC3DUdJQQg==" + }, + "@sentry/utils": { + "version": "7.74.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.74.0.tgz", + "integrity": "sha512-k3np8nuTPtx5KDODPtULfFln4UXdE56MZCcF19Jv6Ljxf+YN/Ady1+0Oi3e0XoSvFpWNyWnglauT7M65qCE6kg==", + "requires": { + "@sentry/types": "7.74.0", + "tslib": "^2.4.1 || ^1.9.3" + } + }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true + }, + "@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "@tootallnate/once": { + "version": "2.0.0" + }, + "@types/body-parser": { + "version": "1.19.1", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bonjour": { + "version": "3.5.10", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/bunyan": { + "version": "1.8.7", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/caseless": { + "version": "0.12.2", + "dev": true + }, + "@types/component-emitter": { + "version": "1.2.11", + "dev": true + }, + "@types/connect": { + "version": "3.4.35", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.3.5", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "@types/cookie": { + "version": "0.4.1", + "dev": true + }, + "@types/cors": { + "version": "2.8.12", + "dev": true + }, + "@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, + "@types/eslint": { + "version": "8.44.4", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.4.tgz", + "integrity": "sha512-lOzjyfY/D9QR4hY9oblZ76B90MYTB3RrQ4z2vBIJKj9ROCRqdkYl2gSUx1x1a4IWPjKJZLL4Aw1Zfay7eMnmnA==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", + "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", + "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", + "dev": true + }, + "@types/expect": { + "version": "1.20.4", + "dev": true + }, + "@types/express": { + "version": "4.17.13", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.24", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/formidable": { + "version": "1.2.4", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "optional": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/hapi__joi": { + "version": "17.1.7", + "dev": true + }, + "@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "@types/http-proxy": { + "version": "1.17.9", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/jasmine": { + "version": "3.10.1", + "dev": true + }, + "@types/js-yaml": { + "version": "3.12.7", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.9", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/mime": { + "version": "1.3.2", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.5", + "dev": true + }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "@types/node": { + "version": "16.11.34", + "dev": true + }, + "@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "@types/node-forge": { + "version": "1.0.2", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/plist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", + "integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "@types/polymer": { + "version": "1.2.11", + "dev": true, + "requires": { + "@types/webcomponents.js": "*" + } + }, + "@types/puppeteer": { + "version": "5.4.4", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/qs": { + "version": "6.9.7", + "dev": true + }, + "@types/randomstring": { + "version": "1.1.7", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "dev": true + }, + "@types/request": { + "version": "2.48.7", + "dev": true, + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, + "@types/restify": { + "version": "8.5.2", + "dev": true, + "requires": { + "@types/bunyan": "*", + "@types/formidable": "*", + "@types/node": "*", + "@types/spdy": "*" + } + }, + "@types/restify-cors-middleware": { + "version": "1.0.2", + "dev": true, + "requires": { + "@types/node": "*", + "@types/restify": "*" + } + }, + "@types/retry": { + "version": "0.12.0", + "dev": true + }, + "@types/semver": { + "version": "5.5.0", + "dev": true + }, + "@types/serve-index": { + "version": "1.9.1", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/serve-static": { + "version": "1.13.10", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/sockjs": { + "version": "0.3.33", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/spdy": { + "version": "3.4.5", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/tmp": { + "version": "0.2.2", + "dev": true + }, + "@types/tough-cookie": { + "version": "4.0.1", + "dev": true + }, + "@types/verror": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", + "integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==", + "dev": true, + "optional": true + }, + "@types/vinyl": { + "version": "2.0.6", + "dev": true, + "requires": { + "@types/expect": "^1.20.4", + "@types/node": "*" + } + }, + "@types/webcomponents.js": { + "version": "0.6.37", + "dev": true + }, + "@types/ws": { + "version": "8.5.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.7.tgz", + "integrity": "sha512-6UrLjiDUvn40CMrAubXuIVtj2PEfKDffJS7ychvnPU44j+KVeXmdHHTgqcM/dxLUTHxlXHiFM8Skmb8ozGdTnQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz", + "integrity": "sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@types/yauzl": { + "version": "2.9.2", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.14.0", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.14.0", + "@typescript-eslint/type-utils": "5.14.0", + "@typescript-eslint/utils": "5.14.0", + "debug": "^4.3.2", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.2.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.7", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "5.14.0", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.14.0", + "@typescript-eslint/types": "5.14.0", + "@typescript-eslint/typescript-estree": "5.14.0", + "debug": "^4.3.2" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.14.0", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.14.0", + "@typescript-eslint/visitor-keys": "5.14.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.14.0", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.14.0", + "debug": "^4.3.2", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.14.0", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.14.0", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.14.0", + "@typescript-eslint/visitor-keys": "5.14.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "array-union": { + "version": "2.1.0", + "dev": true + }, + "globby": { + "version": "11.1.0", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.7", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "slash": { + "version": "3.0.0", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "dev": true + } + } + }, + "@typescript-eslint/utils": { + "version": "5.14.0", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.14.0", + "@typescript-eslint/types": "5.14.0", + "@typescript-eslint/typescript-estree": "5.14.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.14.0", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.14.0", + "eslint-visitor-keys": "^3.0.0" + } + }, + "@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webcomponents/shadycss": { + "version": "1.11.0" + }, + "@webcomponents/webcomponentsjs": { + "version": "2.6.0" + }, + "@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "requires": {} + }, + "@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "requires": {} + }, + "@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "requires": {} + }, + "@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 + }, + "7zip-bin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz", + "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "dev": true + }, + "abort-controller": { + "version": "3.0.0", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "accepts": { + "version": "1.3.8", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "requires": {} + }, + "acorn-jsx": { + "version": "5.3.2", + "dev": true, + "requires": {} + }, + "agent-base": { + "version": "6.0.2", + "requires": { + "debug": "4" + } + }, + "ajv": { + "version": "6.12.6", + "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-formats": { + "version": "2.1.1", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "dev": true + } + } + }, + "ajv-keywords": { + "version": "3.5.2", + "dev": true, + "requires": {} + }, + "ansi-colors": { + "version": "4.1.1", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.2", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-gray": { + "version": "0.1.1", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-html-community": { + "version": "0.0.8", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "ansi-wrap": { + "version": "0.1.0", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true + }, + "app-builder-lib": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.6.0.tgz", + "integrity": "sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA==", + "dev": true, + "requires": { + "@develar/schema-utils": "~2.6.5", + "@electron/universal": "1.2.1", + "@malept/flatpak-bundler": "^0.4.0", + "7zip-bin": "~5.1.1", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.7", + "electron-osx-sign": "^0.6.0", + "electron-publish": "23.6.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^4.0.10", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^3.1.2", + "read-config-file": "6.2.0", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.7", + "tar": "^6.1.11", + "temp-file": "^3.4.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "ci-info": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", + "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", + "dev": true + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "requires": { + "ci-info": "^3.2.0" + } + }, + "isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "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" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "append-buffer": { + "version": "1.0.2", + "dev": true, + "requires": { + "buffer-equal": "^1.0.0" + } + }, + "archive-type": { + "version": "4.0.0", + "dev": true, + "requires": { + "file-type": "^4.2.0" + }, + "dependencies": { + "file-type": { + "version": "4.4.0", + "dev": true + } + } + }, + "archy": { + "version": "1.0.0", + "dev": true + }, + "arg": { + "version": "4.1.3", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "args": { + "version": "2.6.1", + "dev": true, + "requires": { + "camelcase": "4.1.0", + "chalk": "1.1.3", + "minimist": "1.2.0", + "pkginfo": "0.4.0", + "string-similarity": "1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "dev": true + } + } + }, + "arr-diff": { + "version": "4.0.0", + "dev": true + }, + "arr-filter": { + "version": "1.1.2", + "dev": true, + "requires": { + "make-iterator": "^1.0.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "dev": true + }, + "arr-map": { + "version": "2.0.2", + "dev": true, + "requires": { + "make-iterator": "^1.0.0" + } + }, + "arr-union": { + "version": "3.1.0", + "dev": true + }, + "array-differ": { + "version": "3.0.0", + "dev": true + }, + "array-each": { + "version": "1.0.1", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "dev": true + }, + "array-flatten": { + "version": "1.1.1" + }, + "array-ify": { + "version": "1.0.0", + "dev": true + }, + "array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-initial": { + "version": "1.1.0", + "dev": true, + "requires": { + "array-slice": "^1.0.0", + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "dev": true + } + } + }, + "array-last": { + "version": "1.3.0", + "dev": true, + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "dev": true + } + } + }, + "array-slice": { + "version": "1.1.0", + "dev": true + }, + "array-sort": { + "version": "1.0.0", + "dev": true, + "requires": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + } + }, + "array-union": { + "version": "1.0.2", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.2" + }, + "array-unique": { + "version": "0.3.2", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, + "arrify": { + "version": "2.0.1" + }, + "asap": { + "version": "2.0.6", + "dev": true + }, + "asar": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/asar/-/asar-3.2.0.tgz", + "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "chromium-pickle-js": "^0.2.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + } + }, + "asn1": { + "version": "0.2.4", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0" + }, + "assign-symbols": { + "version": "1.0.0", + "dev": true + }, + "ast-metadata-inferer": { + "version": "0.7.0", + "dev": true, + "requires": { + "@mdn/browser-compat-data": "^3.3.14" + }, + "dependencies": { + "@mdn/browser-compat-data": { + "version": "3.3.14", + "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, + "optional": true + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "async-done": { + "version": "1.3.2", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + } + }, + "async-each": { + "version": "1.0.3", + "dev": true + }, + "async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true + }, + "async-retry": { + "version": "1.3.3", + "dev": true, + "requires": { + "retry": "0.13.1" + } + }, + "async-settle": { + "version": "1.0.0", + "dev": true, + "requires": { + "async-done": "^1.2.2" + } + }, + "asynckit": { + "version": "0.4.0" + }, + "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", + "dev": true + }, + "atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + }, + "aws-sign2": { + "version": "0.7.0" + }, + "aws4": { + "version": "1.11.0" + }, + "bach": { + "version": "1.2.0", + "dev": true, + "requires": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "dev": true + }, + "base": { + "version": "0.11.2", + "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", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "base64-js": { + "version": "1.5.1" + }, + "base64id": { + "version": "2.0.0", + "dev": true + }, + "batch": { + "version": "0.6.1", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bignumber.js": { + "version": "2.4.0", + "dev": true + }, + "bin-build": { + "version": "3.0.0", + "dev": true, + "requires": { + "decompress": "^4.0.0", + "download": "^6.2.2", + "execa": "^0.7.0", + "p-map-series": "^1.0.0", + "tempfile": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "download": { + "version": "6.2.5", + "dev": true, + "requires": { + "caw": "^2.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.0.0", + "ext-name": "^5.0.0", + "file-type": "5.2.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^7.0.0", + "make-dir": "^1.0.0", + "p-event": "^1.0.0", + "pify": "^3.0.0" + } + }, + "execa": { + "version": "0.7.0", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.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" + } + }, + "file-type": { + "version": "5.2.0", + "dev": true + }, + "filenamify": { + "version": "2.1.0", + "dev": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "dev": true + }, + "got": { + "version": "7.1.0", + "dev": true, + "requires": { + "decompress-response": "^3.2.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-plain-obj": "^1.1.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "p-cancelable": "^0.3.0", + "p-timeout": "^1.1.1", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "url-parse-lax": "^1.0.0", + "url-to-options": "^1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "p-cancelable": { + "version": "0.3.0", + "dev": true + }, + "p-event": { + "version": "1.3.0", + "dev": true, + "requires": { + "p-timeout": "^1.1.1" + } + }, + "p-timeout": { + "version": "1.2.1", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "prepend-http": { + "version": "1.0.4", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "dev": true + }, + "temp-dir": { + "version": "1.0.0", + "dev": true + }, + "tempfile": { + "version": "2.0.0", + "dev": true, + "requires": { + "temp-dir": "^1.0.0", + "uuid": "^3.0.1" + } + }, + "url-parse-lax": { + "version": "1.0.0", + "dev": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "dev": true + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bintrees": { + "version": "1.0.1" + }, + "bl": { + "version": "1.2.3", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "bluebird": { + "version": "3.7.2", + "dev": true + }, + "bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5" + } + }, + "bmp-js": { + "version": "0.0.3", + "dev": true + }, + "body-parser": { + "version": "1.20.0", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0" + }, + "destroy": { + "version": "1.2.0" + }, + "iconv-lite": { + "version": "0.4.24", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ms": { + "version": "2.0.0" + }, + "on-finished": { + "version": "2.4.1", + "requires": { + "ee-first": "1.1.1" + } + } + } + }, + "bonjour-service": { + "version": "1.0.12", + "dev": true, + "requires": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.4" + }, + "dependencies": { + "array-flatten": { + "version": "2.1.2", + "dev": true + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "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" + } + }, + "browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + } + }, + "buffer": { + "version": "5.7.1", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "dev": true + }, + "buffer-equal": { + "version": "1.0.0", + "dev": true + }, + "buffer-equal-constant-time": { + "version": "1.0.1" + }, + "buffer-fill": { + "version": "1.0.0", + "dev": true + }, + "buffer-from": { + "version": "1.1.2", + "dev": true + }, + "builder-util": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.6.0.tgz", + "integrity": "sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ==", + "dev": true, + "requires": { + "@types/debug": "^4.1.6", + "@types/fs-extra": "^9.0.11", + "7zip-bin": "~5.1.1", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "ci-info": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", + "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", + "dev": true + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "requires": { + "ci-info": "^3.2.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "builder-util-runtime": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", + "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "sax": "^1.2.4" + } + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true + }, + "bytes": { + "version": "3.1.2" + }, + "cache-base": { + "version": "1.0.1", + "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" + } + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + } + } + }, + "call-bind": { + "version": "1.0.2", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "caller-callsite": { + "version": "2.0.0", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "dev": true + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "camelcase-keys": { + "version": "2.1.0", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "dev": true + } + } + }, + "caniuse-lite": { + "version": "1.0.30001546", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz", + "integrity": "sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==", + "dev": true + }, + "caseless": { + "version": "0.12.0" + }, + "caw": { + "version": "2.0.1", + "dev": true, + "requires": { + "get-proxy": "^2.0.0", + "isurl": "^1.0.0-alpha5", + "tunnel-agent": "^0.6.0", + "url-to-options": "^1.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chardet": { + "version": "0.7.0", + "dev": true + }, + "chokidar": { + "version": "2.1.8", + "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" + } + }, + "chownr": { + "version": "1.1.4", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.3", + "dev": true + }, + "chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true + }, + "ci-info": { + "version": "2.0.0", + "dev": true + }, + "circle-flags": { + "version": "git+ssh://git@github.com/HatScripts/circle-flags.git#a21fc224b3079631993b3b8189c490fa0899ea9f", + "from": "circle-flags@https://github.com/HatScripts/circle-flags" + }, + "class-utils": { + "version": "0.3.6", + "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", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + } + } + }, + "clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.6.1", + "dev": true + }, + "cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "optional": true, + "requires": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + } + }, + "cli-width": { + "version": "3.0.0", + "dev": true + }, + "clipboard-polyfill": { + "version": "2.8.6" + }, + "cliui": { + "version": "7.0.4", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "2.1.2", + "dev": true + }, + "clone-buffer": { + "version": "1.0.0", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "kind-of": { + "version": "6.0.3", + "dev": true + } + } + }, + "clone-response": { + "version": "1.0.2", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "clone-stats": { + "version": "1.0.0", + "dev": true + }, + "cloneable-readable": { + "version": "1.1.3", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "code-point-at": { + "version": "1.1.0", + "dev": true + }, + "collection-map": { + "version": "1.0.0", + "dev": true, + "requires": { + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "collection-visit": { + "version": "1.0.0", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "color-support": { + "version": "1.1.3", + "dev": true + }, + "colorette": { + "version": "2.0.16", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + }, + "compare-func": { + "version": "2.0.0", + "dev": true, + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "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": { + "bytes": { + "version": "3.0.0", + "dev": true + }, + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "config-chain": { + "version": "1.1.13", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "configstore": { + "version": "5.0.1", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "make-dir": { + "version": "3.1.0", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + } + } + }, + "connect": { + "version": "3.7.0", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + } + } + }, + "connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true + }, + "content-disposition": { + "version": "0.5.4", + "requires": { + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1" + } + } + }, + "content-type": { + "version": "1.0.4" + }, + "conventional-changelog-conventionalcommits": { + "version": "4.6.3", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + } + }, + "convert-source-map": { + "version": "1.8.0", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.2", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6" + }, + "copy-descriptor": { + "version": "0.1.1", + "dev": true + }, + "copy-props": { + "version": "2.0.5", + "dev": true, + "requires": { + "each-props": "^1.3.2", + "is-plain-object": "^5.0.0" + } + }, + "copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "requires": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } + } + }, + "core-js": { + "version": "3.19.0", + "dev": true + }, + "core-util-is": { + "version": "1.0.2" + }, + "cors": { + "version": "2.8.5", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cosmiconfig": { + "version": "5.2.1", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "optional": true, + "requires": { + "buffer": "^5.1.0" + } + }, + "cross-fetch": { + "version": "3.1.5", + "dev": true, + "requires": { + "node-fetch": "2.6.7" + } + }, + "cross-spawn": { + "version": "7.0.3", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "dev": true + }, + "css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "dependencies": { + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "requires": {} + }, + "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" + } + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "requires": {} + }, + "postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "dependencies": { + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + } + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "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 + }, + "csv": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.3.5.tgz", + "integrity": "sha512-Y+KTCAUljtq2JaGP42ZL1bymqlU5BkfnFpZhxRczGFDZox2VXhlRHnG5DRshyUrwQzmCdEiLjSqNldCfm1OVCA==", + "requires": { + "csv-generate": "^4.3.0", + "csv-parse": "^5.5.2", + "csv-stringify": "^6.4.4", + "stream-transform": "^3.2.10" + } + }, + "csv-generate": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.3.0.tgz", + "integrity": "sha512-7KdVId/2RgwPIKfWHaHtjBq7I9mgdi8ICzsUyIhP8is6UwpwVGGSC/aPnrZ8/SkgBcCP20lXrdPuP64Irs1VBg==" + }, + "csv-parse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.2.tgz", + "integrity": "sha512-YRVtvdtUNXZCMyK5zd5Wty1W6dNTpGKdqQd4EQ8tl/c6KW1aMBB1Kg1ppky5FONKmEqGJ/8WjLlTNLPne4ioVA==" + }, + "csv-stringify": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.4.tgz", + "integrity": "sha512-NDshLupGa7gp4UG4sSNIqwYJqgSwvds0SvENntxoVoVvTzXcrHvd5gG2MWpbRpSNvk59dlmIe1IwNvSxN4IVmg==" + }, + "currently-unhandled": { + "version": "0.4.1", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "custom-event": { + "version": "1.0.1", + "dev": true + }, + "d": { + "version": "1.0.1", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "dashdash": { + "version": "1.14.1", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-and-time": { + "version": "2.3.1", + "dev": true + }, + "date-format": { + "version": "4.0.5", + "dev": true + }, + "debug": { + "version": "4.3.4", + "requires": { + "ms": "2.1.2" + } + }, + "debuglog": { + "version": "1.0.1", + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true + }, + "decompress": { + "version": "4.2.1", + "dev": true, + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "dev": true + } + } + }, + "pify": { + "version": "2.3.0", + "dev": true + } + } + }, + "decompress-response": { + "version": "3.3.0", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "decompress-tar": { + "version": "4.1.1", + "dev": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "dev": true + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "dev": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "dev": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "dev": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "dev": true, + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "get-stream": { + "version": "2.3.1", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.4", + "dev": true + }, + "deepmerge": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", + "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==" + }, + "default-compare": { + "version": "1.0.0", + "dev": true, + "requires": { + "kind-of": "^5.0.2" + } + }, + "default-gateway": { + "version": "6.0.3", + "dev": true, + "requires": { + "execa": "^5.0.0" + }, + "dependencies": { + "execa": { + "version": "5.1.1", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + } + } + }, + "default-resolution": { + "version": "2.0.0", + "dev": true + }, + "defaults": { + "version": "1.0.3", + "dev": true, + "requires": { + "clone": "^1.0.2" + }, + "dependencies": { + "clone": { + "version": "1.0.4", + "dev": true + } + } + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "define-lazy-prop": { + "version": "2.0.0", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "define-property": { + "version": "2.0.2", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + } + }, + "del": { + "version": "2.2.2", + "dev": true, + "requires": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + }, + "dependencies": { + "arrify": { + "version": "1.0.1", + "dev": true + }, + "globby": { + "version": "5.0.0", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0" + }, + "depd": { + "version": "1.1.2", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "dev": true + }, + "detect-node": { + "version": "2.1.0" + }, + "devtools-protocol": { + "version": "0.0.981744", + "dev": true + }, + "dezalgo": { + "version": "1.0.3", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "di": { + "version": "0.0.1", + "dev": true + }, + "dir-compare": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", + "integrity": "sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA==", + "dev": true, + "requires": { + "buffer-equal": "1.0.0", + "colors": "1.0.3", + "commander": "2.9.0", + "minimatch": "3.0.4" + }, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "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" + } + } + } + }, + "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" + } + }, + "dmg-builder": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.6.0.tgz", + "integrity": "sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA==", + "dev": true, + "requires": { + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "dmg-license": "^1.0.11", + "fs-extra": "^10.0.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "optional": true, + "requires": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "dev": true + }, + "dns-packet": { + "version": "5.3.1", + "dev": true, + "requires": { + "@leichtgewicht/ip-codec": "^2.0.1" + } + }, + "doctrine": { + "version": "3.0.0", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "requires": { + "utila": "~0.4" + } + }, + "dom-serialize": { + "version": "2.2.1", + "dev": true, + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "dom-serializer": { + "version": "0.2.2", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.3.0", + "dev": true + }, + "entities": { + "version": "2.2.0", + "dev": true + } + } + }, + "dom-walk": { + "version": "0.1.2", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "dev": true + }, + "domhandler": { + "version": "2.4.2", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "dot-prop": { + "version": "5.3.0", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "dotenv": { + "version": "8.2.0" + }, + "dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "download": { + "version": "8.0.0", + "dev": true, + "requires": { + "archive-type": "^4.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.2.1", + "ext-name": "^5.0.0", + "file-type": "^11.1.0", + "filenamify": "^3.0.0", + "get-stream": "^4.1.0", + "got": "^8.3.1", + "make-dir": "^2.1.0", + "p-event": "^2.1.0", + "pify": "^4.0.1" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "0.7.0", + "dev": true + }, + "cacheable-request": { + "version": "2.1.4", + "dev": true, + "requires": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "dev": true + } + } + }, + "file-type": { + "version": "11.1.0", + "dev": true + }, + "got": { + "version": "8.3.2", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "dev": true + }, + "pify": { + "version": "3.0.0", + "dev": true + } + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "dev": true + }, + "lowercase-keys": { + "version": "1.0.0", + "dev": true + }, + "normalize-url": { + "version": "2.0.1", + "dev": true, + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + } + }, + "p-cancelable": { + "version": "0.4.1", + "dev": true + }, + "pify": { + "version": "4.0.1", + "dev": true + }, + "sort-keys": { + "version": "2.0.0", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + } + } + }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, + "duplexer3": { + "version": "0.1.4", + "dev": true + }, + "duplexify": { + "version": "3.7.1", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "each-props": { + "version": "1.3.2", + "dev": true, + "requires": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1" + }, + "ejs": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, + "electron": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/electron/-/electron-19.1.9.tgz", + "integrity": "sha512-XT5LkTzIHB+ZtD3dTmNnKjVBWrDWReCKt9G1uAFLz6uJMEVcIUiYO+fph5pLXETiBw/QZBx8egduMEfIccLx+g==", + "dev": true, + "requires": { + "@electron/get": "^1.14.1", + "@types/node": "^16.11.26", + "extract-zip": "^1.0.3" + } + }, + "electron-builder": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.6.0.tgz", + "integrity": "sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw==", + "dev": true, + "requires": { + "@types/yargs": "^17.0.1", + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "dmg-builder": "23.6.0", + "fs-extra": "^10.0.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.2.0", + "simple-update-notifier": "^1.0.7", + "yargs": "^17.5.1" + }, + "dependencies": { + "ci-info": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.1.tgz", + "integrity": "sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg==", + "dev": true + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "requires": { + "ci-info": "^3.2.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + } + }, + "yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "dev": true + } + } + }, + "electron-icon-maker": { + "version": "0.0.4", + "dev": true, + "requires": { + "args": "^2.3.0", + "icon-gen": "1.0.7", + "jimp": "^0.2.27" + } + }, + "electron-notarize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.1.tgz", + "integrity": "sha512-u/ECWhIrhkSQpZM4cJzVZ5TsmkaqrRo5LDC/KMbGF0sPkm53Ng59+M0zp8QVaql0obfJy9vlVT+4iOkAi2UDlA==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "dependencies": { + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "electron-osx-sign": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", + "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==", + "dev": true, + "requires": { + "bluebird": "^3.5.0", + "compare-version": "^0.1.2", + "debug": "^2.6.8", + "isbinaryfile": "^3.0.2", + "minimist": "^1.2.0", + "plist": "^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" + } + }, + "isbinaryfile": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "requires": { + "buffer-alloc": "^1.2.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "electron-publish": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.6.0.tgz", + "integrity": "sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg==", + "dev": true, + "requires": { + "@types/fs-extra": "^9.0.11", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "chalk": "^4.1.1", + "fs-extra": "^10.0.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "electron-to-chromium": { + "version": "1.4.545", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.545.tgz", + "integrity": "sha512-G1HKumUw+y5yxMjewGfKz0XrqG6O+Tb4zrlC/Vs1+9riRXBuFlO0hOEXP3xeI+ltlJkbVUuLkYdmjHYH6Jkiow==", + "dev": true + }, + "electron-updater": { + "version": "4.3.9", + "requires": { + "@types/semver": "^7.3.5", + "builder-util-runtime": "8.7.5", + "fs-extra": "^10.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.4", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.3.5" + }, + "dependencies": { + "@types/semver": { + "version": "7.3.9" + }, + "argparse": { + "version": "2.0.1" + }, + "builder-util-runtime": { + "version": "8.7.5", + "requires": { + "debug": "^4.3.2", + "sax": "^1.2.4" + } + }, + "fs-extra": { + "version": "10.1.0", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsonfile": { + "version": "6.1.0", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.7", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "universalify": { + "version": "2.0.0" + }, + "yallist": { + "version": "4.0.0" + } + } + }, + "emoji-regex": { + "version": "8.0.0", + "dev": true + }, + "encodeurl": { + "version": "1.0.2" + }, + "end-of-stream": { + "version": "1.4.4", + "requires": { + "once": "^1.4.0" + } + }, + "engine.io": { + "version": "6.1.3", + "dev": true, + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" + }, + "dependencies": { + "ws": { + "version": "8.2.3", + "dev": true, + "requires": {} + } + } + }, + "engine.io-parser": { + "version": "5.0.3", + "dev": true, + "requires": { + "@socket.io/base64-arraybuffer": "~1.0.2" + } + }, + "enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "enquirer": { + "version": "2.3.6", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "ent": { + "version": "2.2.0" + }, + "entities": { + "version": "1.1.2", + "dev": true + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true + }, + "envinfo": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "dev": true + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "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", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "optional": true + }, + "es6-iterator": { + "version": "2.0.3", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-promise": { + "version": "3.3.1", + "dev": true + }, + "es6-symbol": { + "version": "3.1.3", + "dev": true, + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "escalade": { + "version": "3.1.1", + "dev": true + }, + "escape-html": { + "version": "1.0.3" + }, + "escape-regexp-component": { + "version": "1.0.2" + }, + "escape-string-regexp": { + "version": "1.0.5", + "dev": true + }, + "eslint": { + "version": "8.10.0", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.2.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "dev": true + }, + "eslint-scope": { + "version": "7.1.1", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "import-fresh": { + "version": "3.3.0", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "resolve-from": { + "version": "4.0.0", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "dev": true + } + } + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-import-resolver-typescript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", + "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "glob": "^7.2.0", + "is-glob": "^4.0.3", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + } + }, + "eslint-module-utils": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", + "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true + } + } + }, + "eslint-plugin-compat": { + "version": "4.0.2", + "dev": true, + "requires": { + "@mdn/browser-compat-data": "^4.1.5", + "ast-metadata-inferer": "^0.7.0", + "browserslist": "^4.16.8", + "caniuse-lite": "^1.0.30001304", + "core-js": "^3.16.2", + "find-up": "^5.0.0", + "lodash.memoize": "4.1.2", + "semver": "7.3.5" + }, + "dependencies": { + "find-up": { + "version": "5.0.0", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "dev": true + }, + "semver": { + "version": "7.3.5", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.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" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "dev": true + }, + "esm": { + "version": "3.2.25", + "dev": true + }, + "espree": { + "version": "9.3.1", + "dev": true, + "requires": { + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esprima": { + "version": "4.0.1" + }, + "esquery": { + "version": "1.4.0", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "dev": true + }, + "etag": { + "version": "1.8.1" + }, + "event-target-shim": { + "version": "5.0.1" + }, + "eventemitter3": { + "version": "4.0.7", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "ewma": { + "version": "2.0.1", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "execa": { + "version": "1.0.0", + "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" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "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" + } + }, + "is-stream": { + "version": "1.1.0", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "dev": true + }, + "semver": { + "version": "5.7.1", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "dev": true + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "exif-parser": { + "version": "0.1.12", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "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", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "express": { + "version": "4.18.1", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.5.0" + }, + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0" + }, + "finalhandler": { + "version": "1.2.0", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "ms": { + "version": "2.0.0" + }, + "on-finished": { + "version": "2.4.1", + "requires": { + "ee-first": "1.1.1" + } + }, + "safe-buffer": { + "version": "5.2.1" + }, + "statuses": { + "version": "2.0.1" + } + } + }, + "ext": { + "version": "1.6.0", + "dev": true, + "requires": { + "type": "^2.5.0" + }, + "dependencies": { + "type": { + "version": "2.6.0", + "dev": true + } + } + }, + "ext-list": { + "version": "2.2.2", + "dev": true, + "requires": { + "mime-db": "^1.28.0" + } + }, + "ext-name": { + "version": "5.0.0", + "dev": true, + "requires": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + } + }, + "extend": { + "version": "3.0.2" + }, + "extend-shallow": { + "version": "2.0.1", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "external-editor": { + "version": "3.1.0", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "tmp": { + "version": "0.0.33", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "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", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "extract-zip": { + "version": "1.7.0", + "dev": true, + "requires": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + } + } + }, + "extsprintf": { + "version": "1.3.0" + }, + "fancy-log": { + "version": "1.3.3", + "dev": true, + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + } + }, + "fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "fast-deep-equal": { + "version": "3.1.3" + }, + "fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "glob-parent": { + "version": "5.1.2", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0" + }, + "fast-levenshtein": { + "version": "1.1.4", + "dev": true + }, + "fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "requires": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==" + }, + "fast-text-encoding": { + "version": "1.0.3" + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fd-slicer": { + "version": "1.1.0", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "figures": { + "version": "3.2.0", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "file-type": { + "version": "3.9.0", + "dev": true + }, + "file-uri-to-path": { + "version": "1.0.0", + "dev": true, + "optional": true + }, + "file-url": { + "version": "1.1.0", + "dev": true, + "requires": { + "meow": "^3.7.0" + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", + "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "filename-reserved-regex": { + "version": "2.0.0", + "dev": true + }, + "filenamify": { + "version": "3.0.0", + "dev": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "fill-range": { + "version": "4.0.0", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "finalhandler": { + "version": "1.1.2", + "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", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + } + } + }, + "find-my-way": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.7.0.tgz", + "integrity": "sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "findup": { + "version": "0.1.5", + "dev": true, + "requires": { + "colors": "~0.6.0-1", + "commander": "~2.1.0" + }, + "dependencies": { + "colors": { + "version": "0.6.2", + "dev": true + }, + "commander": { + "version": "2.1.0", + "dev": true + } + } + }, + "findup-sync": { + "version": "3.0.0", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "fined": { + "version": "1.2.0", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "flagged-respawn": { + "version": "1.0.1", + "dev": true + }, + "flat-cache": { + "version": "3.0.4", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "3.2.5", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "follow-redirects": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, + "forever-agent": { + "version": "0.6.1" + }, + "form-data": { + "version": "3.0.1", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.2" + }, + "forwarded": { + "version": "0.2.0" + }, + "fragment-cache": { + "version": "0.2.1", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2" + }, + "from2": { + "version": "2.3.0", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-constants": { + "version": "1.0.0", + "dev": true + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "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-mkdirp-stream": { + "version": "1.0.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "fs-monkey": { + "version": "1.0.3", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "dev": true + }, + "fsevents": { + "version": "1.2.13", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "function-bind": { + "version": "1.1.1" + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functional-red-black-tree": { + "version": "1.0.1", + "dev": true + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gaxios": { + "version": "5.0.0", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.0.0", + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, + "generate-license-file": { + "version": "1.2.0", + "dev": true, + "requires": { + "arg": "^4.1.3", + "cli-spinners": "^2.6.0", + "enquirer": "^2.3.6", + "esm": "^3.2.25", + "glob": "^7.1.7", + "inquirer": "^7.3.3", + "license-checker": "^25.0.1", + "ora": "^4.1.1" + } + }, + "get-caller-file": { + "version": "2.0.5", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-proxy": { + "version": "2.1.0", + "dev": true, + "requires": { + "npm-conf": "^1.1.0" + } + }, + "get-stdin": { + "version": "6.0.0", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "get-value": { + "version": "2.0.6", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.2.0", + "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": "3.1.0", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "glob-stream": { + "version": "6.1.0", + "dev": true, + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "glob-watcher": { + "version": "5.0.5", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "normalize-path": "^3.0.0", + "object.defaults": "^1.1.0" + } + }, + "global": { + "version": "4.4.0", + "dev": true, + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "optional": true, + "requires": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.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, + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "optional": true + } + } + }, + "global-modules": { + "version": "1.0.0", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "global-tunnel-ng": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", + "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", + "dev": true, + "optional": true, + "requires": { + "encodeurl": "^1.0.2", + "lodash": "^4.17.10", + "npm-conf": "^1.1.3", + "tunnel": "^0.0.6" + } + }, + "globals": { + "version": "13.12.1", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + }, + "dependencies": { + "type-fest": { + "version": "0.20.2", + "dev": true + } + } + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "optional": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "requires": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + } + }, + "glogg": { + "version": "1.0.2", + "dev": true, + "requires": { + "sparkles": "^1.0.0" + } + }, + "google-auth-library": { + "version": "8.0.2", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.0.0", + "gtoken": "^5.3.2", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0" + } + } + }, + "google-p12-pem": { + "version": "3.1.4", + "requires": { + "node-forge": "^1.3.1" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", + "dev": true + }, + "gtoken": { + "version": "5.3.2", + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "dependencies": { + "gaxios": { + "version": "4.3.3", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + } + } + }, + "gulp": { + "version": "4.0.2", + "dev": true, + "requires": { + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + } + }, + "gulp-cli": { + "version": "2.3.0", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.4.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.2.0", + "yargs": "^7.1.0" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "dev": true + }, + "camelcase": { + "version": "3.0.0", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "get-caller-file": { + "version": "1.0.3", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "y18n": { + "version": "3.2.2", + "dev": true + }, + "yargs": { + "version": "7.1.2", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.1" + } + }, + "yargs-parser": { + "version": "5.0.1", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" + } + } + } + }, + "gulp-posthtml": { + "version": "3.0.5", + "dev": true, + "requires": { + "plugin-error": "^1.0.1", + "posthtml": "^0.11.6", + "posthtml-load-config": "^1.0.0", + "through2": "^3.0.2" + } + }, + "gulp-replace": { + "version": "1.1.3", + "dev": true, + "requires": { + "@types/node": "^14.14.41", + "@types/vinyl": "^2.0.4", + "istextorbinary": "^3.0.0", + "replacestream": "^4.0.3", + "yargs-parser": ">=5.0.0-security.0" + }, + "dependencies": { + "@types/node": { + "version": "14.18.17", + "dev": true + }, + "binaryextensions": { + "version": "2.3.0", + "dev": true + }, + "istextorbinary": { + "version": "3.3.0", + "dev": true, + "requires": { + "binaryextensions": "^2.2.0", + "textextensions": "^3.2.0" + } + }, + "textextensions": { + "version": "3.3.0", + "dev": true + } + } + }, + "gulplog": { + "version": "1.0.0", + "dev": true, + "requires": { + "glogg": "^1.0.0" + } + }, + "handle-thing": { + "version": "2.0.1" + }, + "har-schema": { + "version": "2.0.0" + }, + "har-validator": { + "version": "5.1.5", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "dev": true + } + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbol-support-x": { + "version": "1.4.2", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-to-string-tag-x": { + "version": "1.4.1", + "dev": true, + "requires": { + "has-symbol-support-x": "^1.4.1" + } + }, + "has-tostringtag": { + "version": "1.0.0", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "has-value": { + "version": "1.0.0", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-stream-validation": { + "version": "0.2.4", + "dev": true + }, + "hasha": { + "version": "2.2.0", + "dev": true, + "requires": { + "is-stream": "^1.0.1", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "dev": true + } + } + }, + "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 + }, + "homedir-polyfill": { + "version": "1.0.3", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "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" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "hpack.js": { + "version": "2.1.6", + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "html-entities": { + "version": "2.3.3", + "dev": true + }, + "html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "requires": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + } + } + }, + "html-webpack-plugin": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz", + "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==", + "dev": true, + "requires": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + } + }, + "htmlparser2": { + "version": "3.10.1", + "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": { + "readable-stream": { + "version": "3.6.0", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "http-deceiver": { + "version": "1.2.7" + }, + "http-errors": { + "version": "2.0.0", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "dependencies": { + "depd": { + "version": "2.0.0" + }, + "statuses": { + "version": "2.0.1" + } + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-agent": { + "version": "5.0.0", + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "http-proxy-middleware": { + "version": "2.0.6", + "dev": true, + "requires": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "dev": true + }, + "is-plain-obj": { + "version": "3.0.0", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "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": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true + }, + "https-proxy-agent": { + "version": "5.0.0", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "1.1.1", + "dev": true + }, + "husky": { + "version": "1.3.1", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.7", + "execa": "^1.0.0", + "find-up": "^3.0.0", + "get-stdin": "^6.0.0", + "is-ci": "^2.0.0", + "pkg-dir": "^3.0.0", + "please-upgrade-node": "^3.1.1", + "read-pkg": "^4.0.1", + "run-node": "^1.0.0", + "slash": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "dev": true + } + } + }, + "icon-gen": { + "version": "1.0.7", + "dev": true, + "requires": { + "del": "^2.2.2", + "mkdirp": "^0.5.1", + "pngjs": "^3.0.0", + "svg2png": "4.1.0", + "uuid": "^3.0.0" + } + }, + "iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "optional": true, + "requires": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "ieee754": { + "version": "1.2.1" + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "2.0.0", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.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" + } + }, + "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" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4" + }, + "ini": { + "version": "1.3.8", + "dev": true + }, + "inquirer": { + "version": "7.3.3", + "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" + } + }, + "internal-slot": { + "version": "1.0.3", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "interpret": { + "version": "1.4.0", + "dev": true + }, + "intl-format-cache": { + "version": "4.3.1" + }, + "intl-messageformat": { + "version": "7.8.4", + "requires": { + "intl-format-cache": "^4.2.21", + "intl-messageformat-parser": "^3.6.4" + } + }, + "intl-messageformat-parser": { + "version": "3.6.4", + "requires": { + "@formatjs/intl-unified-numberformat": "^3.2.0" + } + }, + "into-stream": { + "version": "3.1.0", + "dev": true, + "requires": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "dev": true + }, + "ip-regex": { + "version": "4.3.0" + }, + "ipaddr.js": { + "version": "1.9.1" + }, + "is": { + "version": "3.3.0" + }, + "is-absolute": { + "version": "1.0.0", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "dev": true + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "1.0.1", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "dev": true + }, + "is-callable": { + "version": "1.2.4", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "dev": true + } + } + }, + "is-date-object": { + "version": "1.0.5", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "dev": true + }, + "is-docker": { + "version": "2.2.1", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "dev": true + }, + "is-finite": { + "version": "1.1.0", + "dev": true + }, + "is-function": { + "version": "1.0.2", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "dev": true + }, + "is-invalid-path": { + "version": "0.1.0", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, + "is-natural-number": { + "version": "4.0.1", + "dev": true + }, + "is-negated-glob": { + "version": "1.0.0", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-obj": { + "version": "2.0.0", + "dev": true + }, + "is-object": { + "version": "1.0.2", + "dev": true + }, + "is-path-cwd": { + "version": "1.0.0", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "dev": true + }, + "is-plain-object": { + "version": "5.0.0", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-relative": { + "version": "1.0.0", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-retry-allowed": { + "version": "1.2.0", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1" + }, + "is-string": { + "version": "1.0.7", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typedarray": { + "version": "1.0.0" + }, + "is-unc-path": { + "version": "1.0.0", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-utf8": { + "version": "0.2.1", + "dev": true + }, + "is-valid-glob": { + "version": "1.0.0", + "dev": true + }, + "is-valid-path": { + "version": "0.1.1", + "dev": true, + "requires": { + "is-invalid-path": "^0.1.0" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-windows": { + "version": "1.0.2", + "dev": true + }, + "isarray": { + "version": "1.0.0" + }, + "isbinaryfile": { + "version": "4.0.8", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "dev": true + }, + "isstream": { + "version": "0.1.2" + }, + "isurl": { + "version": "1.0.0", + "dev": true, + "requires": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + } + }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + } + }, + "jasmine": { + "version": "3.10.0", + "dev": true, + "requires": { + "glob": "^7.1.6", + "jasmine-core": "~3.10.0" + } + }, + "jasmine-core": { + "version": "3.10.1", + "dev": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jimp": { + "version": "0.2.28", + "dev": true, + "requires": { + "bignumber.js": "^2.1.0", + "bmp-js": "0.0.3", + "es6-promise": "^3.0.2", + "exif-parser": "^0.1.9", + "file-type": "^3.1.0", + "jpeg-js": "^0.2.0", + "load-bmfont": "^1.2.3", + "mime": "^1.3.4", + "mkdirp": "0.5.1", + "pixelmatch": "^4.0.0", + "pngjs": "^3.0.0", + "read-chunk": "^1.0.1", + "request": "^2.65.0", + "stream-to-buffer": "^0.1.0", + "tinycolor2": "^1.1.2", + "url-regex": "^3.0.0" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "jpeg-js": { + "version": "0.2.0", + "dev": true + }, + "js-base64": { + "version": "3.7.2" + }, + "js-yaml": { + "version": "3.14.1", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1" + }, + "json-bigint": { + "version": "1.0.0", + "requires": { + "bignumber.js": "^9.0.0" + }, + "dependencies": { + "bignumber.js": { + "version": "9.0.2" + } + } + }, + "json-buffer": { + "version": "3.0.0", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1" + }, + "json5": { + "version": "1.0.1", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonic": { + "version": "0.3.1" + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "just-debounce": { + "version": "1.1.0", + "dev": true + }, + "jwa": { + "version": "2.0.0", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "karma": { + "version": "6.3.17", + "dev": true, + "requires": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.2.0", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "dependencies": { + "anymatch": { + "version": "3.1.2", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "dev": true + }, + "braces": { + "version": "3.0.2", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.5.3", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "fill-range": { + "version": "7.0.1", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.2", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rimraf": { + "version": "3.0.2", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "to-regex-range": { + "version": "5.0.1", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "karma-chrome-launcher": { + "version": "3.1.0", + "dev": true, + "requires": { + "which": "^1.2.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "karma-jasmine": { + "version": "4.0.1", + "dev": true, + "requires": { + "jasmine-core": "^3.6.0" + } + }, + "karma-webpack": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.0.tgz", + "integrity": "sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "webpack-merge": "^4.1.5" + }, + "dependencies": { + "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" + } + } + } + }, + "kew": { + "version": "0.7.0", + "dev": true + }, + "keyv": { + "version": "3.0.0", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "dev": true + }, + "klaw": { + "version": "1.3.1", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "last-run": { + "version": "1.1.1", + "dev": true, + "requires": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + } + }, + "launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dev": true, + "requires": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + }, + "dependencies": { + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + } + } + }, + "lazy-val": { + "version": "1.0.5" + }, + "lazystream": { + "version": "1.0.1", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + } + }, + "lcid": { + "version": "1.0.0", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "lead": { + "version": "1.0.0", + "dev": true, + "requires": { + "flush-write-stream": "^1.0.2" + } + }, + "levn": { + "version": "0.4.1", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "license-checker": { + "version": "25.0.1", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "dev": true + }, + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "dev": true + }, + "semver": { + "version": "5.7.1", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "liftoff": { + "version": "3.1.0", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "lit-element": { + "version": "2.5.1", + "requires": { + "lit-html": "^1.1.1" + } + }, + "lit-html": { + "version": "1.4.1" + }, + "load-bmfont": { + "version": "1.4.1", + "dev": true, + "requires": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^2.9.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + }, + "dependencies": { + "buffer-equal": { + "version": "0.0.1", + "dev": true + }, + "mime": { + "version": "1.6.0", + "dev": true + } + } + }, + "load-json-file": { + "version": "1.1.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "parse-json": { + "version": "2.2.0", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pify": { + "version": "2.3.0", + "dev": true + } + } + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.21" + }, + "lodash.assign": { + "version": "4.2.0", + "dev": true + }, + "lodash.escaperegexp": { + "version": "4.1.2" + }, + "lodash.isequal": { + "version": "4.5.0" + }, + "lodash.memoize": { + "version": "4.1.2", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "dev": true + }, + "log-symbols": { + "version": "3.0.0", + "dev": true, + "requires": { + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "log4js": { + "version": "6.4.3", + "dev": true, + "requires": { + "date-format": "^4.0.5", + "debug": "^4.3.3", + "flatted": "^3.2.5", + "rfdc": "^1.3.0", + "streamroller": "^3.0.5" + } + }, + "loud-rejection": { + "version": "1.6.0", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "dev": true + }, + "lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==" + }, + "make-dir": { + "version": "2.1.0", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "dev": true + }, + "semver": { + "version": "5.7.1", + "dev": true + } + } + }, + "make-iterator": { + "version": "1.0.1", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "dev": true + } + } + }, + "map-cache": { + "version": "0.2.2", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "matchdep": { + "version": "2.0.0", + "dev": true, + "requires": { + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" + }, + "dependencies": { + "findup-sync": { + "version": "2.0.0", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "is-glob": { + "version": "3.1.0", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "optional": true, + "requires": { + "escape-string-regexp": "^4.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "optional": true + } + } + }, + "media-typer": { + "version": "0.3.0" + }, + "memfs": { + "version": "3.4.1", + "dev": true, + "requires": { + "fs-monkey": "1.0.3" + } + }, + "meow": { + "version": "3.7.0", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "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" + } + }, + "semver": { + "version": "5.7.1", + "dev": true + } + } + }, + "merge-descriptors": { + "version": "1.0.1" + }, + "merge-stream": { + "version": "2.0.0", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "dev": true + }, + "methods": { + "version": "1.1.2" + }, + "micromatch": { + "version": "3.1.10", + "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" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "kind-of": { + "version": "6.0.3", + "dev": true + } + } + }, + "mime": { + "version": "2.6.0", + "dev": true + }, + "mime-db": { + "version": "1.52.0" + }, + "mime-types": { + "version": "2.1.35", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "dev": true + }, + "min-document": { + "version": "2.19.0", + "dev": true, + "requires": { + "dom-walk": "^0.1.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "dev": true + }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "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" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "mixin-deep": { + "version": "1.3.2", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "mkdirp": { + "version": "0.5.6", + "dev": true, + "requires": { + "minimist": "^1.2.6" + }, + "dependencies": { + "minimist": { + "version": "1.2.6", + "dev": true + } + } + }, + "mkdirp-classic": { + "version": "0.5.3", + "dev": true + }, + "mri": { + "version": "1.2.0", + "dev": true + }, + "ms": { + "version": "2.1.2" + }, + "multicast-dns": { + "version": "7.2.4", + "dev": true, + "requires": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + } + }, + "multimatch": { + "version": "4.0.0", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "array-union": { + "version": "2.1.0", + "dev": true + } + } + }, + "mute-stdout": { + "version": "1.0.1", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "dev": true + }, + "nan": { + "version": "2.15.0", + "optional": true + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "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" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "kind-of": { + "version": "6.0.3", + "dev": true + } + } + }, + "natural-compare": { + "version": "1.4.0", + "dev": true + }, + "negotiator": { + "version": "0.6.3" + }, + "neo-async": { + "version": "2.6.2", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "dev": true + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "optional": true + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1" + }, + "node-jq": { + "version": "1.12.0", + "dev": true, + "requires": { + "@hapi/joi": "^16.1.7", + "@types/hapi__joi": "^17.1.0", + "bin-build": "^3.0.0", + "download": "^8.0.0", + "is-valid-path": "^0.1.1", + "strip-eof": "^1.0.0", + "strip-final-newline": "^2.0.0", + "tempfile": "^3.0.0" + } + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "nopt": { + "version": "4.0.3", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "normalize-path": { + "version": "3.0.0", + "dev": true + }, + "normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "dev": true + }, + "now-and-later": { + "version": "2.0.1", + "dev": true, + "requires": { + "once": "^1.3.2" + } + }, + "npm-conf": { + "version": "1.1.3", + "dev": true, + "requires": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "dev": true, + "requires": { + "path-key": "^2.0.0" + }, + "dependencies": { + "path-key": { + "version": "2.0.1", + "dev": true + } + } + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0" + }, + "object-assign": { + "version": "4.1.1", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "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", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "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", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "object-keys": { + "version": "1.1.1", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.2", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.defaults": { + "version": "1.1.0", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "object.map": { + "version": "1.0.1", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.reduce": { + "version": "1.0.1", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "obuf": { + "version": "1.1.2" + }, + "on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" + }, + "on-finished": { + "version": "2.3.0", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "dev": true + }, + "once": { + "version": "1.4.0", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.4.0", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + } + } + }, + "optionator": { + "version": "0.9.1", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "dependencies": { + "fast-levenshtein": { + "version": "2.0.6", + "dev": true + } + } + }, + "ora": { + "version": "4.1.1", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.2.0", + "is-interactive": "^1.0.0", + "log-symbols": "^3.0.0", + "mute-stream": "0.0.8", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "os-homedir": { + "version": "1.0.2", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "outline-manager": { + "version": "file:src/server_manager", + "requires": { + "@polymer/app-layout": "^3.0.0", + "@polymer/app-localize-behavior": "^3.0.0", + "@polymer/font-roboto": "^3.0.0", + "@polymer/iron-autogrow-textarea": "^3.0.0", + "@polymer/iron-collapse": "^3.0.0", + "@polymer/iron-fit-behavior": "^3.0.0", + "@polymer/iron-icon": "^3.0.1", + "@polymer/iron-icons": "^3.0.0", + "@polymer/iron-pages": "^3.0.0", + "@polymer/paper-button": "^3.0.0", + "@polymer/paper-checkbox": "^3.0.0", + "@polymer/paper-dialog": "^3.0.0", + "@polymer/paper-dialog-scrollable": "^3.0.0", + "@polymer/paper-dropdown-menu": "^3.0.0", + "@polymer/paper-icon-button": "^3.0.0", + "@polymer/paper-input": "^3.0.0", + "@polymer/paper-item": "^3.0.0", + "@polymer/paper-listbox": "^3.0.0", + "@polymer/paper-progress": "^3.0.0", + "@polymer/paper-tabs": "^3.0.0", + "@polymer/paper-toast": "^3.0.0", + "@polymer/paper-tooltip": "^3.0.0", + "@sentry/electron": "^4.14.0", + "@types/node": "^16.11.29", + "@types/node-forge": "^1.0.2", + "@types/polymer": "^1.2.9", + "@types/puppeteer": "^5.4.2", + "@types/request": "^2.47.1", + "@types/semver": "^5.5.0", + "@webcomponents/webcomponentsjs": "^2.0.0", + "circle-flags": "git+ssh://git@github.com/HatScripts/circle-flags.git#a21fc224b3079631993b3b8189c490fa0899ea9f", + "clipboard-polyfill": "^2.4.6", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "dotenv": "~8.2.0", + "electron": "19.1.9", + "electron-builder": "^23.6.0", + "electron-icon-maker": "^0.0.4", + "electron-notarize": "^1.2.1", + "electron-to-chromium": "^1.4.328", + "electron-updater": "^4.1.2", + "express": "^4.17.1", + "google-auth-library": "^8.0.2", + "gulp": "^4.0.0", + "gulp-posthtml": "^3.0.4", + "gulp-replace": "^1.0.0", + "html-webpack-plugin": "^5.5.3", + "intl-messageformat": "^7", + "jsonic": "^0.3.1", + "karma": "^6.3.16", + "karma-chrome-launcher": "^3.1.0", + "karma-jasmine": "^4.0.1", + "karma-webpack": "^5.0.0", + "lit-element": "^2.3.1", + "node-forge": "^1.3.1", + "node-jq": "^1.11.2", + "postcss": "^7.0.29", + "postcss-rtl": "^1.7.3", + "posthtml-postcss": "^0.2.6", + "puppeteer": "^13.6.0", + "request": "^2.87.0", + "style-loader": "^3.3.3", + "ts-loader": "^9.5.0", + "web-animations-js": "^2.3.1", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1", + "webpack-node-externals": "^3.0.0" + } + }, + "outline-metrics-server": { + "version": "file:src/metrics_server", + "requires": { + "@google-cloud/bigquery": "^5.12.0", + "@google-cloud/storage": "^5.19.4", + "@types/express": "^4.17.12", + "express": "^4.17.1" + } + }, + "outline-server": { + "version": "file:src/shadowbox", + "requires": { + "@types/js-yaml": "^3.11.2", + "@types/node": "^12", + "@types/randomstring": "^1.1.6", + "@types/restify": "^8.4.2", + "@types/restify-cors-middleware": "^1.0.1", + "@types/tmp": "^0.2.1", + "ip-regex": "^4.1.0", + "js-yaml": "^3.12.0", + "outline-shadowsocksconfig": "git+ssh://git@github.com/Jigsaw-Code/outline-shadowsocksconfig.git#add590ed57277653d02dd2031ae301500ae881e1", + "prom-client": "^11.1.3", + "randomstring": "^1.1.5", + "restify": "^11.1.0", + "restify-cors-middleware2": "^2.2.1", + "restify-errors": "^8.0.2", + "tmp": "^0.2.1", + "ts-loader": "^9.5.0", + "uuid": "^3.1.0", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "@types/node": { + "version": "12.20.51", + "dev": true + } + } + }, + "outline-shadowsocksconfig": { + "version": "git+ssh://git@github.com/Jigsaw-Code/outline-shadowsocksconfig.git#add590ed57277653d02dd2031ae301500ae881e1", + "from": "outline-shadowsocksconfig@github:Jigsaw-Code/outline-shadowsocksconfig#v0.2.0", + "requires": { + "ipaddr.js": "^2.0.0", + "js-base64": "^3.5.2", + "punycode": "^1.4.1" + }, + "dependencies": { + "ipaddr.js": { + "version": "2.0.1" + } + } + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true + }, + "p-event": { + "version": "2.3.1", + "dev": true, + "requires": { + "p-timeout": "^2.0.1" + } + }, + "p-finally": { + "version": "1.0.0" + }, + "p-is-promise": { + "version": "1.1.0", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map-series": { + "version": "1.0.0", + "dev": true, + "requires": { + "p-reduce": "^1.0.0" + } + }, + "p-reduce": { + "version": "1.0.0", + "dev": true + }, + "p-retry": { + "version": "4.6.2", + "dev": true, + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + } + }, + "p-timeout": { + "version": "2.0.1", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "dev": true + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "parent-module": { + "version": "1.0.1", + "dev": true, + "requires": { + "callsites": "^3.0.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "dev": true + } + } + }, + "parse-bmfont-ascii": { + "version": "1.0.6", + "dev": true + }, + "parse-bmfont-binary": { + "version": "1.0.6", + "dev": true + }, + "parse-bmfont-xml": { + "version": "1.1.4", + "dev": true, + "requires": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.4.5" + } + }, + "parse-filepath": { + "version": "1.0.2", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-headers": { + "version": "2.0.4", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse-node-version": { + "version": "1.0.1", + "dev": true + }, + "parse-passwd": { + "version": "1.0.0", + "dev": true + }, + "parseurl": { + "version": "1.3.3" + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "pascalcase": { + "version": "0.1.1", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "dev": true + }, + "path-root": { + "version": "0.1.1", + "dev": true, + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "dev": true + }, + "path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "requires": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + } + } + }, + "path-to-regexp": { + "version": "0.1.7" + }, + "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 + }, + "pend": { + "version": "1.2.0", + "dev": true + }, + "performance-now": { + "version": "2.1.0" + }, + "phantomjs-prebuilt": { + "version": "2.1.16", + "dev": true, + "requires": { + "es6-promise": "^4.0.3", + "extract-zip": "^1.6.5", + "fs-extra": "^1.0.0", + "hasha": "^2.2.0", + "kew": "^0.7.0", + "progress": "^1.1.8", + "request": "^2.81.0", + "request-progress": "^2.0.1", + "which": "^1.2.10" + }, + "dependencies": { + "es6-promise": { + "version": "4.2.8", + "dev": true + }, + "fs-extra": { + "version": "1.0.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0" + } + }, + "jsonfile": { + "version": "2.4.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "progress": { + "version": "1.1.8", + "dev": true + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "phin": { + "version": "2.9.3", + "dev": true + }, + "picocolors": { + "version": "0.2.1", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "dev": true + }, + "pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "requires": { + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "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==" + } + } + }, + "pify": { + "version": "3.0.0", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pino": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.0.tgz", + "integrity": "sha512-UUmvQ/7KTZt/vHjhRrnyS7h+J7qPBQnpG80V56xmIC+o9IqYmQOw/UIny9S9zYDfRBR0ClouCr464EkBMIT7Fw==", + "requires": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + } + }, + "pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "requires": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "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==" + }, + "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" + } + } + } + }, + "pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "pixelmatch": { + "version": "4.0.2", + "dev": true, + "requires": { + "pngjs": "^3.0.0" + } + }, + "pkg-dir": { + "version": "3.0.0", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "pkginfo": { + "version": "0.4.0", + "dev": true + }, + "please-upgrade-node": { + "version": "3.2.0", + "dev": true, + "requires": { + "semver-compare": "^1.0.0" + } + }, + "plist": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", + "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", + "dev": true, + "requires": { + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "dependencies": { + "xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true + } + } + }, + "plugin-error": { + "version": "1.0.1", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "extend-shallow": { + "version": "3.0.2", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "pn": { + "version": "1.1.0", + "dev": true + }, + "pngjs": { + "version": "3.4.0", + "dev": true + }, + "posix-character-classes": { + "version": "0.1.1", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "postcss-rtl": { + "version": "1.7.3", + "dev": true, + "requires": { + "rtlcss": "2.5.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "posthtml": { + "version": "0.11.6", + "dev": true, + "requires": { + "posthtml-parser": "^0.4.1", + "posthtml-render": "^1.1.5" + } + }, + "posthtml-load-config": { + "version": "1.0.0", + "dev": true, + "requires": { + "cosmiconfig": "^2.1.0", + "posthtml-load-options": "^1.0.0" + }, + "dependencies": { + "cosmiconfig": { + "version": "2.2.2", + "dev": true, + "requires": { + "is-directory": "^0.3.1", + "js-yaml": "^3.4.3", + "minimist": "^1.2.0", + "object-assign": "^4.1.0", + "os-homedir": "^1.0.1", + "parse-json": "^2.2.0", + "require-from-string": "^1.1.0" + } + }, + "parse-json": { + "version": "2.2.0", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + } + } + }, + "posthtml-load-options": { + "version": "1.0.0", + "dev": true, + "requires": { + "cosmiconfig": "^2.1.0" + }, + "dependencies": { + "cosmiconfig": { + "version": "2.2.2", + "dev": true, + "requires": { + "is-directory": "^0.3.1", + "js-yaml": "^3.4.3", + "minimist": "^1.2.0", + "object-assign": "^4.1.0", + "os-homedir": "^1.0.1", + "parse-json": "^2.2.0", + "require-from-string": "^1.1.0" + } + }, + "parse-json": { + "version": "2.2.0", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + } + } + }, + "posthtml-parser": { + "version": "0.4.2", + "dev": true, + "requires": { + "htmlparser2": "^3.9.2" + } + }, + "posthtml-postcss": { + "version": "0.2.6", + "dev": true, + "requires": { + "postcss": "^6.0.14" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "supports-color": { + "version": "5.5.0", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "posthtml-render": { + "version": "1.4.0", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "dev": true + }, + "prepend-http": { + "version": "2.0.0", + "dev": true + }, + "prettier": { + "version": "2.4.1", + "dev": true + }, + "pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "requires": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "pretty-hrtime": { + "version": "1.0.3", + "dev": true + }, + "pretty-quick": { + "version": "3.1.1", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "execa": "^4.0.0", + "find-up": "^4.1.0", + "ignore": "^5.1.4", + "mri": "^1.1.5", + "multimatch": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "execa": { + "version": "4.1.0", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "find-up": { + "version": "4.1.0", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "npm-run-path": { + "version": "4.0.1", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "dev": true + } + } + }, + "process": { + "version": "0.11.10" + }, + "process-nextick-args": { + "version": "2.0.1" + }, + "process-warning": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.2.0.tgz", + "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" + }, + "progress": { + "version": "2.0.3", + "dev": true + }, + "prom-client": { + "version": "11.5.3", + "requires": { + "tdigest": "^0.1.1" + } + }, + "proto-list": { + "version": "1.2.4", + "dev": true + }, + "proxy-addr": { + "version": "2.0.7", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "dev": true + }, + "psl": { + "version": "1.8.0" + }, + "pump": { + "version": "3.0.0", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "1.4.1" + }, + "puppeteer": { + "version": "13.6.0", + "dev": true, + "requires": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.981744", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.0", + "pkg-dir": "4.2.0", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.5.0" + }, + "dependencies": { + "extract-zip": { + "version": "2.0.1", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "find-up": { + "version": "4.1.0", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "q": { + "version": "1.5.1", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "dev": true + }, + "qs": { + "version": "6.10.3", + "requires": { + "side-channel": "^1.0.4" + } + }, + "query-string": { + "version": "5.1.1", + "dev": true, + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "queue-microtask": { + "version": "1.2.3", + "dev": true + }, + "quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "randombytes": { + "version": "2.0.3" + }, + "randomstring": { + "version": "1.2.1", + "requires": { + "array-uniq": "1.0.2", + "randombytes": "2.0.3" + } + }, + "range-parser": { + "version": "1.2.1" + }, + "raw-body": { + "version": "2.5.1", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "read-chunk": { + "version": "1.0.1", + "dev": true + }, + "read-config-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.2.0.tgz", + "integrity": "sha512-gx7Pgr5I56JtYz+WuqEbQHj/xWo+5Vwua2jhb1VwM4Wid5PqYmZ4i00ZB0YEGIfkVBsCv9UrjgyqCiQfS/Oosg==", + "dev": true, + "requires": { + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + } + } + }, + "read-installed": { + "version": "4.0.3", + "dev": true, + "requires": { + "debuglog": "^1.0.1", + "graceful-fs": "^4.1.2", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "dev": true + } + } + }, + "read-package-json": { + "version": "2.1.2", + "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" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "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" + } + }, + "semver": { + "version": "5.7.1", + "dev": true + } + } + }, + "read-pkg": { + "version": "4.0.1", + "dev": true, + "requires": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "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" + } + }, + "semver": { + "version": "5.7.1", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "1.0.1", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "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" + } + }, + "path-exists": { + "version": "2.1.0", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "semver": { + "version": "5.7.1", + "dev": true + } + } + }, + "readable-stream": { + "version": "2.3.7", + "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" + } + }, + "readdir-scoped-modules": { + "version": "1.1.0", + "dev": true, + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "readdirp": { + "version": "2.2.1", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + }, + "rechoir": { + "version": "0.6.2", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "redent": { + "version": "1.0.0", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "regex-not": { + "version": "1.0.2", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "dev": true + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true + }, + "remove-bom-buffer": { + "version": "3.0.0", + "dev": true, + "requires": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + } + }, + "remove-bom-stream": { + "version": "1.2.0", + "dev": true, + "requires": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "dev": true + }, + "renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "requires": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + } + } + }, + "repeat-element": { + "version": "1.1.4", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "replace-ext": { + "version": "1.0.1", + "dev": true + }, + "replace-homedir": { + "version": "1.0.0", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.0" + } + }, + "replacestream": { + "version": "4.0.3", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.3", + "object-assign": "^4.0.1", + "readable-stream": "^2.0.2" + } + }, + "request": { + "version": "2.88.2", + "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": { + "form-data": { + "version": "2.3.3", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "qs": { + "version": "6.5.3" + } + } + }, + "request-progress": { + "version": "2.0.1", + "dev": true, + "requires": { + "throttleit": "^1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "dev": true + }, + "require-from-string": { + "version": "1.2.1", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "resolve-dir": { + "version": "1.0.1", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "dev": true + }, + "resolve-options": { + "version": "1.1.0", + "dev": true, + "requires": { + "value-or-function": "^3.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "dev": true + }, + "responselike": { + "version": "1.0.2", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "restify": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/restify/-/restify-11.1.0.tgz", + "integrity": "sha512-ng7uBlj4wpIpshhAjNNSd6JG5Eg32+zgync2gG8OlF4e2xzIflZo54GJ/qLs765OtQaVU+uJPcNOL5Atm2F/dg==", + "requires": { + "assert-plus": "^1.0.0", + "csv": "^6.2.2", + "dtrace-provider": "~0.8", + "escape-regexp-component": "^1.0.2", + "ewma": "^2.0.1", + "find-my-way": "^7.2.0", + "formidable": "^1.2.1", + "http-signature": "^1.3.6", + "lodash": "^4.17.11", + "lru-cache": "^7.14.1", + "mime": "^3.0.0", + "negotiator": "^0.6.2", + "once": "^1.4.0", + "pidusage": "^3.0.2", + "pino": "^8.7.0", + "qs": "^6.7.0", + "restify-errors": "^8.0.2", + "semver": "^7.3.8", + "send": "^0.18.0", + "spdy": "^4.0.0", + "uuid": "^9.0.0", + "vasync": "^2.2.0" + }, + "dependencies": { + "http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + } + }, + "jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "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==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "restify-cors-middleware2": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/restify-cors-middleware2/-/restify-cors-middleware2-2.2.1.tgz", + "integrity": "sha512-j7Hvufd5dv699OeVuSk6YUH/HMga6a4A1zriGc6En/SDE3kzeDvQF82a3LMcVa+GaUfXxQa+TpVP8LRtcdIJWg==", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "restify-errors": { + "version": "8.0.2", + "requires": { + "@netflix/nerror": "^1.0.0", + "assert-plus": "^1.0.0", + "lodash": "^4.17.15", + "safe-json-stringify": "^1.0.4" + } + }, + "restore-cursor": { + "version": "3.1.0", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "dev": true + }, + "retry": { + "version": "0.13.1", + "dev": true + }, + "retry-request": { + "version": "4.2.2", + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + } + }, + "reusify": { + "version": "1.0.4", + "dev": true + }, + "rfdc": { + "version": "1.3.0", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "optional": true, + "requires": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true, + "optional": true + } + } + }, + "rtlcss": { + "version": "2.5.0", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "findup": "^0.1.5", + "mkdirp": "^0.5.1", + "postcss": "^6.0.23", + "strip-json-comments": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + }, + "supports-color": { + "version": "5.5.0", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "run-async": { + "version": "2.4.1", + "dev": true + }, + "run-node": { + "version": "1.0.0", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "6.6.7", + "dev": true, + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "dev": true + } + } + }, + "safe-buffer": { + "version": "5.1.2" + }, + "safe-json-stringify": { + "version": "1.2.0", + "optional": true + }, + "safe-regex": { + "version": "1.1.0", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "requires": { + "ret": "~0.2.0" + }, + "dependencies": { + "ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==" + } + } + }, + "safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==" + }, + "safer-buffer": { + "version": "2.1.2" + }, + "sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "requires": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "sax": { + "version": "1.2.4" + }, + "seek-bzip": { + "version": "1.0.6", + "dev": true, + "requires": { + "commander": "^2.8.1" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "dev": true + } + } + }, + "select-hose": { + "version": "2.0.0" + }, + "selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dev": true, + "requires": { + "node-forge": "^1" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "semver-compare": { + "version": "1.0.0", + "dev": true + }, + "semver-greatest-satisfied-range": { + "version": "1.1.0", + "dev": true, + "requires": { + "sver-compat": "^1.5.0" + } + }, + "send": { + "version": "0.18.0", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0" + } + } + }, + "depd": { + "version": "2.0.0" + }, + "destroy": { + "version": "1.2.0" + }, + "mime": { + "version": "1.6.0" + }, + "ms": { + "version": "2.1.3" + }, + "on-finished": { + "version": "2.4.1", + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "2.0.1" + } + } + }, + "sentry_webhook": { + "version": "file:src/sentry_webhook", + "requires": { + "@sentry/types": "^4.4.1", + "@types/express": "^4.17.12", + "@types/jasmine": "^5.1.0", + "https-browserify": "^1.0.0", + "jasmine": "^5.1.0", + "stream-http": "^3.2.0", + "url": "^0.11.3" + }, + "dependencies": { + "@sentry/types": { + "version": "4.5.3", + "dev": true + }, + "@types/jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.0.tgz", + "integrity": "sha512-XOV0KsqXNX2gUSqk05RWeolIMgaAQ7+l/ozOBoQ8NGwLg+E7J9vgagODtNgfim4jCzEUP0oJ3gnXeC+Zv+Xi1A==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + } + }, + "jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.1.0.tgz", + "integrity": "sha512-prmJlC1dbLhti4nE4XAPDWmfJesYO15sjGXVp7Cs7Ym5I9Xtwa/hUHxxJXjnpfLO72+ySttA0Ztf8g/RiVnUKw==", + "dev": true, + "requires": { + "glob": "^10.2.2", + "jasmine-core": "~5.1.0" + } + }, + "jasmine-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.1.tgz", + "integrity": "sha512-UrzO3fL7nnxlQXlvTynNAenL+21oUQRlzqQFsA2U11ryb4+NLOCOePZ70PTojEaUKhiFugh7dG0Q+I58xlPdWg==", + "dev": true + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + } + } + }, + "serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "optional": true, + "requires": { + "type-fest": "^0.13.1" + }, + "dependencies": { + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "optional": true + } + } + }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + }, + "dependencies": { + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + } + } + }, + "serve-index": { + "version": "1.9.1", + "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", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.6.3", + "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", + "dev": true + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "dev": true + } + } + }, + "serve-static": { + "version": "1.15.0", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "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": { + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "setprototypeof": { + "version": "1.2.0" + }, + "shallow-clone": { + "version": "3.0.1", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "dev": true + } + } + }, + "shebang-command": { + "version": "2.0.0", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "dev": true + }, + "shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.5", + "dev": true + }, + "simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "requires": { + "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 + } + } + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true + }, + "slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "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, + "optional": true + } + } + }, + "slide": { + "version": "1.1.6", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "optional": true + }, + "snakeize": { + "version": "0.1.0", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "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", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "socket.io": { + "version": "4.4.1", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + } + }, + "socket.io-adapter": { + "version": "2.3.3", + "dev": true + }, + "socket.io-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.5.tgz", + "integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==", + "dev": true, + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + } + }, + "sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } + } + }, + "sonic-boom": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", + "requires": { + "atomic-sleep": "^1.0.0" + } + }, + "sort-keys": { + "version": "1.1.2", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "sort-keys-length": { + "version": "1.0.1", + "dev": true, + "requires": { + "sort-keys": "^1.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "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.20", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.1", + "dev": true + }, + "sparkles": { + "version": "1.0.1", + "dev": true + }, + "spdx-compare": { + "version": "1.0.0", + "dev": true, + "requires": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "spdx-correct": { + "version": "3.1.1", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.10", + "dev": true + }, + "spdx-ranges": { + "version": "2.1.1", + "dev": true + }, + "spdx-satisfies": { + "version": "4.0.1", + "dev": true, + "requires": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "spdy": { + "version": "4.0.2", + "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", + "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" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "split-string": { + "version": "3.1.0", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + }, + "sprintf-js": { + "version": "1.0.3" + }, + "sshpk": { + "version": "1.16.1", + "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" + } + }, + "stack-trace": { + "version": "0.0.10", + "dev": true + }, + "stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "dev": true + }, + "stream-events": { + "version": "1.0.5", + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-exhaust": { + "version": "1.0.2", + "dev": true + }, + "stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "stream-shift": { + "version": "1.0.1" + }, + "stream-to": { + "version": "0.2.2", + "dev": true + }, + "stream-to-buffer": { + "version": "0.1.0", + "dev": true, + "requires": { + "stream-to": "~0.2.0" + } + }, + "stream-transform": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.10.tgz", + "integrity": "sha512-Yu+x7zcWbWdyB0Td8dFzHt2JEyD6694CNq2lqh1rbuEBVxPtjb/GZ7xDnZcdYiU5E/RtufM54ClSEOzZDeWguA==" + }, + "streamroller": { + "version": "3.0.5", + "dev": true, + "requires": { + "date-format": "^4.0.5", + "debug": "^4.3.3", + "fs-extra": "^10.0.1" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "dev": true + } + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string-similarity": { + "version": "1.1.0", + "dev": true, + "requires": { + "lodash": "^4.13.1" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "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-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "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.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "strip-ansi": { + "version": "6.0.1", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-dirs": { + "version": "2.1.0", + "dev": true, + "requires": { + "is-natural-number": "^4.0.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + }, + "dependencies": { + "get-stdin": { + "version": "4.0.1", + "dev": true + } + } + }, + "strip-json-comments": { + "version": "2.0.1", + "dev": true + }, + "strip-outer": { + "version": "1.0.1", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "stubs": { + "version": "3.0.0" + }, + "style-loader": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", + "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", + "dev": true, + "requires": {} + }, + "sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "requires": { + "debug": "^4.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "sver-compat": { + "version": "1.5.0", + "dev": true, + "requires": { + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "svg2png": { + "version": "4.1.0", + "dev": true, + "requires": { + "file-url": "^1.1.0", + "phantomjs-prebuilt": "^2.1.10", + "pn": "^1.0.0", + "yargs": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "dev": true + }, + "camelcase": { + "version": "3.0.0", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "get-caller-file": { + "version": "1.0.3", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "y18n": { + "version": "3.2.2", + "dev": true + }, + "yargs": { + "version": "5.0.0", + "dev": true, + "requires": { + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "lodash.assign": "^4.2.0", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "window-size": "^0.2.0", + "y18n": "^3.2.1", + "yargs-parser": "^3.2.0" + } + }, + "yargs-parser": { + "version": "3.2.0", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "lodash.assign": "^4.1.0" + } + } + } + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "tar": { + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.12.tgz", + "integrity": "sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw==", + "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": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "tar-fs": { + "version": "2.1.1", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + }, + "dependencies": { + "bl": { + "version": "4.1.0", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "tar-stream": { + "version": "2.2.0", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + } + } + }, + "tar-stream": { + "version": "1.6.2", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, + "tdigest": { + "version": "0.1.1", + "requires": { + "bintrees": "1.0.1" + } + }, + "teeny-request": { + "version": "7.2.0", + "requires": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + }, + "dependencies": { + "uuid": { + "version": "8.3.2" + } + } + }, + "temp-dir": { + "version": "2.0.0", + "dev": true + }, + "temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "requires": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "tempfile": { + "version": "3.0.0", + "dev": true, + "requires": { + "temp-dir": "^2.0.0", + "uuid": "^3.3.2" + } + }, + "terser": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.21.0.tgz", + "integrity": "sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "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 + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "dependencies": { + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "dev": true + }, + "thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "requires": { + "real-require": "^0.2.0" + } + }, + "throttleit": { + "version": "1.0.0", + "dev": true + }, + "through": { + "version": "2.3.8", + "dev": true + }, + "through2": { + "version": "3.0.2", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "through2-filter": { + "version": "3.0.0", + "dev": true, + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "thunky": { + "version": "1.1.0", + "dev": true + }, + "time-stamp": { + "version": "1.1.0", + "dev": true + }, + "timed-out": { + "version": "4.0.1", + "dev": true + }, + "tinycolor2": { + "version": "1.4.2", + "dev": true + }, + "tmp": { + "version": "0.2.1", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "requires": { + "tmp": "^0.2.0" + } + }, + "to-absolute-glob": { + "version": "2.0.2", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "to-buffer": { + "version": "1.1.1", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true + }, + "to-regex": { + "version": "3.0.2", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "to-regex-range": { + "version": "2.1.1", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "to-through": { + "version": "2.0.0", + "dev": true, + "requires": { + "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "toidentifier": { + "version": "1.0.1" + }, + "tough-cookie": { + "version": "2.5.0", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1" + } + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "treeify": { + "version": "1.1.0", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "dev": true + }, + "trim-repeated": { + "version": "1.0.0", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "requires": { + "utf8-byte-length": "^1.0.1" + } + }, + "ts-loader": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.0.tgz", + "integrity": "sha512-LLlB/pkB4q9mW2yLdFMnK3dEHbrBjeZTYguaaIfusyojBgAGf5kF+O6KcWqiGzWqHk0LBsoolrp4VftEURhybg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "dev": true + }, + "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" + } + }, + "micromatch": { + "version": "4.0.5", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + } + } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "tsutils": { + "version": "3.21.0", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "dev": true + } + } + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "optional": true + }, + "tunnel-agent": { + "version": "0.6.0", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5" + }, + "type": { + "version": "1.2.0", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.21.3", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "4.4.4", + "dev": true + }, + "ua-parser-js": { + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", + "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unbzip2-stream": { + "version": "1.4.3", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "unc-path-regex": { + "version": "0.1.2", + "dev": true + }, + "undertaker": { + "version": "1.3.0", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "fast-levenshtein": "^1.0.0", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + } + }, + "undertaker-registry": { + "version": "1.0.1", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unique-stream": { + "version": "2.3.1", + "dev": true, + "requires": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "unique-string": { + "version": "2.0.0", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "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" + }, + "unset-value": { + "version": "1.0.0", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "dependencies": { + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + } + } + }, + "uri-js": { + "version": "4.4.1", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1" + } + } + }, + "urix": { + "version": "0.1.0", + "dev": true + }, + "url": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "dev": true, + "requires": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + }, + "dependencies": { + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + } + } + }, + "url-parse-lax": { + "version": "3.0.0", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } + }, + "url-regex": { + "version": "3.2.0", + "dev": true, + "requires": { + "ip-regex": "^1.0.1" + }, + "dependencies": { + "ip-regex": { + "version": "1.0.3", + "dev": true + } + } + }, + "url-to-options": { + "version": "1.0.1", + "dev": true + }, + "use": { + "version": "3.1.1", + "dev": true + }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2" + }, + "util-extend": { + "version": "1.0.3", + "dev": true + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "utils-merge": { + "version": "1.0.1" + }, + "uuid": { + "version": "3.4.0" + }, + "v8-compile-cache": { + "version": "2.3.0", + "dev": true + }, + "v8flags": { + "version": "3.2.0", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "value-or-function": { + "version": "3.0.0", + "dev": true + }, + "vary": { + "version": "1.1.2" + }, + "vasync": { + "version": "2.2.0", + "requires": { + "verror": "1.10.0" + } + }, + "verror": { + "version": "1.10.0", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vinyl": { + "version": "2.2.1", + "dev": true, + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + } + }, + "vinyl-fs": { + "version": "3.0.3", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "vinyl-sourcemap": { + "version": "1.1.0", + "dev": true, + "requires": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "void-elements": { + "version": "2.0.1", + "dev": true + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "web-animations-js": { + "version": "2.3.2" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "webpack": { + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true + }, + "interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true + }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "requires": { + "resolve": "^1.20.0" + } + } + } + }, + "webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "requires": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "anymatch": { + "version": "3.1.2", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "dev": true + }, + "braces": { + "version": "3.0.2", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.5.3", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "fill-range": { + "version": "7.0.1", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.2", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "ipaddr.js": { + "version": "2.0.1", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-from-string": { + "version": "2.0.2", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "schema-utils": { + "version": "4.0.0", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "webpack-dev-middleware": { + "version": "5.3.1", + "dev": true, + "requires": { + "colorette": "^2.0.10", + "memfs": "^3.4.1", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + } + }, + "ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "dev": true, + "requires": {} + } + } + }, + "webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "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-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-module": { + "version": "1.0.0", + "dev": true + }, + "wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "window-size": { + "version": "0.2.0", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@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" + } + }, + "wrappy": { + "version": "1.0.2" + }, + "write-file-atomic": { + "version": "3.0.3", + "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": "8.5.0", + "dev": true, + "requires": {} + }, + "xdg-basedir": { + "version": "4.0.0", + "dev": true + }, + "xhr": { + "version": "2.6.0", + "dev": true, + "requires": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "xml-parse-from-string": { + "version": "1.0.1", + "dev": true + }, + "xml2js": { + "version": "0.4.23", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "dependencies": { + "y18n": { + "version": "5.0.8", + "dev": true + } + } + }, + "yargs-parser": { + "version": "20.2.9", + "dev": true + }, + "yauzl": { + "version": "2.10.0", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..46071f71b --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "outline-server", + "private": true, + "dependencies": { + "node-fetch": "^2.6.7" + }, + "devDependencies": { + "@commitlint/config-conventional": "^17.0.0", + "@types/jasmine": "^3.5.10", + "@types/node-fetch": "^2.6.2", + "@typescript-eslint/eslint-plugin": "^5.14.0", + "@typescript-eslint/parser": "^5.14.0", + "@webpack-cli/serve": "^2.0.5", + "browserslist": "^4.20.3", + "eslint": "^8.10.0", + "eslint-import-resolver-typescript": "^2.7.1", + "eslint-plugin-compat": "^4.0.2", + "eslint-plugin-import": "^2.26.0", + "generate-license-file": "^1.2.0", + "husky": "^1.3.1", + "jasmine": "^3.5.0", + "prettier": "^2.4.1", + "pretty-quick": "^3.1.1", + "typescript": "^4" + }, + "engines": { + "node": "18.x.x" + }, + "scripts": { + "action": "bash ./scripts/run_action.sh", + "action:help": "npm run action", + "action:list": "npm run action", + "clean": "rm -rf src/*/node_modules/ build/ node_modules/ src/server_manager/install_scripts/do_install_script.ts src/server_manager/install_scripts/gcp_install_script.ts third_party/shellcheck/download/ third_party/*/bin third_party/jsign/*.jar", + "format": "pretty-quick --staged --pattern \"**/*.{cjs,html,js,json,md,ts}\"", + "format:all": "prettier --write \"**/*.{cjs,html,js,json,md,ts}\"", + "lint": "npm run lint:sh && npm run lint:ts", + "lint:sh": "bash ./scripts/shellcheck.sh", + "lint:ts": "eslint \"**/*.{js,ts}\"", + "test": "npm run lint && npm run action metrics_server/test && npm run action sentry_webhook/build && npm run action server_manager/test && npm run action shadowbox/test" + }, + "workspaces": [ + "src/*" + ], + "husky": { + "hooks": { + "pre-commit": "npm run lint && npm run format" + } + } +} diff --git a/scripts/run_action.sh b/scripts/run_action.sh new file mode 100755 index 000000000..f8861b284 --- /dev/null +++ b/scripts/run_action.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# +# Copyright 2018 The Outline Authors +# +# 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. + +set -eu + +# TODO: Because Node.js on Cygwin doesn't handle absolute paths very +# well, it would be worth pushd-ing to ROOT_DIR before invoking +# them and making BUILD_DIR a relative path, viz. just "build". + +readonly ROOT_DIR=${ROOT_DIR:-$(pwd)/$(git rev-parse --show-cdup)} +readonly BUILD_DIR=${BUILD_DIR:-${ROOT_DIR}/build} + +export ROOT_DIR +export BUILD_DIR + +export run_action_indent='' + +function run_action() { + local -r STYLE_BOLD_WHITE='\033[1;37m' + local -r STYLE_BOLD_GREEN='\033[1;32m' + local -r STYLE_BOLD_RED='\033[1;31m' + local -r STYLE_RESET='\033[0m' + + local -r action="${1:-""}" + local -r old_indent="${run_action_indent}" + + run_action_indent="=> ${run_action_indent}" + + if [[ -z "${action}" ]]; then + echo -e "Please provide an action to run. ${STYLE_BOLD_WHITE}List of valid actions:${STYLE_RESET}\n" + find . -name '*.action.sh' | sed -E 's:\./src/(.*)\.action\.sh:\1:' + exit 0 + fi + + echo -e "${old_indent}${STYLE_BOLD_WHITE}[Running ${action}]${STYLE_RESET}" + shift + + "${ROOT_DIR}src/${action}.action.sh" "$@" + + local -ir status="$?" + if (( status == 0 )); then + echo -e "${old_indent}${STYLE_BOLD_GREEN}[${action}: Finished]${STYLE_RESET}" + else + echo -e "${old_indent}${STYLE_BOLD_RED}[${action}: Failed]${STYLE_RESET}" + fi + + run_action_indent="${old_indent}" + + return "${status}" +} + +export -f run_action + +run_action "$@" diff --git a/scripts/shellcheck.sh b/scripts/shellcheck.sh new file mode 100644 index 000000000..cfc414f27 --- /dev/null +++ b/scripts/shellcheck.sh @@ -0,0 +1,24 @@ +#!/bin/bash -eu +# +# Copyright 2021 The Outline Authors +# +# 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. + +# This script intended to run at the repository root. + +readonly WRAPPER='third_party/shellcheck/run.sh' +declare -ar START=(src scripts "${WRAPPER}") + +# From the specified starting points, +# run shellcheck over all files ending in .sh. +find "${START[@]}" -name '*.sh' -exec "${WRAPPER}" {} + diff --git a/src/build/download_file.mjs b/src/build/download_file.mjs new file mode 100644 index 000000000..591292c33 --- /dev/null +++ b/src/build/download_file.mjs @@ -0,0 +1,58 @@ +// Copyright 2023 The Outline Authors +// +// 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. + +import {createWriteStream} from 'node:fs'; +import {mkdir} from 'node:fs/promises'; +import {pipeline} from 'node:stream/promises'; +import * as path from 'path' +import url from 'url'; + +import fetch from 'node-fetch'; +import minimist from 'minimist'; + +import {getFileChecksum} from './get_file_checksum.mjs' + +/** + * Download a remote file from `fileUrl` and save it to `filepath`, using HTTPS protocol. + * This function will also follow HTTP redirects. + * @param {string} fileUrl The full URL of the remote resource. + * @param {string} filepath The full path of the target file. + * @param {string} sha256Checksum The SHA256 checksum of the file to use for verification. + * @returns {Promise} A task that will be completed once the download is completed. + */ +export async function downloadFile(fileUrl, filepath, sha256Checksum) { + await mkdir(path.dirname(filepath), { recursive: true }); + + const response = await fetch(fileUrl); + if (!response.ok) { + throw new Error(`failed to download "${fileUrl}": ${response.status} ${response.statusText}`); + } + const target = createWriteStream(filepath); + await pipeline(response.body, target); + + const actualChecksum = await getFileChecksum(filepath, 'sha256'); + if (actualChecksum !== sha256Checksum) { + throw new Error(`failed to verify "${filepath}". ` + + `Expected checksum ${sha256Checksum}, but found ${actualChecksum}`); + } +} + +async function main(...args) { + const {url, sha256, out} = minimist(args); + await downloadFile(url, out, sha256) +} + +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + await main(...process.argv.slice(2)); +} diff --git a/src/build/get_file_checksum.mjs b/src/build/get_file_checksum.mjs new file mode 100644 index 000000000..bb42a7ac1 --- /dev/null +++ b/src/build/get_file_checksum.mjs @@ -0,0 +1,39 @@ +// Copyright 2023 The Outline Authors +// +// 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. + +import {createHash} from 'node:crypto'; +import {readFile} from 'node:fs/promises'; + +/** + * Read and calculate the checksum of file located in `filepath` using the + * specific hashing `algorithm`. + * @param {string} filepath The full path of the file to be read. + * @param {'sha256'|'sha512'} algorithm The hashing algorithm supported by node. + * @returns {Promise} The checksum represented in hex string with lower + * case letters (e.g. `123acf`); or `null` if any + * errors are thrown (such as file not readable). + */ +export async function getFileChecksum(filepath, algorithm) { + if (!filepath || !algorithm) { + throw new Error('filepath and algorithm are required'); + } + try { + const buffer = await readFile(filepath); + const hasher = createHash(algorithm); + hasher.update(buffer); + return hasher.digest('hex'); + } catch { + return null; + } +} diff --git a/src/build/get_root_dir.mjs b/src/build/get_root_dir.mjs new file mode 100644 index 000000000..2be364ff2 --- /dev/null +++ b/src/build/get_root_dir.mjs @@ -0,0 +1,23 @@ +// Copyright 2023 The Outline Authors +// +// 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. + +import {dirname, resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; + +// WARNING: if you move this file, you MUST update this file path +const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..'); + +export function getRootDir() { + return ROOT_DIR; +} diff --git a/src/metrics_server/README.md b/src/metrics_server/README.md new file mode 100644 index 000000000..c4a0298f8 --- /dev/null +++ b/src/metrics_server/README.md @@ -0,0 +1,86 @@ +# Outline Metrics Server + +The Outline Metrics Server is a [Google App Engine](https://cloud.google.com/appengine) project that writes feature and connections metrics to BigQuery, as reported by opted-in Outline servers. + +## API + +### Endpoints + +The metrics server deploys two services: `dev`, used for development testing and debugging; and `prod`, used for production metrics. The `dev` environment is deployed to `https://dev.metrics.getoutline.org`; the `prod` environment is deployed to `https://prod.metrics.getoutline.org`. Each environment posts metrics to its own BigQuery dataset (see `config_[dev|prod].json`). + +### URLs + +The metrics server supports two URL paths: + +- `POST /connections`: report server data usage broken down by user. + + ``` + { + serverId: string, + startUtcMs: number, + endUtcMs: number, + userReports: [{ + userId: string, + countries: string[], + bytesTransferred: number, + }] + } + ``` + +- `POST /features`: report feature usage. + + ``` + { + serverId: string, + serverVersion: string, + timestampUtcMs: number, + dataLimit: { + enabled: boolean + perKeyLimitCount: number + } + } + ``` + +## Requirements + +- [Google Cloud SDK](https://cloud.google.com/sdk/) + +## Build + +```sh +npm run action metrics_server/build +``` + +## Run + +Run a local development metrics server: + +```sh +npm run action metrics_server/start +``` + +## Deploy + +- Authenticate with `gcloud`: + ```sh + gcloud auth login + ``` +- To deploy to dev: + ```sh + npm run action metrics_server/deploy_dev + ``` +- To deploy to prod: + ```sh + npm run action metrics_server/deploy_prod + ``` + +## Test + +- Unit test + ```sh + npm run action metrics_server/test + ``` +- Integration test + ```sh + npm run action metrics_server/test_integration + ``` diff --git a/src/metrics_server/app_dev.yaml b/src/metrics_server/app_dev.yaml new file mode 100644 index 000000000..01d663bad --- /dev/null +++ b/src/metrics_server/app_dev.yaml @@ -0,0 +1,7 @@ +runtime: nodejs16 +service: dev +handlers: +- url: /.* + script: auto + secure: always + redirect_http_response_code: 307 diff --git a/src/metrics_server/app_prod.yaml b/src/metrics_server/app_prod.yaml new file mode 100644 index 000000000..360be28be --- /dev/null +++ b/src/metrics_server/app_prod.yaml @@ -0,0 +1,7 @@ +runtime: nodejs16 +service: prod +handlers: +- url: /.* + script: auto + secure: always + redirect_http_response_code: 307 diff --git a/src/metrics_server/build.action.sh b/src/metrics_server/build.action.sh new file mode 100755 index 000000000..5ed513fc2 --- /dev/null +++ b/src/metrics_server/build.action.sh @@ -0,0 +1,17 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +tsc -p "$(dirname "$0")" diff --git a/src/metrics_server/config_dev.json b/src/metrics_server/config_dev.json new file mode 100644 index 000000000..59013283d --- /dev/null +++ b/src/metrics_server/config_dev.json @@ -0,0 +1,5 @@ +{ + "datasetName": "uproxy_metrics_dev", + "connectionMetricsTableName": "connections_v1", + "featureMetricsTableName": "feature_metrics" +} diff --git a/src/metrics_server/config_prod.json b/src/metrics_server/config_prod.json new file mode 100644 index 000000000..dd7801799 --- /dev/null +++ b/src/metrics_server/config_prod.json @@ -0,0 +1,5 @@ +{ + "datasetName": "uproxy_metrics", + "connectionMetricsTableName": "connections_v1", + "featureMetricsTableName": "feature_metrics" +} diff --git a/src/metrics_server/connection_metrics.spec.ts b/src/metrics_server/connection_metrics.spec.ts new file mode 100644 index 000000000..b5e559a58 --- /dev/null +++ b/src/metrics_server/connection_metrics.spec.ts @@ -0,0 +1,277 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import { + ConnectionRow, + isValidConnectionMetricsReport, + postConnectionMetrics, +} from './connection_metrics'; +import {InsertableTable} from './infrastructure/table'; + +class FakeConnectionsTable implements InsertableTable { + public rows: ConnectionRow[] | undefined; + + async insert(rows: ConnectionRow[]) { + this.rows = rows; + } +} + +describe('postConnectionMetrics', () => { + it('correctly inserts feature metrics rows', async () => { + const table = new FakeConnectionsTable(); + const userReports = [ + { + userId: 'uid0', + countries: ['US', 'UK'], + bytesTransferred: 123, + }, + { + userId: 'uid1', + countries: ['EC'], + bytesTransferred: 456, + }, + { + userId: '', + countries: ['BR'], + bytesTransferred: 789, + }, + { + userId: 'uid1', + countries: [], + bytesTransferred: 555, + }, + ]; + const report = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports}; + await postConnectionMetrics(table, report); + const rows: ConnectionRow[] = [ + { + serverId: report.serverId, + startTimestamp: new Date(report.startUtcMs).toISOString(), + endTimestamp: new Date(report.endUtcMs).toISOString(), + userId: userReports[0].userId, + bytesTransferred: userReports[0].bytesTransferred, + countries: userReports[0].countries, + }, + { + serverId: report.serverId, + startTimestamp: new Date(report.startUtcMs).toISOString(), + endTimestamp: new Date(report.endUtcMs).toISOString(), + userId: userReports[1].userId, + bytesTransferred: userReports[1].bytesTransferred, + countries: userReports[1].countries, + }, + { + serverId: report.serverId, + startTimestamp: new Date(report.startUtcMs).toISOString(), + endTimestamp: new Date(report.endUtcMs).toISOString(), + userId: undefined, + bytesTransferred: userReports[2].bytesTransferred, + countries: userReports[2].countries, + }, + { + serverId: report.serverId, + startTimestamp: new Date(report.startUtcMs).toISOString(), + endTimestamp: new Date(report.endUtcMs).toISOString(), + userId: userReports[3].userId, + bytesTransferred: userReports[3].bytesTransferred, + countries: userReports[3].countries, + }, + ]; + expect(table.rows).toEqual(rows); + }); +}); + +describe('isValidConnectionMetricsReport', () => { + it('returns true for valid report', () => { + const userReports = [ + {userId: 'uid0', countries: ['AA'], bytesTransferred: 111}, + {userId: 'uid1', bytesTransferred: 222}, + {userId: 'uid2', countries: [], bytesTransferred: 333}, + {countries: ['BB'], bytesTransferred: 444}, + {userId: '', countries: ['CC'], bytesTransferred: 555}, + ]; + const report = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports}; + expect(isValidConnectionMetricsReport(report)).toBeTruthy(); + }); + it('returns false for missing report', () => { + expect(isValidConnectionMetricsReport(undefined)).toBeFalsy(); + }); + it('returns false for inconsistent timestamp values', () => { + const userReports = [ + {userId: 'uid0', countries: ['US', 'UK'], bytesTransferred: 123}, + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, + ]; + const invalidReport = { + serverId: 'id', + startUtcMs: 999, // startUtcMs > endUtcMs + endUtcMs: 1, + userReports, + }; + expect(isValidConnectionMetricsReport(invalidReport)).toBeFalsy(); + }); + it('returns false for out-of-bounds transferred bytes', () => { + const userReports = [ + { + userId: 'uid0', + countries: ['US', 'UK'], + bytesTransferred: -123, // Should not be negative + }, + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, + ]; + const invalidReport = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports}; + expect(isValidConnectionMetricsReport(invalidReport)).toBeFalsy(); + + const userReports2 = [ + {userId: 'uid0', countries: ['US', 'UK'], bytesTransferred: 123}, + { + userId: 'uid1', + countries: ['EC'], + bytesTransferred: 2 * Math.pow(2, 40), // 2TB is above the server capacity + }, + ]; + const invalidReport2 = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports: userReports2}; + expect(isValidConnectionMetricsReport(invalidReport2)).toBeFalsy(); + }); + it('returns false for missing report fields', () => { + const invalidReport = { + // Missing `userReports` + serverId: 'id', + startUtcMs: 1, + endUtcMs: 2, + }; + expect(isValidConnectionMetricsReport(invalidReport)).toBeFalsy(); + + const invalidReport2 = { + serverId: 'id', + startUtcMs: 1, + endUtcMs: 2, + userReports: [], // Should not be empty + }; + expect(isValidConnectionMetricsReport(invalidReport2)).toBeFalsy(); + + const userReports = [ + {userId: 'uid0', countries: ['US', 'UK'], bytesTransferred: 123}, + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, + ]; + const invalidReport3 = { + // Missing `serverId` + startUtcMs: 1, + endUtcMs: 2, + userReports, + }; + expect(isValidConnectionMetricsReport(invalidReport3)).toBeFalsy(); + + const invalidReport4 = { + // Missing `startUtcMs` + serverId: 'id', + endUtcMs: 2, + userReports, + }; + expect(isValidConnectionMetricsReport(invalidReport4)).toBeFalsy(); + + const invalidReport5 = { + // Missing `endUtcMs` + serverId: 'id', + startUtcMs: 2, + userReports, + }; + expect(isValidConnectionMetricsReport(invalidReport5)).toBeFalsy(); + }); + it('returns false for missing user report fields', () => { + const invalidReport1 = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports: [ + { + // Missing `userId` and `countries` + bytesTransferred: 123, + }, + ]}; + expect(isValidConnectionMetricsReport(invalidReport1)).toBeFalsy(); + + const invalidReport2 = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports: { + // Missing `bytesTransferred` + userId: 'uid0', + countries: ['US', 'UK'], + }}; + expect(isValidConnectionMetricsReport(invalidReport2)).toBeFalsy(); + }); + it('returns false for incorrect report field types', () => { + const invalidReport = { + serverId: 'id', + startUtcMs: 1, + endUtcMs: 2, + userReports: [1, 2, 3], // Should be `HourlyUserConnectionMetricsReport[]` + }; + expect(isValidConnectionMetricsReport(invalidReport)).toBeFalsy(); + + const userReports = [ + {userId: 'uid0', countries: ['US', 'UK'], bytesTransferred: 123}, + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, + ]; + const invalidReport2 = { + serverId: 987, // Should be a string + startUtcMs: 1, + endUtcMs: 2, + userReports, + }; + expect(isValidConnectionMetricsReport(invalidReport2)).toBeFalsy(); + + const invalidReport3 = { + serverId: 'id', + startUtcMs: '100', // Should be a number + endUtcMs: 200, + userReports, + }; + expect(isValidConnectionMetricsReport(invalidReport3)).toBeFalsy(); + + const invalidReport4 = { + // Missing `startUtcMs` + serverId: 'id', + startUtcMs: 1, + endUtcMs: '200', // Should be a number + userReports, + }; + expect(isValidConnectionMetricsReport(invalidReport4)).toBeFalsy(); + }); + it('returns false for incorrect user report field types ', () => { + const userReports = [ + { + userId: 1234, // Should be a string + countries: ['US', 'UK'], + bytesTransferred: 123, + }, + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, + ]; + const invalidReport = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports}; + expect(isValidConnectionMetricsReport(invalidReport)).toBeFalsy(); + + const userReports2 = [ + { + userId: 'uid0', + countries: [1, 2, 3], // Should be string[] + bytesTransferred: 123, + }, + ]; + const invalidReport2 = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports: userReports2}; + expect(isValidConnectionMetricsReport(invalidReport2)).toBeFalsy(); + + const userReports3 = [ + { + userId: 'uid0', + countries: ['US', 'UK'], + bytesTransferred: '1234', // Should be a number + }, + ]; + const invalidReport3 = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports: userReports3}; + expect(isValidConnectionMetricsReport(invalidReport3)).toBeFalsy(); + }); +}); diff --git a/src/metrics_server/connection_metrics.ts b/src/metrics_server/connection_metrics.ts new file mode 100644 index 000000000..01dc81352 --- /dev/null +++ b/src/metrics_server/connection_metrics.ts @@ -0,0 +1,129 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import {Table} from '@google-cloud/bigquery'; +import {InsertableTable} from './infrastructure/table'; +import {HourlyConnectionMetricsReport} from './model'; + +export interface ConnectionRow { + serverId: string; + startTimestamp: string; // ISO formatted string. + endTimestamp: string; // ISO formatted string. + userId?: string; + bytesTransferred: number; + countries?: string[]; +} + +export class BigQueryConnectionsTable implements InsertableTable { + constructor(private bigqueryTable: Table) {} + + async insert(rows: ConnectionRow[]): Promise { + await this.bigqueryTable.insert(rows); + } +} + +export function postConnectionMetrics( + table: InsertableTable, + report: HourlyConnectionMetricsReport +): Promise { + return table.insert(getConnectionRowsFromReport(report)); +} + +function getConnectionRowsFromReport(report: HourlyConnectionMetricsReport): ConnectionRow[] { + const startTimestampStr = new Date(report.startUtcMs).toISOString(); + const endTimestampStr = new Date(report.endUtcMs).toISOString(); + const rows = []; + for (const userReport of report.userReports) { + rows.push({ + serverId: report.serverId, + startTimestamp: startTimestampStr, + endTimestamp: endTimestampStr, + userId: userReport.userId || undefined, + bytesTransferred: userReport.bytesTransferred, + countries: userReport.countries || [], + }); + } + return rows; +} + +// Returns true iff testObject contains a valid HourlyConnectionMetricsReport. +export function isValidConnectionMetricsReport( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + testObject: any +): testObject is HourlyConnectionMetricsReport { + if (!testObject) { + return false; + } + + // Check that all required fields are present. + const requiredConnectionMetricsFields = ['serverId', 'startUtcMs', 'endUtcMs', 'userReports']; + for (const fieldName of requiredConnectionMetricsFields) { + if (!testObject[fieldName]) { + return false; + } + } + + // Check that `serverId` is a string. + if (typeof testObject.serverId !== 'string') { + return false; + } + + // Check timestamp types and that startUtcMs is not after endUtcMs. + if ( + typeof testObject.startUtcMs !== 'number' || + typeof testObject.endUtcMs !== 'number' || + testObject.startUtcMs >= testObject.endUtcMs + ) { + return false; + } + + // Check that userReports is an array of 1 or more item. + if (!(testObject.userReports.length >= 1)) { + return false; + } + + const MIN_BYTES_TRANSFERRED = 0; + const MAX_BYTES_TRANSFERRED = 1 * Math.pow(2, 40); // 1 TB. + for (const userReport of testObject.userReports) { + // We require at least the userId or the country to be set. + if (!userReport.userId && (userReport.countries?.length ?? 0) === 0) { + return false; + } + // Check that `userId` is a string. + if (userReport.userId && typeof userReport.userId !== 'string') { + return false; + } + + // Check that `bytesTransferred` is a number between min and max transfer limits + if ( + typeof userReport.bytesTransferred !== 'number' || + userReport.bytesTransferred < MIN_BYTES_TRANSFERRED || + userReport.bytesTransferred > MAX_BYTES_TRANSFERRED + ) { + return false; + } + + // Check that `countries` are strings. + if (userReport.countries) { + for (const country of userReport.countries) { + if (typeof country !== 'string') { + return false; + } + } + } + } + + // Request is a valid HourlyConnectionMetricsReport. + return true; +} diff --git a/src/metrics_server/deploy_dev.action.sh b/src/metrics_server/deploy_dev.action.sh new file mode 100755 index 000000000..1abc72432 --- /dev/null +++ b/src/metrics_server/deploy_dev.action.sh @@ -0,0 +1,29 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +readonly SRC_DIR="src/metrics_server" +readonly BUILD_DIR="build/metrics_server" + +rm -rf "${BUILD_DIR}" + +npm run action metrics_server/build + +cp "${SRC_DIR}/app_dev.yaml" "${BUILD_DIR}/app.yaml" +cp "${SRC_DIR}/config_dev.json" "${BUILD_DIR}/config.json" +cp "${SRC_DIR}/package.json" "${BUILD_DIR}/" +cp "./package-lock.json" "${BUILD_DIR}/" + +gcloud app deploy "${SRC_DIR}/dispatch.yaml" "${BUILD_DIR}" --project uproxysite --verbosity info --promote --stop-previous-version diff --git a/src/metrics_server/deploy_prod.action.sh b/src/metrics_server/deploy_prod.action.sh new file mode 100755 index 000000000..880cade04 --- /dev/null +++ b/src/metrics_server/deploy_prod.action.sh @@ -0,0 +1,29 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +readonly SRC_DIR="src/metrics_server" +readonly BUILD_DIR="build/metrics_server" + +rm -rf "${BUILD_DIR}" + +npm run action metrics_server/build + +cp "${SRC_DIR}/app_prod.yaml" "${BUILD_DIR}/app.yaml" +cp "${SRC_DIR}/config_prod.json" "${BUILD_DIR}/config.json" +cp "${SRC_DIR}/package.json" "${BUILD_DIR}/" +cp "./package-lock.json" "${BUILD_DIR}/" + +gcloud app deploy "${SRC_DIR}/dispatch.yaml" "${BUILD_DIR}" --project uproxysite --verbosity info --no-promote --no-stop-previous-version diff --git a/src/metrics_server/dispatch.yaml b/src/metrics_server/dispatch.yaml new file mode 100644 index 000000000..8520b49e9 --- /dev/null +++ b/src/metrics_server/dispatch.yaml @@ -0,0 +1,5 @@ +dispatch: + - url: "prod.metrics.getoutline.org/*" + service: prod + - url: "dev.metrics.getoutline.org/*" + service: dev diff --git a/src/metrics_server/feature_metrics.spec.ts b/src/metrics_server/feature_metrics.spec.ts new file mode 100644 index 000000000..d7cab0226 --- /dev/null +++ b/src/metrics_server/feature_metrics.spec.ts @@ -0,0 +1,165 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import {FeatureRow, isValidFeatureMetricsReport, postFeatureMetrics} from './feature_metrics'; +import {InsertableTable} from './infrastructure/table'; +import {DailyFeatureMetricsReport} from './model'; + +class FakeFeaturesTable implements InsertableTable { + public rows: FeatureRow[] | undefined; + + async insert(rows: FeatureRow[]) { + this.rows = rows; + } +} + +describe('postFeatureMetrics', () => { + it('correctly inserts feature metrics rows', async () => { + const table = new FakeFeaturesTable(); + const report: DailyFeatureMetricsReport = { + serverId: 'id', + serverVersion: '0.0.0', + timestampUtcMs: 123456, + dataLimit: {enabled: false}, + }; + await postFeatureMetrics(table, report); + const rows: FeatureRow[] = [ + { + serverId: report.serverId, + serverVersion: report.serverVersion, + timestamp: new Date(report.timestampUtcMs).toISOString(), + dataLimit: report.dataLimit, + }, + ]; + expect(table.rows).toEqual(rows); + }); +}); + +describe('isValidFeatureMetricsReport', () => { + it('returns true for valid report', () => { + const report = { + serverId: 'id', + serverVersion: '0.0.0', + timestampUtcMs: 123456, + dataLimit: {enabled: true}, + }; + expect(isValidFeatureMetricsReport(report)).toBeTruthy(); + }); + it('returns true for valid report with per-key data limit count', () => { + const report = { + serverId: 'id', + serverVersion: '0.0.0', + timestampUtcMs: 123456, + dataLimit: {enabled: true, perKeyLimitCount: 1}, + }; + expect(isValidFeatureMetricsReport(report)).toBeTruthy(); + }); + it('returns false for report with negative per-key data limit count', () => { + const report = { + serverId: 'id', + serverVersion: '0.0.0', + timestampUtcMs: 123456, + dataLimit: {enabled: true, perKeyLimitCount: -1}, + }; + expect(isValidFeatureMetricsReport(report)).toBeFalsy(); + }); + it('returns false for missing report', () => { + expect(isValidFeatureMetricsReport(undefined)).toBeFalsy(); + }); + it('returns false for incorrect report field types', () => { + const invalidReport = { + serverId: 1234, // Should be a string + serverVersion: '0.0.0', + timestampUtcMs: 123456, + dataLimit: {enabled: true}, + }; + expect(isValidFeatureMetricsReport(invalidReport)).toBeFalsy(); + + const invalidReport2 = { + serverId: 'id', + serverVersion: 1010, // Should be a string + timestampUtcMs: 123456, + dataLimit: {enabled: true}, + }; + expect(isValidFeatureMetricsReport(invalidReport2)).toBeFalsy(); + + const invalidReport3 = { + serverId: 'id', + serverVersion: '0.0.0', + timestampUtcMs: '123', // Should be a number + dataLimit: {enabled: true}, + }; + expect(isValidFeatureMetricsReport(invalidReport3)).toBeFalsy(); + + const invalidReport4 = { + serverId: 'id', + serverVersion: '0.0.0', + timestampUtcMs: 123456, + dataLimit: 'enabled', // Should be `DailyDataLimitMetricsReport` + }; + expect(isValidFeatureMetricsReport(invalidReport4)).toBeFalsy(); + + const invalidReport5 = { + serverId: 'id', + serverVersion: '0.0.0', + timestampUtcMs: 123456, + dataLimit: { + enabled: 'true', // Should be a boolean + }, + }; + expect(isValidFeatureMetricsReport(invalidReport5)).toBeFalsy(); + }); + it('returns false for missing report fields', () => { + const invalidReport = { + // Missing `serverId` + serverVersion: '0.0.0', + timestampUtcMs: 123456, + dataLimit: {enabled: true}, + }; + expect(isValidFeatureMetricsReport(invalidReport)).toBeFalsy(); + + const invalidReport2 = { + // Missing `serverVersion` + serverId: 'id', + timestampUtcMs: 123456, + dataLimit: {enabled: true}, + }; + expect(isValidFeatureMetricsReport(invalidReport2)).toBeFalsy(); + + const invalidReport3 = { + // Missing `timestampUtcMs` + serverId: 'id', + serverVersion: '0.0.0', + dataLimit: {enabled: true}, + }; + expect(isValidFeatureMetricsReport(invalidReport3)).toBeFalsy(); + + const invalidReport4 = { + // Missing `dataLimit` + serverId: 'id', + serverVersion: '0.0.0', + timestampUtcMs: 123456, + }; + expect(isValidFeatureMetricsReport(invalidReport4)).toBeFalsy(); + + const invalidReport5 = { + // Missing `dataLimit.enabled` + serverId: 'id', + serverVersion: '0.0.0', + timestampUtcMs: 123456, + dataLimit: {}, + }; + expect(isValidFeatureMetricsReport(invalidReport5)).toBeFalsy(); + }); +}); diff --git a/src/metrics_server/feature_metrics.ts b/src/metrics_server/feature_metrics.ts new file mode 100644 index 000000000..293d630c9 --- /dev/null +++ b/src/metrics_server/feature_metrics.ts @@ -0,0 +1,92 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import {Table} from '@google-cloud/bigquery'; + +import {InsertableTable} from './infrastructure/table'; +import {DailyDataLimitMetricsReport, DailyFeatureMetricsReport} from './model'; + +// Reflects the feature metrics BigQuery table schema. +export interface FeatureRow { + serverId: string; + serverVersion: string; + timestamp: string; // ISO formatted string + dataLimit: DailyDataLimitMetricsReport; +} + +export class BigQueryFeaturesTable implements InsertableTable { + constructor(private bigqueryTable: Table) {} + + async insert(rows: FeatureRow | FeatureRow[]): Promise { + await this.bigqueryTable.insert(rows); + } +} + +export async function postFeatureMetrics( + table: InsertableTable, + report: DailyFeatureMetricsReport +) { + const featureRow: FeatureRow = { + serverId: report.serverId, + serverVersion: report.serverVersion, + timestamp: new Date(report.timestampUtcMs).toISOString(), + dataLimit: report.dataLimit, + }; + return table.insert([featureRow]); +} + +// Returns true iff `obj` contains a valid DailyFeatureMetricsReport. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isValidFeatureMetricsReport(obj: any): obj is DailyFeatureMetricsReport { + if (!obj) { + return false; + } + + // Check that all required fields are present. + const requiredFeatureMetricsReportFields = [ + 'serverId', + 'serverVersion', + 'timestampUtcMs', + 'dataLimit', + ]; + for (const fieldName of requiredFeatureMetricsReportFields) { + if (!obj[fieldName]) { + return false; + } + } + + // Validate the report types are what we expect. + if ( + typeof obj.serverId !== 'string' || + typeof obj.serverVersion !== 'string' || + typeof obj.timestampUtcMs !== 'number' + ) { + return false; + } + + // Validate the server data limit feature + if (typeof obj.dataLimit.enabled !== 'boolean') { + return false; + } + + // Validate the per-key data limit feature + const perKeyLimitCount = obj.dataLimit.perKeyLimitCount; + if (perKeyLimitCount === undefined) { + return true; + } + if (typeof perKeyLimitCount === 'number') { + return obj.dataLimit.perKeyLimitCount >= 0; + } + return false; +} diff --git a/src/metrics_server/index.ts b/src/metrics_server/index.ts new file mode 100644 index 000000000..d925de265 --- /dev/null +++ b/src/metrics_server/index.ts @@ -0,0 +1,81 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import {BigQuery} from '@google-cloud/bigquery'; +import * as express from 'express'; +import * as fs from 'fs'; +import * as path from 'path'; + +import * as connections from './connection_metrics'; +import * as features from './feature_metrics'; + +interface Config { + datasetName: string; + connectionMetricsTableName: string; + featureMetricsTableName: string; +} + +function loadConfig(): Config { + const configText = fs.readFileSync(path.join(__dirname, 'config.json'), {encoding: 'utf8'}); + return JSON.parse(configText); +} + +const PORT = Number(process.env.PORT) || 8080; +const config = loadConfig(); + +const bigqueryDataset = new BigQuery({projectId: 'uproxysite'}).dataset(config.datasetName); +const connectionsTable = new connections.BigQueryConnectionsTable( + bigqueryDataset.table(config.connectionMetricsTableName) +); +const featuresTable = new features.BigQueryFeaturesTable( + bigqueryDataset.table(config.featureMetricsTableName) +); + +const app = express(); +// Parse the request body for content-type 'application/json'. +app.use(express.json()); + +// Accepts hourly connection metrics and inserts them into BigQuery. +// Request body should contain an HourlyServerMetricsReport. +app.post('/connections', async (req: express.Request, res: express.Response) => { + try { + if (!connections.isValidConnectionMetricsReport(req.body)) { + res.status(400).send('Invalid request'); + return; + } + await connections.postConnectionMetrics(connectionsTable, req.body); + res.status(200).send('OK'); + } catch (err) { + res.status(500).send(`Error: ${err}`); + } +}); + +// Accepts daily feature metrics and inserts them into BigQuery. +// Request body should contain a `DailyFeatureMetricsReport`. +app.post('/features', async (req: express.Request, res: express.Response) => { + try { + if (!features.isValidFeatureMetricsReport(req.body)) { + res.status(400).send('Invalid request'); + return; + } + await features.postFeatureMetrics(featuresTable, req.body); + res.status(200).send('OK'); + } catch (err) { + res.status(500).send(`Error: ${err}`); + } +}); + +app.listen(PORT, () => { + console.log(`Metrics server listening on port ${PORT}`); +}); diff --git a/src/metrics_server/infrastructure/table.ts b/src/metrics_server/infrastructure/table.ts new file mode 100644 index 000000000..90e97fda3 --- /dev/null +++ b/src/metrics_server/infrastructure/table.ts @@ -0,0 +1,18 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +// Generic table interface that supports row insertion. +export interface InsertableTable { + insert(rows: T[]): Promise; +} diff --git a/src/metrics_server/model.ts b/src/metrics_server/model.ts new file mode 100644 index 000000000..e355e8243 --- /dev/null +++ b/src/metrics_server/model.ts @@ -0,0 +1,40 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +// NOTE: These interfaces are mirrored in in src/shadowbox/server/metrics.ts +// Find a way to share them between shadowbox and metrics_server. +export interface HourlyConnectionMetricsReport { + serverId: string; + startUtcMs: number; + endUtcMs: number; + userReports: HourlyUserConnectionMetricsReport[]; +} + +export interface HourlyUserConnectionMetricsReport { + userId: string; + countries: string[]; + bytesTransferred: number; +} + +export interface DailyFeatureMetricsReport { + serverId: string; + serverVersion: string; + timestampUtcMs: number; + dataLimit: DailyDataLimitMetricsReport; +} + +export interface DailyDataLimitMetricsReport { + enabled: boolean; + perKeyLimitCount?: number; +} diff --git a/src/metrics_server/package.json b/src/metrics_server/package.json new file mode 100644 index 000000000..0ec776a14 --- /dev/null +++ b/src/metrics_server/package.json @@ -0,0 +1,22 @@ +{ + "name": "outline-metrics-server", + "private": true, + "version": "1.0.1", + "description": "Outline metrics server", + "author": "Outline", + "license": "Apache", + "__COMMENTS__": [ + "@google-cloud/storage here only to help Typescript code using @google-cloud/bigquery compile" + ], + "dependencies": { + "@google-cloud/bigquery": "^5.12.0", + "express": "^4.17.1" + }, + "devDependencies": { + "@google-cloud/storage": "^5.19.4", + "@types/express": "^4.17.12" + }, + "scripts": { + "start": "node ./index.js" + } +} diff --git a/src/metrics_server/start.action.sh b/src/metrics_server/start.action.sh new file mode 100755 index 000000000..974b1d044 --- /dev/null +++ b/src/metrics_server/start.action.sh @@ -0,0 +1,25 @@ +#!/bin/bash -eu +# +# Copyright 2020 The Outline Authors +# +# 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. + +readonly SRC_DIR="src/metrics_server" +readonly BUILD_DIR="build/metrics_server" + +npm run action metrics_server/build + +cp "${SRC_DIR}/config_dev.json" "${BUILD_DIR}/config.json" +cp "${SRC_DIR}/package.json" "${BUILD_DIR}/" + +npx node "${BUILD_DIR}/index.js" diff --git a/src/metrics_server/test.action.sh b/src/metrics_server/test.action.sh new file mode 100755 index 000000000..844aed2e1 --- /dev/null +++ b/src/metrics_server/test.action.sh @@ -0,0 +1,23 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +readonly TEST_DIR="${BUILD_DIR}/js/metrics_server/" +rm -rf "${TEST_DIR}" + +tsc -p "${ROOT_DIR}/src/metrics_server" --outDir "${TEST_DIR}" +jasmine --config="${ROOT_DIR}/jasmine.json" + +rm -rf "${TEST_DIR}" diff --git a/src/metrics_server/test_integration.action.sh b/src/metrics_server/test_integration.action.sh new file mode 100755 index 000000000..b2b6ead5e --- /dev/null +++ b/src/metrics_server/test_integration.action.sh @@ -0,0 +1,99 @@ +#!/bin/bash -eu +# +# Copyright 2020 The Outline Authors +# +# 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. + +# Metrics server integration test. Posts metrics to the development environment and queries BigQuery +# to ensure the rows have been inserted to the features and connections tables. +readonly BIGQUERY_PROJECT='uproxysite' +readonly BIGQUERY_DATASET='uproxy_metrics_dev' +readonly CONNECTIONS_TABLE='connections_v1' +readonly FEATURES_TABLE='feature_metrics' + +readonly METRICS_URL='https://dev.metrics.getoutline.org' + +TMPDIR="$(mktemp -d)" +readonly TMPDIR +readonly CONNECTIONS_REQUEST="${TMPDIR}/connections.json" +readonly CONNECTIONS_RESPONSE="${TMPDIR}/connections_res.json" +readonly CONNECTIONS_EXPECTED_RESPONSE="${TMPDIR}/connections_expected_res.json" +readonly FEATURES_REQUEST="${TMPDIR}/features_req.json" +readonly FEATURES_RESPONSE="${TMPDIR}/features_res.json" +readonly FEATURES_EXPECTED_RESPONSE="${TMPDIR}/features_expected_res.json" + +TIMESTAMP="$(date +%s%3N)" +SERVER_ID="$(uuidgen)" +SERVER_VERSION="$(uuidgen)" +USER_ID1="$(uuidgen)" +USER_ID2="$(uuidgen)" +readonly TIMESTAMP SERVER_ID SERVER_VERSION USER_ID1 USER_ID2 +# BYTES_TRANSFERRED2 < BYTES_TRANSFERRED1 so we can order the records before comparing them. +BYTES_TRANSFERRED1=$((2 + RANDOM % 100)) +BYTES_TRANSFERRED2=$((BYTES_TRANSFERRED1 - 1)) +PER_KEY_LIMIT_COUNT=$((RANDOM)) +declare -ir BYTES_TRANSFERRED1 BYTES_TRANSFERRED2 PER_KEY_LIMIT_COUNT + +echo "Using tmp directory ${TMPDIR}" + +# Write the request data to temporary files. +cat << EOF > "${CONNECTIONS_REQUEST}" +{ + "serverId": "${SERVER_ID}", + "startUtcMs": ${TIMESTAMP}, + "endUtcMs": $((TIMESTAMP+1)), + "userReports": [{ + "userId": "${USER_ID1}", + "bytesTransferred": ${BYTES_TRANSFERRED1}, + "countries": ["US", "NL"] + }, { + "userId": "${USER_ID2}", + "bytesTransferred": ${BYTES_TRANSFERRED2}, + "countries": ["UK"] + }] +} +EOF +cat << EOF > "${FEATURES_REQUEST}" +{ + "serverId": "${SERVER_ID}", + "serverVersion": "${SERVER_VERSION}", + "timestampUtcMs": ${TIMESTAMP}, + "dataLimit": { + "enabled": false, + "perKeyLimitCount": ${PER_KEY_LIMIT_COUNT} + } +} +EOF + +# Write the expected responses to temporary files. +# Ignore the ISO formatted timestamps to ease the comparison. +cat << EOF > "${CONNECTIONS_EXPECTED_RESPONSE}" +[{"bytesTransferred":"${BYTES_TRANSFERRED1}","countries":["US","NL"],"serverId":"${SERVER_ID}","userId":"${USER_ID1}"},{"bytesTransferred":"${BYTES_TRANSFERRED2}","countries":["UK"],"serverId":"${SERVER_ID}","userId":"${USER_ID2}"}] +EOF +cat << EOF > "${FEATURES_EXPECTED_RESPONSE}" +[{"dataLimit":{"enabled":"false","perKeyLimitCount":"${PER_KEY_LIMIT_COUNT}"},"serverId":"${SERVER_ID}","serverVersion":"${SERVER_VERSION}"}] +EOF + +echo "Connections request:" +cat "${CONNECTIONS_REQUEST}" +curl -X POST -H "Content-Type: application/json" -d "@${CONNECTIONS_REQUEST}" "${METRICS_URL}/connections" && echo +sleep 5 +bq --project_id "${BIGQUERY_PROJECT}" --format json query --nouse_legacy_sql "SELECT serverId, userId, bytesTransferred, countries FROM \`${BIGQUERY_DATASET}.${CONNECTIONS_TABLE}\` WHERE serverId = \"${SERVER_ID}\" ORDER BY bytesTransferred DESC LIMIT 2" > "${CONNECTIONS_RESPONSE}" +diff "${CONNECTIONS_RESPONSE}" "${CONNECTIONS_EXPECTED_RESPONSE}" + +echo "Features request:" +cat "${FEATURES_REQUEST}" +curl -X POST -H "Content-Type: application/json" -d "@${FEATURES_REQUEST}" "${METRICS_URL}/features" && echo +sleep 5 +bq --project_id "${BIGQUERY_PROJECT}" --format json query --nouse_legacy_sql "SELECT serverId, serverVersion, dataLimit FROM \`${BIGQUERY_DATASET}.${FEATURES_TABLE}\` WHERE serverId = \"${SERVER_ID}\" ORDER BY timestamp DESC LIMIT 1" > "${FEATURES_RESPONSE}" +diff "${FEATURES_RESPONSE}" "${FEATURES_EXPECTED_RESPONSE}" diff --git a/src/metrics_server/tsconfig.json b/src/metrics_server/tsconfig.json new file mode 100644 index 000000000..5be55c953 --- /dev/null +++ b/src/metrics_server/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "removeComments": false, + "strict": true, + "module": "commonjs", + "outDir": "../../build/metrics_server", + "skipLibCheck": true + }, + "include": ["**/*.ts"] +} diff --git a/src/sentry_webhook/README.md b/src/sentry_webhook/README.md new file mode 100644 index 000000000..9a92f41e0 --- /dev/null +++ b/src/sentry_webhook/README.md @@ -0,0 +1,38 @@ +# Outline Sentry Webhook + +The Outline Sentry webhook is a [Google Cloud Function](https://cloud.google.com/functions/) that receives a Sentry event and posts it to Salesforce. + +## Requirements + +- [Google Cloud SDK](https://cloud.google.com/sdk/) +- Access to Outline's Sentry account. + +## Build + +```sh +npm run action sentry_webhook/build +``` + +## Deploy + +Authenticate with `gcloud`: + +```sh +gcloud auth login +``` + +To deploy: + +```sh +npm run action sentry_webhook/deploy +``` + +## Configure Sentry Webhooks + +- Log in to Outline's [Sentry account](https://sentry.io/outlinevpn/) +- Select a project (outline-client, outline-client-dev, outline-server, outline-server-dev). + - Note that this process must be repeated for all Sentry projects. +- Enable the WebHooks plugin at `https://sentry.io/settings/outlinevpn//plugins/` +- Set the webhook endpoint at `https://sentry.io/settings/outlinevpn//plugins/webhooks/` +- Configure alerts to invoke the webhook at `https://sentry.io/settings/outlinevpn//alerts/` +- Create rules to trigger the webhook at `https://sentry.io/settings/outlinevpn//alerts/rules/` diff --git a/src/sentry_webhook/build.action.sh b/src/sentry_webhook/build.action.sh new file mode 100755 index 000000000..c989235ec --- /dev/null +++ b/src/sentry_webhook/build.action.sh @@ -0,0 +1,17 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +tsc --project "${ROOT_DIR}"src/sentry_webhook/tsconfig.prod.json diff --git a/src/sentry_webhook/deploy.action.sh b/src/sentry_webhook/deploy.action.sh new file mode 100755 index 000000000..6899f8d4e --- /dev/null +++ b/src/sentry_webhook/deploy.action.sh @@ -0,0 +1,25 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +npm run action sentry_webhook/build + +cp src/sentry_webhook/package.json build/sentry_webhook/ +gcloud functions deploy postSentryEventToSalesforce \ + --project=uproxysite \ + --runtime=nodejs18 \ + --trigger-http \ + --source=build/sentry_webhook \ + --entry-point=postSentryEventToSalesforce diff --git a/src/sentry_webhook/event.ts b/src/sentry_webhook/event.ts new file mode 100644 index 000000000..776a87353 --- /dev/null +++ b/src/sentry_webhook/event.ts @@ -0,0 +1,9 @@ +import type {SentryEvent as SentryEventBase} from '@sentry/types'; + +// Although SentryEvent.tags is declared as an index signature object, it is actually an array of +// arrays i.e. [['key0', 'value0'], ['key1', 'value1']]. +export type Tags = null | [string, string][]; + +export interface SentryEvent extends Omit { + tags?: Tags | null; +} diff --git a/src/sentry_webhook/index.ts b/src/sentry_webhook/index.ts new file mode 100644 index 000000000..4d0aa7608 --- /dev/null +++ b/src/sentry_webhook/index.ts @@ -0,0 +1,53 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as express from 'express'; + +import { + postSentryEventToSalesforce, + shouldPostEventToSalesforce, +} from './post_sentry_event_to_salesforce'; +import {SentryEvent} from './event'; + +exports.postSentryEventToSalesforce = (req: express.Request, res: express.Response) => { + if (req.method !== 'POST') { + return res.status(405).send('Method not allowed'); + } + if (!req.body) { + return res.status(400).send('Missing request body'); + } + + const sentryEvent: SentryEvent = req.body.event; + if (!sentryEvent) { + return res.status(400).send('Missing Sentry event'); + } + const eventId = sentryEvent.event_id?.replace(/\n|\r/g, ''); + if (!shouldPostEventToSalesforce(sentryEvent)) { + console.log('Not posting event:', eventId); + return res.status(200).send(); + } + // Use the request message if SentryEvent.message is unpopulated. + sentryEvent.message = sentryEvent.message || req.body.message; + postSentryEventToSalesforce(sentryEvent, req.body.project) + .then(() => { + console.log('Successfully posted event:', eventId); + res.status(200).send(); + }) + .catch((e) => { + console.error(e); + // Send an OK response to Sentry - they don't need to know about errors with posting to + // Salesforce. + res.status(200).send(); + }); +}; diff --git a/src/sentry_webhook/karma.conf.js b/src/sentry_webhook/karma.conf.js new file mode 100644 index 000000000..77c945f76 --- /dev/null +++ b/src/sentry_webhook/karma.conf.js @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +// Copyright 2020 The Outline Authors +// +// 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. + +const {makeConfig} = require('./test.webpack.js'); +process.env.CHROMIUM_BIN = require('puppeteer').executablePath(); + +const baseConfig = makeConfig({ + defaultMode: 'development', +}); + +const TEST_PATTERNS = ['**/*.spec.ts']; + +let preprocessors = {}; +for (const pattern of TEST_PATTERNS) { + preprocessors[pattern] = ['webpack']; +} + +module.exports = function (config) { + config.set({ + frameworks: ['jasmine'], + files: TEST_PATTERNS, + preprocessors, + reporters: ['progress'], + colors: true, + logLevel: config.LOG_INFO, + browsers: ['ChromiumHeadless'], + restartOnFileChange: true, + singleRun: true, + concurrency: Infinity, + webpack: { + module: baseConfig.module, + resolve: baseConfig.resolve, + plugins: baseConfig.plugins, + mode: baseConfig.mode, + }, + }); +}; diff --git a/src/sentry_webhook/package.json b/src/sentry_webhook/package.json new file mode 100644 index 000000000..c3d89670b --- /dev/null +++ b/src/sentry_webhook/package.json @@ -0,0 +1,17 @@ +{ + "name": "sentry_webhook", + "private": true, + "version": "0.1.0", + "description": "Outline Sentry Webhook", + "author": "Outline", + "license": "Apache", + "devDependencies": { + "@sentry/types": "^4.4.1", + "@types/express": "^4.17.12", + "@types/jasmine": "^5.1.0", + "https-browserify": "^1.0.0", + "jasmine": "^5.1.0", + "stream-http": "^3.2.0", + "url": "^0.11.3" + } +} diff --git a/src/sentry_webhook/post_sentry_event_to_salesforce.spec.ts b/src/sentry_webhook/post_sentry_event_to_salesforce.spec.ts new file mode 100644 index 000000000..008ae0311 --- /dev/null +++ b/src/sentry_webhook/post_sentry_event_to_salesforce.spec.ts @@ -0,0 +1,117 @@ +// Copyright 2023 The Outline Authors +// +// 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. + +import {ClientRequest} from 'http'; +import * as https from 'https'; + +import {postSentryEventToSalesforce} from './post_sentry_event_to_salesforce'; +import {SentryEvent} from './event'; + +// NOTE: Jasmine's `toHaveBeenCalledWith` infers parameters for overloads +// incorrectly. See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42455. +function expectToHaveBeenCalledWith(spy: jasmine.Spy, expected: unknown) { + expect(spy.calls.argsFor(0)[0]).toEqual(expected); +} + +const BASIC_EVENT: SentryEvent = { + user: {email: 'foo@bar.com'}, + message: 'my message', +}; + +describe('postSentryEventToSalesforce', () => { + let mockRequest: jasmine.SpyObj; + let requestSpy: jasmine.Spy; + + beforeEach(() => { + mockRequest = jasmine.createSpyObj('request', ['on', 'write', 'end']); + requestSpy = spyOn(https, 'request').and.returnValue(mockRequest); + }); + + it('sends the correct data for a basic prod event', () => { + postSentryEventToSalesforce(BASIC_EVENT, 'outline-clients'); + + const expectedOptions = { + host: 'webto.salesforce.com', + path: '/servlet/servlet.WebToCase', + protocol: 'https:', + method: 'post', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + }; + + expectToHaveBeenCalledWith(requestSpy, expectedOptions); + expectToHaveBeenCalledWith( + mockRequest.write, + 'orgid=00D0b000000BrsN' + + '&recordType=0120b0000006e8i' + + '&email=foo%40bar.com' + + '&00N0b00000BqOA4=' + + '&description=my%20message' + + '&type=Outline%20client' + ); + expect(mockRequest.end).toHaveBeenCalled(); + }); + + it('sends the correct data for a basic dev event', () => { + postSentryEventToSalesforce(BASIC_EVENT, 'outline-clients-dev'); + + const expectedOptions = { + host: 'google-jigsaw--jigsawuat.sandbox.my.salesforce.com', + path: '/servlet/servlet.WebToCase', + protocol: 'https:', + method: 'post', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + }; + + expectToHaveBeenCalledWith(requestSpy, expectedOptions); + expectToHaveBeenCalledWith( + mockRequest.write, + 'orgid=00D750000004dFg' + + '&recordType=0123F000000MWTS' + + '&email=foo%40bar.com' + + '&00N3F000002Rqhq=' + + '&description=my%20message' + + '&type=Outline%20client' + ); + expect(mockRequest.end).toHaveBeenCalled(); + }); + + it('sends correctly converted tags', () => { + const event: SentryEvent = { + user: {email: 'foo@bar.com'}, + message: 'my message', + tags: [ + ['category', 'test category'], + ['os.name', 'Mac OS X'], + ['sentry:release', 'test version'], + ['unknown:tag', 'foo'], + ], + }; + + postSentryEventToSalesforce(event, 'outline-clients'); + + expectToHaveBeenCalledWith( + mockRequest.write, + 'orgid=00D0b000000BrsN' + + '&recordType=0120b0000006e8i' + + '&email=foo%40bar.com' + + '&00N0b00000BqOA4=' + + '&description=my%20message' + + '&type=Outline%20client' + + '&00N0b00000BqOA2=test%20category' + + '&00N0b00000BqOfW=macOs' + + '&00N0b00000BqOfR=test%20version' + ); + expect(mockRequest.end).toHaveBeenCalled(); + }); +}); diff --git a/src/sentry_webhook/post_sentry_event_to_salesforce.ts b/src/sentry_webhook/post_sentry_event_to_salesforce.ts new file mode 100644 index 000000000..45883043c --- /dev/null +++ b/src/sentry_webhook/post_sentry_event_to_salesforce.ts @@ -0,0 +1,186 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as https from 'https'; +import {SentryEvent} from './event'; + +// Defines the Salesforce form field names. +interface SalesforceFormFields { + orgId: string; + recordType: string; + email: string; + description: string; + category: string; + cloudProvider: string; + sentryEventUrl: string; + os: string; + version: string; + type: string; +} + +// Defines the Salesforce form values. +interface SalesforceFormValues { + orgId: string; + recordType: string; +} + +const SALESFORCE_DEV_HOST = 'google-jigsaw--jigsawuat.sandbox.my.salesforce.com'; +const SALESFORCE_PROD_HOST = 'webto.salesforce.com'; +const SALESFORCE_PATH = '/servlet/servlet.WebToCase'; +const SALESFORCE_FORM_FIELDS_DEV: SalesforceFormFields = { + orgId: 'orgid', + recordType: 'recordType', + email: 'email', + description: 'description', + category: '00N3F000002Rqho', + cloudProvider: '00N3F000002Rqhs', + sentryEventUrl: '00N3F000002Rqhq', + os: '00N3F000002cLcN', + version: '00N3F000002cLcI', + type: 'type', +}; +const SALESFORCE_FORM_FIELDS_PROD: SalesforceFormFields = { + orgId: 'orgid', + recordType: 'recordType', + email: 'email', + description: 'description', + category: '00N0b00000BqOA2', + cloudProvider: '00N0b00000BqOA7', + sentryEventUrl: '00N0b00000BqOA4', + os: '00N0b00000BqOfW', + version: '00N0b00000BqOfR', + type: 'type', +}; +const SALESFORCE_FORM_VALUES_DEV: SalesforceFormValues = { + orgId: '00D750000004dFg', + recordType: '0123F000000MWTS', +}; +const SALESFORCE_FORM_VALUES_PROD: SalesforceFormValues = { + orgId: '00D0b000000BrsN', + recordType: '0120b0000006e8i', +}; + +// Returns whether a Sentry event should be sent to Salesforce by checking that it contains an +// email address. +export function shouldPostEventToSalesforce(event: SentryEvent): boolean { + return !!event.user && !!event.user.email && event.user.email !== '[undefined]'; +} + +// Posts a Sentry event to Salesforce using predefined form data. Assumes +// `shouldPostEventToSalesforce` has returned true for `event`. +export function postSentryEventToSalesforce(event: SentryEvent, project: string): Promise { + return new Promise((resolve, reject) => { + // Sentry development projects are marked with 'dev', i.e. outline-client-dev. + const isProd = project.indexOf('-dev') === -1; + const salesforceHost = isProd ? SALESFORCE_PROD_HOST : SALESFORCE_DEV_HOST; + const formFields = isProd ? SALESFORCE_FORM_FIELDS_PROD : SALESFORCE_FORM_FIELDS_DEV; + const formValues = isProd ? SALESFORCE_FORM_VALUES_PROD : SALESFORCE_FORM_VALUES_DEV; + const isClient = project.indexOf('client') !== -1; + const formData = getSalesforceFormData( + formFields, + formValues, + event, + event.user!.email!, + isClient, + project + ); + const req = https.request( + { + host: salesforceHost, + path: SALESFORCE_PATH, + protocol: 'https:', + method: 'post', + headers: { + // The production server will reject requests that do not specify this content type. + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + (res) => { + if (res.statusCode === 200) { + console.debug('Salesforce `is-processed`:', res.headers['is-processed']); + resolve(); + } else { + reject(new Error(`Failed to post form data, response status: ${res.statusCode}`)); + } + } + ); + req.on('error', (err) => { + reject(new Error(`Failed to submit form: ${err}`)); + }); + req.write(formData); + req.end(); + }); +} + +// Returns a URL-encoded string with the Salesforce form data. +function getSalesforceFormData( + formFields: SalesforceFormFields, + formValues: SalesforceFormValues, + event: SentryEvent, + email: string, + isClient: boolean, + project: string +): string { + const form = []; + form.push(encodeFormData(formFields.orgId, formValues.orgId)); + form.push(encodeFormData(formFields.recordType, formValues.recordType)); + form.push(encodeFormData(formFields.email, email)); + form.push(encodeFormData(formFields.sentryEventUrl, getSentryEventUrl(project, event.event_id))); + form.push(encodeFormData(formFields.description, event.message)); + form.push(encodeFormData(formFields.type, isClient ? 'Outline client' : 'Outline manager')); + if (event.tags) { + const tags = new Map(event.tags); + form.push(encodeFormData(formFields.category, tags.get('category'))); + form.push(encodeFormData(formFields.os, toOSPicklistValue(tags.get('os.name')))); + form.push(encodeFormData(formFields.version, tags.get('sentry:release'))); + if (!isClient) { + form.push(encodeFormData(formFields.cloudProvider, tags.get('cloudProvider'))); + } + } + return form.join('&'); +} + +// Returns a picklist value that is allowed by SalesForce for the OS record. +function toOSPicklistValue(value: string | undefined): string | undefined { + if (!value) { + console.warn('No OS found'); + return undefined; + } + + const normalizedValue = value.toLowerCase(); + if (normalizedValue.includes('android')) { + return 'Android'; + } + if (normalizedValue.includes('ios')) { + return 'iOS'; + } + if (normalizedValue.includes('windows')) { + return 'Windows'; + } + if (normalizedValue.includes('mac')) { + return 'macOs'; + } + return 'Linux'; +} + +function encodeFormData(field: string, value?: string) { + return `${encodeURIComponent(field)}=${encodeURIComponent(value || '')}`; +} + +function getSentryEventUrl(project: string, eventId?: string) { + if (!eventId) { + return ''; + } + return `https://sentry.io/outlinevpn/${project}/events/${eventId}`; +} diff --git a/src/sentry_webhook/test.action.sh b/src/sentry_webhook/test.action.sh new file mode 100755 index 000000000..8bcbe36ac --- /dev/null +++ b/src/sentry_webhook/test.action.sh @@ -0,0 +1,26 @@ +#!/bin/bash -eu +# +# Copyright 2023 The Outline Authors +# +# 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. + +readonly TEST_DIR="${BUILD_DIR}/js/sentry_webhook/" +rm -rf "${TEST_DIR}" + +# Use commonjs modules, jasmine runs in node. +tsc -p "${ROOT_DIR}/src/sentry_webhook" --outDir "${TEST_DIR}" --module commonjs +jasmine --config="${ROOT_DIR}/jasmine.json" + +karma start "${ROOT_DIR}/src/sentry_webhook/karma.conf.js" + +rm -rf "${TEST_DIR}" diff --git a/src/sentry_webhook/test.webpack.js b/src/sentry_webhook/test.webpack.js new file mode 100644 index 000000000..7a9f5d767 --- /dev/null +++ b/src/sentry_webhook/test.webpack.js @@ -0,0 +1,38 @@ +// Copyright 2023 The Outline Authors +// +// 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. + +exports.makeConfig = (options) => { + return { + mode: options.defaultMode, + target: options.target, + devtool: 'inline-source-map', + module: { + rules: [ + { + test: /\.ts(x)?$/, + exclude: /node_modules/, + use: ['ts-loader'], + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + fallback: { + https: require.resolve('https-browserify'), + url: require.resolve('url/'), + http: require.resolve('stream-http'), + }, + }, + }; +}; diff --git a/src/sentry_webhook/tsconfig.json b/src/sentry_webhook/tsconfig.json new file mode 100644 index 000000000..75987142d --- /dev/null +++ b/src/sentry_webhook/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "removeComments": false, + "strict": true, + "module": "commonjs", + "outDir": "../../build/sentry_webhook", + "skipLibCheck": true + }, + "include": ["**/*.ts"] +} diff --git a/src/sentry_webhook/tsconfig.prod.json b/src/sentry_webhook/tsconfig.prod.json new file mode 100644 index 000000000..8c0f74bb0 --- /dev/null +++ b/src/sentry_webhook/tsconfig.prod.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.spec.ts"] +} diff --git a/src/server_manager/README.md b/src/server_manager/README.md new file mode 100644 index 000000000..220ee7cd2 --- /dev/null +++ b/src/server_manager/README.md @@ -0,0 +1,65 @@ +# Outline Manager + +## Running + +To run the Outline Manager Electron app: + +``` +npm run action server_manager/electron_app/start ${PLATFORM} +``` + +To run the Outline Manager Electron app with a development build (code not minified): + +``` +BUILD_ENV=development npm run action server_manager/electron_app/start ${PLATFORM} +``` + +Where `${PLATFORM}` is one of `linux`, `macos`, `windows`. + +## Development Server + +To run the Outline Manager as a web app on the browser and listen for changes: + +``` +npm run action server_manager/web_app/start +``` + +## Gallery Server for UI Development + +We have a server app to for quickly iterating on UI components. To spin it up, run + +``` +npm run action server_manager/web_app/start_gallery +``` + +Changes to UI components will be hot reloaded into the gallery. + +## Debug an existing binary + +You can run an existing binary in debug mode by setting `OUTLINE_DEBUG=true`. +This will enable the Developer menu on the application window. + +## Packaging + +To build the app binary: + +``` +npm run action server_manager/electron_app/build ${PLATFORM} -- --buildMode=[debug,release] +``` + +Where `${PLATFORM}` is one of `linux`, `macos`, `windows`. + +The per-platform standalone apps will be at `build/electron_app/static/dist`. + +- Windows: zip files. Only generated if you have [wine](https://www.winehq.org/download) installed. +- Linux: tar.gz files. +- macOS: dmg files if built from macOS, zip files otherwise. + +## Error reporting + +To enable error reporting through [Sentry](https://sentry.io/) for local builds, run: + +```bash +export SENTRY_DSN=[Sentry development API key] +npm run action server_manager/electron_app/start ${PLATFORM} +``` diff --git a/src/server_manager/base.webpack.js b/src/server_manager/base.webpack.js new file mode 100644 index 000000000..9255694b3 --- /dev/null +++ b/src/server_manager/base.webpack.js @@ -0,0 +1,82 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +// Copyright 2020 The Outline Authors +// +// 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. + +const path = require('path'); +const CopyPlugin = require('copy-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const webpack = require('webpack'); + +const OUTPUT_BASE = path.resolve(__dirname, '../../build/server_manager/web_app/static'); + +const GENERATE_CSS_RTL_LOADER = path.resolve(__dirname, 'css-in-js-rtl-loader.js'); + +const CIRCLE_FLAGS_PATH = path.dirname(require.resolve('circle-flags/package.json')); + +exports.makeConfig = (options) => { + return { + mode: options.defaultMode, + entry: [ + require.resolve('@webcomponents/webcomponentsjs/webcomponents-loader.js'), + path.resolve(__dirname, './web_app/ui_components/style.css'), + options.main, + ], + target: options.target, + devtool: 'inline-source-map', + // Run the dev server with `npm run webpack-dev-server --workspace=outline-manager --open` + devServer: { + client: { + overlay: true, + }, + }, + output: {path: OUTPUT_BASE, filename: 'main.js'}, + module: { + rules: [ + { + test: /\.ts(x)?$/, + exclude: /node_modules/, + use: ['ts-loader', GENERATE_CSS_RTL_LOADER], + }, + { + test: /\.js$/, + exclude: /node_modules/, + loader: GENERATE_CSS_RTL_LOADER, + }, + { + test: /\.css?$/, + use: ['style-loader', 'css-loader'], + }, + ], + }, + resolve: {extensions: ['.tsx', '.ts', '.js']}, + plugins: [ + new webpack.DefinePlugin({ + 'outline.gcpAuthEnabled': JSON.stringify(process.env.GCP_AUTH_ENABLED !== 'false'), + // Statically link the Roboto font, rather than link to fonts.googleapis.com + 'window.polymerSkipLoadingFontRoboto': JSON.stringify(true), + }), + new CopyPlugin({ + patterns: [ + {from: `${CIRCLE_FLAGS_PATH}/flags`, to: 'images/flags', context: __dirname}, + {from: 'images', to: 'images', context: __dirname}, // Overwrite any colliding flags. + {from: 'messages', to: 'messages', context: __dirname}, + ], + }), + new HtmlWebpackPlugin({ + template: options.template || path.resolve(__dirname, './index.html'), + filename: 'index.html', + }), + ], + }; +}; diff --git a/src/server_manager/browser.webpack.js b/src/server_manager/browser.webpack.js new file mode 100644 index 000000000..7479f60b3 --- /dev/null +++ b/src/server_manager/browser.webpack.js @@ -0,0 +1,25 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +// Copyright 2020 The Outline Authors +// +// 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. + +// Webpack config to run the Outline Manager on the browser. + +const path = require('path'); +const {makeConfig} = require('./base.webpack.js'); + +module.exports = makeConfig({ + main: path.resolve(__dirname, './web_app/browser_main.ts'), + target: 'web', + defaultMode: 'development', +}); diff --git a/src/server_manager/cloud/digitalocean_api.ts b/src/server_manager/cloud/digitalocean_api.ts new file mode 100644 index 000000000..aa38288fa --- /dev/null +++ b/src/server_manager/cloud/digitalocean_api.ts @@ -0,0 +1,267 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as errors from '../infrastructure/custom_error'; + +export interface DigitalOceanDropletSpecification { + installCommand: string; + size: string; + image: string; + tags: string[]; +} + +// See definition and example at +// https://developers.digitalocean.com/documentation/v2/#retrieve-an-existing-droplet-by-id +export type DropletInfo = Readonly<{ + id: number; + status: 'new' | 'active'; + tags: string[]; + region: {readonly slug: string}; + size: Readonly<{ + transfer: number; + price_monthly: number; + }>; + networks: Readonly<{ + v4: ReadonlyArray< + Readonly<{ + type: string; + ip_address: string; + }> + >; + }>; +}>; + +// Reference: +// https://developers.digitalocean.com/documentation/v2/#get-user-information +export type Account = Readonly<{ + droplet_limit: number; + email: string; + uuid: string; + email_verified: boolean; + status: 'active' | 'warning' | 'locked'; + status_message: string; +}>; + +// Reference: +// https://developers.digitalocean.com/documentation/v2/#regions +export type RegionInfo = Readonly<{ + slug: string; + name: string; + sizes: string[]; + available: boolean; + features: string[]; +}>; + +// Marker class for errors due to network or authentication. +// See below for more details on when this is raised. +export class XhrError extends errors.CustomError { + constructor() { + // No message because XMLHttpRequest.onerror provides no useful info. + super(); + } +} + +// This class contains methods to interact with DigitalOcean on behalf of a user. +export interface DigitalOceanSession { + accessToken: string; + getAccount(): Promise; + createDroplet( + displayName: string, + region: string, + publicKeyForSSH: string, + dropletSpec: DigitalOceanDropletSpecification + ): Promise<{droplet: DropletInfo}>; + deleteDroplet(dropletId: number): Promise; + getRegionInfo(): Promise; + getDroplet(dropletId: number): Promise; + getDropletTags(dropletId: number): Promise; + getDropletsByTag(tag: string): Promise; + getDroplets(): Promise; +} + +export class RestApiSession implements DigitalOceanSession { + // Constructor takes a DigitalOcean access token, which should have + // read+write permissions. + constructor(public accessToken: string) {} + + public getAccount(): Promise { + console.info('Requesting account'); + return this.request<{account: Account}>('GET', 'account').then((response) => { + return response.account; + }); + } + + public createDroplet( + displayName: string, + region: string, + publicKeyForSSH: string, + dropletSpec: DigitalOceanDropletSpecification + ): Promise<{droplet: DropletInfo}> { + const dropletName = makeValidDropletName(displayName); + // Register a key with DigitalOcean, so the user will not get a potentially + // confusing email with their droplet password, which could get mistaken for + // an invite. + return this.registerKey_(dropletName, publicKeyForSSH).then((keyId: number) => { + return this.makeCreateDropletRequest(dropletName, region, keyId, dropletSpec); + }); + } + + private makeCreateDropletRequest( + dropletName: string, + region: string, + keyId: number, + dropletSpec: DigitalOceanDropletSpecification + ): Promise<{droplet: DropletInfo}> { + let requestCount = 0; + const MAX_REQUESTS = 10; + const RETRY_TIMEOUT_MS = 5000; + return new Promise((fulfill, reject) => { + const makeRequestRecursive = () => { + ++requestCount; + console.info(`Requesting droplet creation ${requestCount}/${MAX_REQUESTS}`); + // See https://docs.digitalocean.com/reference/api/api-reference/#operation/droplets_create + this.request<{droplet: DropletInfo}>('POST', 'droplets', { + name: dropletName, + region, + size: dropletSpec.size, + image: dropletSpec.image, + ssh_keys: [keyId], + user_data: dropletSpec.installCommand, + tags: dropletSpec.tags, + ipv6: true, + // We install metrics and droplet agents in the user_data script in order to not delay the droplet readiness. + monitoring: false, + with_droplet_agent: false, + }) + .then(fulfill) + .catch((e) => { + if (e.message.toLowerCase().indexOf('finalizing') >= 0 && requestCount < MAX_REQUESTS) { + // DigitalOcean is still validating this account and may take + // up to 30 seconds. We can retry more frequently to see when + // this error goes away. + setTimeout(makeRequestRecursive, RETRY_TIMEOUT_MS); + } else { + reject(e); + } + }); + }; + makeRequestRecursive(); + }); + } + + public deleteDroplet(dropletId: number): Promise { + console.info('Requesting droplet deletion'); + return this.request('DELETE', 'droplets/' + dropletId); + } + + public getRegionInfo(): Promise { + console.info('Requesting region info'); + return this.request<{regions: RegionInfo[]}>('GET', 'regions').then((response) => { + return response.regions; + }); + } + + // Registers a SSH key with DigitalOcean. + private registerKey_(keyName: string, publicKeyForSSH: string): Promise { + console.info('Requesting key registration'); + return this.request<{ssh_key: {id: number}}>('POST', 'account/keys', { + name: keyName, + public_key: publicKeyForSSH, + }).then((response) => { + return response.ssh_key.id; + }); + } + + public getDroplet(dropletId: number): Promise { + console.info('Requesting droplet'); + return this.request<{droplet: DropletInfo}>('GET', 'droplets/' + dropletId).then((response) => { + return response.droplet; + }); + } + + public getDropletTags(dropletId: number): Promise { + return this.getDroplet(dropletId).then((droplet: DropletInfo) => { + return droplet.tags; + }); + } + + public getDropletsByTag(tag: string): Promise { + console.info('Requesting droplet by tag'); + // TODO Add proper pagination support. Going with 100 for now to extend the default of 20, and confirm UI works + return this.request<{droplets: DropletInfo[]}>( + 'GET', + `droplets?per_page=100&tag_name=${encodeURI(tag)}` + ).then((response) => { + return response.droplets; + }); + } + + public getDroplets(): Promise { + console.info('Requesting droplets'); + // TODO Add proper pagination support. Going with 100 for now to extend the default of 20, and confirm UI works + return this.request<{droplets: DropletInfo[]}>('GET', 'droplets?per_page=100').then((response) => { + return response.droplets; + }); + } + + // Makes an XHR request to DigitalOcean's API, returns a promise which fulfills + // with the parsed object if successful. + private request(method: string, actionPath: string, data?: {}): Promise { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open(method, `https://api.digitalocean.com/v2/${actionPath}`); + xhr.setRequestHeader('Authorization', `Bearer ${this.accessToken}`); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.onload = () => { + // DigitalOcean may return any 2xx status code for success. + if (xhr.status >= 200 && xhr.status <= 299) { + // Parse JSON response if available. For requests like DELETE + // this.response may be empty. + const responseObj = xhr.response ? JSON.parse(xhr.response) : {}; + resolve(responseObj); + } else if (xhr.status === 401) { + console.error('DigitalOcean request failed with Unauthorized error'); + reject(new XhrError()); + } else { + // this.response is a JSON object, whose message is an error string. + const responseJson = JSON.parse(xhr.response); + console.error(`DigitalOcean request failed with status ${xhr.status}`); + reject( + new Error(`XHR ${responseJson.id} failed with ${xhr.status}: ${responseJson.message}`) + ); + } + }; + xhr.onerror = () => { + // This is raised for both network-level and CORS (authentication) + // problems. Since there is, by design for security reasons, no way + // to programmatically distinguish the two (the error instance + // passed to this handler has *no* useful information), we should + // prompt the user for whether to retry or re-authenticate against + // DigitalOcean (this isn't so bad because application-level + // errors, e.g. bad request parameters and even 404s, do *not* raise + // an onerror event). + console.error('Failed to perform DigitalOcean request'); + reject(new XhrError()); + }; + xhr.send(data ? JSON.stringify(data) : undefined); + }); + } +} + +// Removes invalid characters from input name so it can be used with +// DigitalOcean APIs. +function makeValidDropletName(name: string): string { + // Remove all characters outside of A-Z, a-z, 0-9 and '-'. + return name.replace(/[^A-Za-z0-9-]/g, ''); +} diff --git a/src/server_manager/cloud/gcp_api.ts b/src/server_manager/cloud/gcp_api.ts new file mode 100644 index 000000000..9975e521e --- /dev/null +++ b/src/server_manager/cloud/gcp_api.ts @@ -0,0 +1,783 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +// TODO: Share the same OAuth config between electron app and renderer. +// Keep this in sync with {@link gcp_oauth.ts#OAUTH_CONFIG} +const GCP_OAUTH_CLIENT_ID = + '946220775492-a5v6bsdin6o7ncnqn34snuatmrp7dqh0.apps.googleusercontent.com'; +// Note: For native apps, the "client secret" is not actually a secret. +// See https://developers.google.com/identity/protocols/oauth2/native-app. +const GCP_OAUTH_CLIENT_SECRET = 'lQT4Qx9b3CaSHDcnuYFgyYVE'; + +export class GcpError extends Error { + constructor(code: number, message?: string) { + // ref: + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + super(`Error ${code}: ${message}`); // 'Error' breaks prototype chain here + Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain + this.name = new.target.name; + } +} + +/** @see https://cloud.google.com/compute/docs/reference/rest/v1/instances */ +export type Instance = Readonly<{ + id: string; + creationTimestamp: string; + name: string; + description: string; + tags: {items: string[]; fingerprint: string}; + machineType: string; + zone: string; + networkInterfaces: Array<{ + network: string; + subnetwork: string; + networkIP: string; + ipv6Address: string; + name: string; + accessConfigs: Array<{ + type: string; + name: string; + natIP: string; + setPublicPtr: boolean; + publicPtrDomainName: string; + networkTier: string; + kind: string; + }>; + }>; +}>; + +/** @see https://cloud.google.com/compute/docs/reference/rest/v1/addresses */ +type StaticIp = Readonly<{}>; + +const GCE_V1_API = 'https://compute.googleapis.com/compute/v1'; + +function projectUrl(projectId: string): string { + return `${GCE_V1_API}/projects/${projectId}`; +} + +export interface RegionLocator { + /** The GCP project ID. */ + projectId: string; + /** The region of the operation. */ + regionId: string; +} + +function regionUrl({projectId, regionId}: RegionLocator): string { + return `${projectUrl(projectId)}/regions/${regionId}`; +} + +/** + * Represents the scope of a zonal operation + */ +export interface ZoneLocator { + /** The GCP project ID. */ + projectId: string; + /** The zone of the operation. */ + zoneId: string; +} + +function zoneUrl({projectId, zoneId}: ZoneLocator): string { + return `${projectUrl(projectId)}/zones/${zoneId}`; +} + +const zoneUrlRegExp = new RegExp( + '/compute/v1/projects/(?[^/]+)/zones/(?[^/]+)$' +); + +export function parseZoneUrl(url: string): ZoneLocator { + const groups = new URL(url).pathname.match(zoneUrlRegExp).groups; + return { + projectId: groups['projectId'], + zoneId: groups['zoneId'], + }; +} + +/** + * Helper type to avoid error-prone positional arguments to instance-related + * functions. + */ +export interface InstanceLocator extends ZoneLocator { + /** The ID of the instance. */ + instanceId: string; +} + +function instanceUrl(instance: InstanceLocator): string { + return `${zoneUrl(instance)}/instances/${instance.instanceId}`; +} + +/** + * @see https://cloud.google.com/compute/docs/reference/rest/v1/instances/getGuestAttributes#response-body + */ +type GuestAttributes = Readonly<{ + variableKey: string; + variableValue: string; + queryPath: string; + queryValue: {items: Array<{namespace: string; key: string; value: string}>}; +}>; + +/** @see https://cloud.google.com/compute/docs/reference/rest/v1/zones */ +type Zone = Readonly<{ + id: string; + creationTimestamp: string; + name: string; + description: string; + status: 'UP' | 'DOWN'; + region: string; +}>; + +type Status = Readonly<{code: number; message: string}>; + +/** @see https://cloud.google.com/resource-manager/reference/rest/Shared.Types/Operation */ +export type ResourceManagerOperation = Readonly<{name: string; done: boolean; error: Status}>; + +/** + * @see https://cloud.google.com/compute/docs/reference/rest/v1/globalOperations + * @see https://cloud.google.com/compute/docs/reference/rest/v1/zoneOperations + */ +export type ComputeEngineOperation = Readonly<{ + id: string; + name: string; + targetId: string; + status: string; + error: {errors: Status[]}; +}>; + +/** + * @see https://cloud.google.com/service-usage/docs/reference/rest/Shared.Types/ListOperationsResponse#Operation + */ +type ServiceUsageOperation = Readonly<{name: string; done: boolean; error: Status}>; + +/** @see https://cloud.google.com/resource-manager/reference/rest/v1/projects */ +export type Project = Readonly<{ + projectNumber: string; + projectId: string; + name: string; + lifecycleState: string; +}>; + +/** @see https://cloud.google.com/compute/docs/reference/rest/v1/firewalls/get#response-body */ +type Firewall = Readonly<{id: string; name: string}>; + +/** https://cloud.google.com/billing/docs/reference/rest/v1/billingAccounts */ +export type BillingAccount = Readonly<{ + name: string; + open: boolean; + displayName: string; + masterBillingAccount: string; +}>; + +/** https://cloud.google.com/billing/docs/reference/rest/v1/ProjectBillingInfo */ +export type ProjectBillingInfo = Readonly<{ + name: string; + projectId: string; + billingAccountName?: string; + billingEnabled?: boolean; +}>; + +/** + * @see https://accounts.google.com/.well-known/openid-configuration for + * supported claims. + * + * Note: The supported claims are optional and not guaranteed to be in the + * response. + */ +export type UserInfo = Readonly<{email: string}>; + +type Service = Readonly<{ + name: string; + config: {name: string}; + state: 'STATE_UNSPECIFIED' | 'DISABLED' | 'ENABLED'; +}>; + +type ItemsResponse = Readonly<{items: T; nextPageToken: string}>; + +type ListInstancesResponse = ItemsResponse; +type ListAllInstancesResponse = ItemsResponse<{[zone: string]: {instances: Instance[]}}>; +type ListZonesResponse = ItemsResponse; +type ListProjectsResponse = Readonly<{projects: Project[]; nextPageToken: string}>; +type ListFirewallsResponse = ItemsResponse; +type ListBillingAccountsResponse = Readonly<{ + billingAccounts: BillingAccount[]; + nextPageToken: string; +}>; +type ListEnabledServicesResponse = Readonly<{services: Service[]; nextPageToken: string}>; +type RefreshAccessTokenResponse = Readonly<{access_token: string; expires_in: number}>; + +export class HttpError extends Error { + constructor(private statusCode: number, message?: string) { + super(message); + } + + getStatusCode(): number { + return this.statusCode; + } +} + +export class RestApiClient { + private readonly GCP_HEADERS = new Map([ + ['Content-type', 'application/json'], + ['Accept', 'application/json'], + ]); + + private accessToken: string; + + constructor(private refreshToken: string) {} + + /** + * Creates a new Google Compute Engine VM instance in a specified GCP project. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/instances/insert + * + * @param zone - Indicates the GCP project and zone. + * @param data - Request body data. See documentation. + * @return The initial operation response. Call computeEngineOperationZoneWait + * to wait for the creation process to complete. + */ + async createInstance(zone: ZoneLocator, data: {}): Promise { + return this.fetchAuthenticated( + 'POST', + new URL(`${zoneUrl(zone)}/instances`), + this.GCP_HEADERS, + null, + data + ); + } + + /** + * Deletes a specified Google Compute Engine VM instance. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/instances/delete + * + * @param instance - Identifies the instance to delete. + * @return The initial operation response. Call computeEngineOperationZoneWait + * to wait for the deletion process to complete. + */ + deleteInstance(instance: InstanceLocator): Promise { + return this.fetchAuthenticated( + 'DELETE', + new URL(instanceUrl(instance)), + this.GCP_HEADERS + ); + } + + /** + * Gets the specified Google Compute Engine VM instance resource. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/instances/get + * + * @param instance - Identifies the instance to return. + */ + getInstance(instance: InstanceLocator): Promise { + return this.fetchAuthenticated('GET', new URL(instanceUrl(instance)), this.GCP_HEADERS); + } + + /** + * Lists the Google Compute Engine VM instances in a specified zone. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/instances/list + * + * @param zone - Indicates the GCP project and zone. + * @param filter - See documentation. + */ + // TODO: Pagination + listInstances(zone: ZoneLocator, filter?: string): Promise { + let parameters = null; + if (filter) { + parameters = new Map([['filter', filter]]); + } + return this.fetchAuthenticated( + 'GET', + new URL(`${zoneUrl(zone)}/instances`), + this.GCP_HEADERS, + parameters + ); + } + + /** + * Lists all the Google Compute Engine VM instances in a specified project. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/instances/aggregatedList + * + * @param projectId - The GCP project. + * @param filter - See documentation. + */ + // TODO: Pagination + listAllInstances(projectId: string, filter?: string): Promise { + let parameters = null; + if (filter) { + parameters = new Map([['filter', filter]]); + } + return this.fetchAuthenticated( + 'GET', + new URL(`${projectUrl(projectId)}/aggregated/instances`), + this.GCP_HEADERS, + parameters + ); + } + + /** + * Creates a static IP address. + * + * If no IP address is provided, a new static IP address is created. If an + * ephemeral IP address is provided, it is promoted to a static IP address. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/addresses/insert + * + * @param region - The GCP project and region. + * @param data - Request body data. See documentation. + */ + async createStaticIp(region: RegionLocator, data: {}): Promise { + const operation = await this.fetchAuthenticated( + 'POST', + new URL(`${regionUrl(region)}/addresses`), + this.GCP_HEADERS, + null, + data + ); + return await this.computeEngineOperationRegionWait(region, operation.name); + } + + /** + * Deletes a static IP address. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/addresses/delete + * + * @param region - The GCP project and region. + * @param addressName - The name of the static IP address resource. + * @return The initial operation response. Call computeEngineOperationRegionWait + * to wait for the deletion process to complete. + */ + deleteStaticIp(region: RegionLocator, addressName: string): Promise { + return this.fetchAuthenticated( + 'DELETE', + new URL(`${regionUrl(region)}/addresses/${addressName}`), + this.GCP_HEADERS + ); + } + + /** + * Retrieves a static IP address, if it exists. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/addresses/get + * + * @param region - The GCP project and region. + * @param addressName - The name of the static IP address resource. + */ + getStaticIp(region: RegionLocator, addressName: string): Promise { + return this.fetchAuthenticated( + 'GET', + new URL(`${regionUrl(region)}/addresses/${addressName}`), + this.GCP_HEADERS + ); + } + + /** + * Lists the guest attributes applied to the specified Google Compute Engine VM instance. + * + * @see https://cloud.google.com/compute/docs/storing-retrieving-metadata#guest_attributes + * @see https://cloud.google.com/compute/docs/reference/rest/v1/instances/getGuestAttributes + * + * @param instance - Identifies the instance to inspect. + * @param namespace - The namespace of the guest attributes. + */ + async getGuestAttributes( + instance: InstanceLocator, + namespace: string + ): Promise { + try { + const parameters = new Map([['queryPath', namespace]]); + // We must await the call to getGuestAttributes to properly catch any exceptions. + return await this.fetchAuthenticated( + 'GET', + new URL(`${instanceUrl(instance)}/getGuestAttributes`), + this.GCP_HEADERS, + parameters + ); + } catch (error) { + // TODO: Distinguish between 404 not found and other errors. + return undefined; + } + } + + /** + * Creates a firewall under the specified GCP project. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/firewalls/insert + * + * @param projectId - The GCP project ID. + * @param data - Request body data. See documentation. + */ + async createFirewall(projectId: string, data: {}): Promise { + const operation = await this.fetchAuthenticated( + 'POST', + new URL(`${projectUrl(projectId)}/global/firewalls`), + this.GCP_HEADERS, + null, + data + ); + return await this.computeEngineOperationGlobalWait(projectId, operation.name); + } + + /** + * @param projectId - The GCP project ID. + * @param name - The firewall name. + */ + // TODO: Replace with getFirewall (and handle 404 NotFound) + listFirewalls(projectId: string, name: string): Promise { + const filter = `name=${name}`; + const parameters = new Map([['filter', filter]]); + return this.fetchAuthenticated( + 'GET', + new URL(`${projectUrl(projectId)}/global/firewalls`), + this.GCP_HEADERS, + parameters + ); + } + + /** + * Lists the zones available to a given GCP project. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/zones/list + * + * @param projectId - The GCP project ID. + */ + // TODO: Pagination + listZones(projectId: string): Promise { + return this.fetchAuthenticated( + 'GET', + new URL(`${projectUrl(projectId)}/zones`), + this.GCP_HEADERS + ); + } + + /** + * Lists all services that have been enabled on the project. + * + * @param projectId - The GCP project ID. + */ + listEnabledServices(projectId: string): Promise { + const parameters = new Map([['filter', 'state:ENABLED']]); + return this.fetchAuthenticated( + 'GET', + new URL(`https://serviceusage.googleapis.com/v1/projects/${projectId}/services`), + this.GCP_HEADERS, + parameters + ); + } + + /** + * @param projectId - The GCP project ID. + * @param data - Request body data. See documentation. + */ + enableServices(projectId: string, data: {}): Promise { + return this.fetchAuthenticated( + 'POST', + new URL(`https://serviceusage.googleapis.com/v1/projects/${projectId}/services:batchEnable`), + this.GCP_HEADERS, + null, + data + ); + } + + /** + * Creates a new GCP project + * + * The project ID must conform to the following: + * - must be 6 to 30 lowercase letters, digits, or hyphens + * - must start with a letter + * - no trailing hyphens + * + * @see https://cloud.google.com/resource-manager/reference/rest/v1/projects/create + * + * @param data - Request body data. See documentation. + */ + createProject(data: {}): Promise { + return this.fetchAuthenticated( + 'POST', + new URL('https://cloudresourcemanager.googleapis.com/v1/projects'), + this.GCP_HEADERS, + null, + data + ); + } + + /** + * Lists the GCP projects that the user has access to. + * + * @see https://cloud.google.com/resource-manager/reference/rest/v1/projects/list + * + * @param filter - See documentation. + */ + listProjects(filter?: string): Promise { + let parameters = null; + if (filter) { + parameters = new Map([['filter', filter]]); + } + return this.fetchAuthenticated( + 'GET', + new URL('https://cloudresourcemanager.googleapis.com/v1/projects'), + this.GCP_HEADERS, + parameters + ); + } + + /** + * Gets the billing information for a specified GCP project. + * + * @see https://cloud.google.com/billing/docs/reference/rest/v1/projects/getBillingInfo + * + * @param projectId - The GCP project ID. + */ + getProjectBillingInfo(projectId: string): Promise { + return this.fetchAuthenticated( + 'GET', + new URL(`https://cloudbilling.googleapis.com/v1/projects/${projectId}/billingInfo`), + this.GCP_HEADERS + ); + } + + /** + * Associates a GCP project with a billing account. + * + * @see https://cloud.google.com/billing/docs/reference/rest/v1/projects/updateBillingInfo + * + * @param projectId - The GCP project ID. + * @param data - Request body data. See documentation. + */ + updateProjectBillingInfo(projectId: string, data: {}): Promise { + return this.fetchAuthenticated( + 'PUT', + new URL(`https://cloudbilling.googleapis.com/v1/projects/${projectId}/billingInfo`), + this.GCP_HEADERS, + null, + data + ); + } + + /** + * Lists the billing accounts that the user has access to. + * + * @see https://cloud.google.com/billing/docs/reference/rest/v1/billingAccounts/list + */ + listBillingAccounts(): Promise { + return this.fetchAuthenticated( + 'GET', + new URL(`https://cloudbilling.googleapis.com/v1/billingAccounts`), + this.GCP_HEADERS + ); + } + + /** + * Waits for a specified Google Compute Engine zone operation to complete. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/zoneOperations/wait + * + * @param zone - Indicates the GCP project and zone. + * @param operationId - The operation ID. + */ + async computeEngineOperationZoneWait( + zone: ZoneLocator, + operationId: string + ): Promise { + const operation = await this.fetchAuthenticated( + 'POST', + new URL(`${zoneUrl(zone)}/operations/${operationId}/wait`), + this.GCP_HEADERS + ); + if (operation.error?.errors) { + throw new GcpError(operation?.error.errors[0]?.code, operation?.error.errors[0]?.message); + } + return operation; + } + + /** + * Waits for a specified Google Compute Engine region operation to complete. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/regionOperations/wait + * + * @param region - The GCP project and region. + * @param operationId - The operation ID. + */ + computeEngineOperationRegionWait( + region: RegionLocator, + operationId: string + ): Promise { + return this.fetchAuthenticated( + 'POST', + new URL(`${regionUrl(region)}/operations/${operationId}/wait`), + this.GCP_HEADERS + ); + } + + /** + * Waits for a specified Google Compute Engine global operation to complete. + * + * @see https://cloud.google.com/compute/docs/reference/rest/v1/globalOperations/wait + * + * @param projectId - The GCP project ID. + * @param operationId - The operation ID. + */ + computeEngineOperationGlobalWait( + projectId: string, + operationId: string + ): Promise { + return this.fetchAuthenticated( + 'POST', + new URL(`${projectUrl(projectId)}/global/operations/${operationId}/wait`), + this.GCP_HEADERS + ); + } + + resourceManagerOperationGet(operationId: string): Promise { + return this.fetchAuthenticated( + 'GET', + new URL(`https://cloudresourcemanager.googleapis.com/v1/${operationId}`), + this.GCP_HEADERS + ); + } + + serviceUsageOperationGet(operationId: string): Promise { + return this.fetchAuthenticated( + 'GET', + new URL(`https://serviceusage.googleapis.com/v1/${operationId}`), + this.GCP_HEADERS + ); + } + + /** + * Gets the OpenID Connect profile information. + * + * For a list of the supported Google OpenID claims + * @see https://accounts.google.com/.well-known/openid-configuration. + * + * The OpenID standard, including the "userinfo" response and core claims, is + * defined in the links below: + * @see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + * @see https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + */ + getUserInfo(): Promise { + return this.fetchAuthenticated( + 'POST', + new URL('https://openidconnect.googleapis.com/v1/userinfo'), + this.GCP_HEADERS + ); + } + + private async refreshGcpAccessToken(refreshToken: string): Promise { + const headers = new Map([ + ['Host', 'oauth2.googleapis.com'], + ['Content-Type', 'application/x-www-form-urlencoded'], + ]); + const data = { + // TODO: Consider moving client ID to the caller. + client_id: GCP_OAUTH_CLIENT_ID, + client_secret: GCP_OAUTH_CLIENT_SECRET, + refresh_token: refreshToken, + grant_type: 'refresh_token', + }; + const encodedData = this.encodeFormData(data); + const response: RefreshAccessTokenResponse = await this.fetchUnauthenticated( + 'POST', + new URL('https://oauth2.googleapis.com/token'), + headers, + null, + encodedData + ); + return response.access_token; + } + + /** + * Revokes a token. + * + * @see https://developers.google.com/identity/protocols/oauth2/native-app + * + * @param token - A refresh token or access token + */ + // TODO(fortuna): use this to revoke the access token on account disconnection. + // private async revokeGcpToken(token: string): Promise { + // const headers = new Map( + // [['Host', 'oauth2.googleapis.com'], ['Content-Type', 'application/x-www-form-urlencoded']]); + // const parameters = new Map([['token', token]]); + // return this.fetchUnauthenticated( + // 'GET', new URL('https://oauth2.googleapis.com/revoke'), headers, parameters); + // } + + private async fetchAuthenticated( + method: string, + url: URL, + headers: Map, + parameters?: Map, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any + ): Promise { + const httpHeaders = new Map(headers); + + // TODO: Handle token expiration/revokation. + if (!this.accessToken) { + this.accessToken = await this.refreshGcpAccessToken(this.refreshToken); + } + httpHeaders.set('Authorization', `Bearer ${this.accessToken}`); + return this.fetchUnauthenticated(method, url, httpHeaders, parameters, data); + } + + private async fetchUnauthenticated( + method: string, + url: URL, + headers: Map, + parameters?: Map, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any + ): Promise { + const customHeaders = new Headers(); + headers.forEach((value, key) => { + customHeaders.append(key, value); + }); + if (parameters) { + parameters.forEach((value: string, key: string) => url.searchParams.append(key, value)); + } + + // TODO: More robust handling of data types + if (typeof data === 'object') { + data = JSON.stringify(data); + } + + const response = await fetch(url.toString(), { + method: method.toUpperCase(), + headers: customHeaders, + ...(data && {body: data}), + }); + + if (!response.ok) { + throw new HttpError(response.status, response.statusText); + } + + try { + let result = undefined; + if (response.status !== 204) { + result = await response.json(); + } + return result; + } catch (e) { + throw new Error('Error parsing response body: ' + JSON.stringify(e)); + } + } + + private encodeFormData(data: object): string { + return Object.entries(data) + .map((entry) => { + return encodeURIComponent(entry[0]) + '=' + encodeURIComponent(entry[1]); + }) + .join('&'); + } +} diff --git a/src/server_manager/css-in-js-rtl-loader.js b/src/server_manager/css-in-js-rtl-loader.js new file mode 100644 index 000000000..c512cb1d5 --- /dev/null +++ b/src/server_manager/css-in-js-rtl-loader.js @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +// Copyright 2020 The Outline Authors +// +// 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. + +const postcss = require('postcss'); +const rtl = require('postcss-rtl'); + +const CSS_PROCESSOR = postcss([rtl()]); + +function generateRtlCss(css) { + return ( + CSS_PROCESSOR.process(css) + .css // Replace the generated selectors with Shadow DOM selectors for Polymer compatibility. + .replace(/\[dir=rtl\]/g, ':host(:dir(rtl))') + .replace(/\[dir=ltr\]/g, ':host(:dir(ltr))') + // rtlcss generates [dir] selectors for rules unaffected by directionality; ignore them. + .replace(/\[dir\]/g, '') + ); +} +// This is a Webpack loader that searches for diff --git a/src/server_manager/images/aws-thumbnail-1.png b/src/server_manager/images/aws-thumbnail-1.png new file mode 100755 index 000000000..5e057800f Binary files /dev/null and b/src/server_manager/images/aws-thumbnail-1.png differ diff --git a/src/server_manager/images/aws-thumbnail-2.png b/src/server_manager/images/aws-thumbnail-2.png new file mode 100755 index 000000000..c81733fa0 Binary files /dev/null and b/src/server_manager/images/aws-thumbnail-2.png differ diff --git a/src/server_manager/images/check_blue.svg b/src/server_manager/images/check_blue.svg new file mode 100644 index 000000000..47cda61d7 --- /dev/null +++ b/src/server_manager/images/check_blue.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/server_manager/images/check_blue_rtl.svg b/src/server_manager/images/check_blue_rtl.svg new file mode 100644 index 000000000..4360cf86f --- /dev/null +++ b/src/server_manager/images/check_blue_rtl.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/server_manager/images/check_green.svg b/src/server_manager/images/check_green.svg new file mode 100644 index 000000000..fecf51339 --- /dev/null +++ b/src/server_manager/images/check_green.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/server_manager/images/check_green_rtl.svg b/src/server_manager/images/check_green_rtl.svg new file mode 100644 index 000000000..23e9342b7 --- /dev/null +++ b/src/server_manager/images/check_green_rtl.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/server_manager/images/check_orange.svg b/src/server_manager/images/check_orange.svg new file mode 100644 index 000000000..86381f69f --- /dev/null +++ b/src/server_manager/images/check_orange.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/server_manager/images/check_orange_rtl.svg b/src/server_manager/images/check_orange_rtl.svg new file mode 100644 index 000000000..8aa8130b7 --- /dev/null +++ b/src/server_manager/images/check_orange_rtl.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/server_manager/images/check_white.svg b/src/server_manager/images/check_white.svg new file mode 100644 index 000000000..6cc03acb6 --- /dev/null +++ b/src/server_manager/images/check_white.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/server_manager/images/check_white_rtl.svg b/src/server_manager/images/check_white_rtl.svg new file mode 100644 index 000000000..7ee1a6155 --- /dev/null +++ b/src/server_manager/images/check_white_rtl.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/server_manager/images/cloud.svg b/src/server_manager/images/cloud.svg new file mode 100755 index 000000000..50879c3c7 --- /dev/null +++ b/src/server_manager/images/cloud.svg @@ -0,0 +1,16 @@ + + + + Artboard Copy + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/connect-tip-2x.png b/src/server_manager/images/connect-tip-2x.png new file mode 100644 index 000000000..d1cef6cec Binary files /dev/null and b/src/server_manager/images/connect-tip-2x.png differ diff --git a/src/server_manager/images/connected_large.png b/src/server_manager/images/connected_large.png new file mode 100644 index 000000000..6939e9abc Binary files /dev/null and b/src/server_manager/images/connected_large.png differ diff --git a/src/server_manager/images/digital_ocean_logo.svg b/src/server_manager/images/digital_ocean_logo.svg new file mode 100755 index 000000000..ea80e6fb2 --- /dev/null +++ b/src/server_manager/images/digital_ocean_logo.svg @@ -0,0 +1,16 @@ + + + + Artboard + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/do_oauth_billing.svg b/src/server_manager/images/do_oauth_billing.svg new file mode 100755 index 000000000..cbc6449a5 --- /dev/null +++ b/src/server_manager/images/do_oauth_billing.svg @@ -0,0 +1,29 @@ + + + + digitalocean_step_2 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/do_oauth_done.svg b/src/server_manager/images/do_oauth_done.svg new file mode 100755 index 000000000..4d0f39a8c --- /dev/null +++ b/src/server_manager/images/do_oauth_done.svg @@ -0,0 +1,41 @@ + + + + digitalocean_step_3 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/do_oauth_email.svg b/src/server_manager/images/do_oauth_email.svg new file mode 100755 index 000000000..71ebcc3af --- /dev/null +++ b/src/server_manager/images/do_oauth_email.svg @@ -0,0 +1,18 @@ + + + + digitalocean_step_1 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/do_white_logo.svg b/src/server_manager/images/do_white_logo.svg new file mode 100755 index 000000000..e71569f24 --- /dev/null +++ b/src/server_manager/images/do_white_logo.svg @@ -0,0 +1,16 @@ + + + DigitalOcean icon + + + + + + + + + + + + + diff --git a/src/server_manager/images/flags/unknown.png b/src/server_manager/images/flags/unknown.png new file mode 100644 index 000000000..7fc04bfaa Binary files /dev/null and b/src/server_manager/images/flags/unknown.png differ diff --git a/src/server_manager/images/gcp-create-instance-screenshot.png b/src/server_manager/images/gcp-create-instance-screenshot.png new file mode 100644 index 000000000..761b3fa91 Binary files /dev/null and b/src/server_manager/images/gcp-create-instance-screenshot.png differ diff --git a/src/server_manager/images/gcp-create-instance-thumbnail.png b/src/server_manager/images/gcp-create-instance-thumbnail.png new file mode 100644 index 000000000..07caf7a99 Binary files /dev/null and b/src/server_manager/images/gcp-create-instance-thumbnail.png differ diff --git a/src/server_manager/images/gcp-create-project-screenshot.png b/src/server_manager/images/gcp-create-project-screenshot.png new file mode 100644 index 000000000..7046023eb Binary files /dev/null and b/src/server_manager/images/gcp-create-project-screenshot.png differ diff --git a/src/server_manager/images/gcp-create-project-thumbnail.png b/src/server_manager/images/gcp-create-project-thumbnail.png new file mode 100644 index 000000000..22904b6be Binary files /dev/null and b/src/server_manager/images/gcp-create-project-thumbnail.png differ diff --git a/src/server_manager/images/gcp-logo.svg b/src/server_manager/images/gcp-logo.svg new file mode 100644 index 000000000..3fa6234e1 --- /dev/null +++ b/src/server_manager/images/gcp-logo.svg @@ -0,0 +1,29 @@ + + + + logo/google_cloud copy 4 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/gcp-screenshot-1.png b/src/server_manager/images/gcp-screenshot-1.png new file mode 100644 index 000000000..62ba66f85 Binary files /dev/null and b/src/server_manager/images/gcp-screenshot-1.png differ diff --git a/src/server_manager/images/gcp-screenshot-2.png b/src/server_manager/images/gcp-screenshot-2.png new file mode 100644 index 000000000..7ef454eb3 Binary files /dev/null and b/src/server_manager/images/gcp-screenshot-2.png differ diff --git a/src/server_manager/images/gcp-thumbnail-1.png b/src/server_manager/images/gcp-thumbnail-1.png new file mode 100755 index 000000000..57bb3a7ef Binary files /dev/null and b/src/server_manager/images/gcp-thumbnail-1.png differ diff --git a/src/server_manager/images/gcp-thumbnail-2.png b/src/server_manager/images/gcp-thumbnail-2.png new file mode 100755 index 000000000..659226c45 Binary files /dev/null and b/src/server_manager/images/gcp-thumbnail-2.png differ diff --git a/src/server_manager/images/github-icon.png b/src/server_manager/images/github-icon.png new file mode 100644 index 000000000..7b0609fa4 Binary files /dev/null and b/src/server_manager/images/github-icon.png differ diff --git a/src/server_manager/images/ic_done_white_24dp.svg b/src/server_manager/images/ic_done_white_24dp.svg new file mode 100644 index 000000000..076f89762 --- /dev/null +++ b/src/server_manager/images/ic_done_white_24dp.svg @@ -0,0 +1,16 @@ + + + + Group + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/jigsaw-logo.svg b/src/server_manager/images/jigsaw-logo.svg new file mode 100644 index 000000000..74a777155 --- /dev/null +++ b/src/server_manager/images/jigsaw-logo.svg @@ -0,0 +1,28 @@ + + + + +Jigsaw +Created with Sketch. + + + + + + + + + + diff --git a/src/server_manager/images/key-avatar.svg b/src/server_manager/images/key-avatar.svg new file mode 100644 index 000000000..a0c359a0a --- /dev/null +++ b/src/server_manager/images/key-avatar.svg @@ -0,0 +1,14 @@ + + + + Group 6 Copy 6 + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/key-tip-2x.png b/src/server_manager/images/key-tip-2x.png new file mode 100644 index 000000000..2472c1e36 Binary files /dev/null and b/src/server_manager/images/key-tip-2x.png differ diff --git a/src/server_manager/images/launcher-icon.png b/src/server_manager/images/launcher-icon.png new file mode 100644 index 000000000..c345dac21 Binary files /dev/null and b/src/server_manager/images/launcher-icon.png differ diff --git a/src/server_manager/images/manager-about-logo2x.png b/src/server_manager/images/manager-about-logo2x.png new file mode 100644 index 000000000..2ea033c42 Binary files /dev/null and b/src/server_manager/images/manager-about-logo2x.png differ diff --git a/src/server_manager/images/manager-profile-2x.png b/src/server_manager/images/manager-profile-2x.png new file mode 100644 index 000000000..44e252138 Binary files /dev/null and b/src/server_manager/images/manager-profile-2x.png differ diff --git a/src/server_manager/images/metrics.png b/src/server_manager/images/metrics.png new file mode 100644 index 000000000..e8cb9435b Binary files /dev/null and b/src/server_manager/images/metrics.png differ diff --git a/src/server_manager/images/outline-manager-logo.svg b/src/server_manager/images/outline-manager-logo.svg new file mode 100644 index 000000000..a6d031724 --- /dev/null +++ b/src/server_manager/images/outline-manager-logo.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + +Group +Created with Sketch. + + + + + + + + diff --git a/src/server_manager/images/reddit-icon.png b/src/server_manager/images/reddit-icon.png new file mode 100644 index 000000000..38bf21c29 Binary files /dev/null and b/src/server_manager/images/reddit-icon.png differ diff --git a/src/server_manager/images/server-icon-selected.png b/src/server_manager/images/server-icon-selected.png new file mode 100644 index 000000000..dd0d97a49 Binary files /dev/null and b/src/server_manager/images/server-icon-selected.png differ diff --git a/src/server_manager/images/server-icon.png b/src/server_manager/images/server-icon.png new file mode 100644 index 000000000..3209bfcd8 Binary files /dev/null and b/src/server_manager/images/server-icon.png differ diff --git a/src/server_manager/images/server-unreachable.png b/src/server_manager/images/server-unreachable.png new file mode 100644 index 000000000..d9840bc20 Binary files /dev/null and b/src/server_manager/images/server-unreachable.png differ diff --git a/src/server_manager/images/tos-icon.png b/src/server_manager/images/tos-icon.png new file mode 100644 index 000000000..dc8b4d742 Binary files /dev/null and b/src/server_manager/images/tos-icon.png differ diff --git a/src/server_manager/index.html b/src/server_manager/index.html new file mode 100644 index 000000000..943ce3c6b --- /dev/null +++ b/src/server_manager/index.html @@ -0,0 +1,35 @@ + + + + + + + + + + Outline Manager + + + + + diff --git a/src/server_manager/infrastructure/crypto.ts b/src/server_manager/infrastructure/crypto.ts new file mode 100644 index 000000000..ec4329cad --- /dev/null +++ b/src/server_manager/infrastructure/crypto.ts @@ -0,0 +1,38 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as forge from 'node-forge'; + +// Keys are in OpenSSH format +export class KeyPair { + public: string; + private: string; +} + +// Generates an RSA keypair using forge +export function generateKeyPair(): Promise { + return new Promise((resolve, reject) => { + forge.pki.rsa.generateKeyPair({bits: 4096, workers: -1}, (forgeError, keypair) => { + if (forgeError) { + reject(new Error(`Failed to generate SSH key: ${forgeError}`)); + } + // trim() the string because forge adds a trailing space to + // public keys which really messes things up later. + resolve({ + public: forge.ssh.publicKeyToOpenSSH(keypair.publicKey, '').trim(), + private: forge.ssh.privateKeyToOpenSSH(keypair.privateKey, '').trim(), + }); + }); + }); +} diff --git a/src/server_manager/infrastructure/custom_error.ts b/src/server_manager/infrastructure/custom_error.ts new file mode 100644 index 000000000..1250836a6 --- /dev/null +++ b/src/server_manager/infrastructure/custom_error.ts @@ -0,0 +1,23 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +export class CustomError extends Error { + constructor(message?: string) { + // ref: + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + super(message); // 'Error' breaks prototype chain here + Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain + this.name = new.target.name; + } +} diff --git a/src/server_manager/infrastructure/hex_encoding.ts b/src/server_manager/infrastructure/hex_encoding.ts new file mode 100644 index 000000000..8a906ef93 --- /dev/null +++ b/src/server_manager/infrastructure/hex_encoding.ts @@ -0,0 +1,25 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +export function hexToString(hexString: string) { + const bytes: string[] = []; + if (hexString.length % 2 !== 0) { + throw new Error('hexString has odd length, ignoring: ' + hexString); + } + for (let i = 0; i < hexString.length; i += 2) { + const hexByte = hexString.slice(i, i + 2); + bytes.push(String.fromCharCode(parseInt(hexByte, 16))); + } + return bytes.join(''); +} diff --git a/src/server_manager/infrastructure/i18n.spec.ts b/src/server_manager/infrastructure/i18n.spec.ts new file mode 100644 index 000000000..ba6cff305 --- /dev/null +++ b/src/server_manager/infrastructure/i18n.spec.ts @@ -0,0 +1,50 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import * as i18n from './i18n'; + +describe('LanguageMatcher', () => { + it('returns supported language on match', () => { + const SUPPORTED_LANGUAGES = i18n.languageList(['es', 'pt-BR', 'ru']); + const matcher = new i18n.LanguageMatcher(SUPPORTED_LANGUAGES, undefined); + const supportedLanguage = matcher.getBestSupportedLanguage(i18n.languageList(['pt-PT'])); + expect(supportedLanguage.string()).toEqual('pt-BR'); + }); + it('returns the right variant', () => { + const SUPPORTED_LANGUAGES = i18n.languageList(['en-GB', 'en-IN', 'en-US']); + const matcher = new i18n.LanguageMatcher(SUPPORTED_LANGUAGES, undefined); + const supportedLanguage = matcher.getBestSupportedLanguage(i18n.languageList(['en-IN'])); + expect(supportedLanguage.string()).toEqual('en-IN'); + }); + it('prefers first matched user language', () => { + const SUPPORTED_LANGUAGES = i18n.languageList(['en-US', 'pt-BR']); + const matcher = new i18n.LanguageMatcher(SUPPORTED_LANGUAGES, undefined); + const supportedLanguage = matcher.getBestSupportedLanguage( + i18n.languageList(['cn', 'en-GB', 'pt-BR']) + ); + expect(supportedLanguage.string()).toEqual('en-US'); + }); + it('returns default on no match', () => { + const SUPPORTED_LANGUAGES = i18n.languageList(['es', 'pt-BR', 'ru']); + const matcher = new i18n.LanguageMatcher(SUPPORTED_LANGUAGES, new i18n.LanguageCode('fr')); + const supportedLanguage = matcher.getBestSupportedLanguage(i18n.languageList(['cn'])); + expect(supportedLanguage.string()).toEqual('fr'); + }); + it('returns undefined on no match and no default', () => { + const SUPPORTED_LANGUAGES = i18n.languageList(['es', 'pt-BR', 'ru']); + const matcher = new i18n.LanguageMatcher(SUPPORTED_LANGUAGES); + const supportedLanguage = matcher.getBestSupportedLanguage(i18n.languageList(['cn'])); + expect(supportedLanguage).toBeUndefined(); + }); +}); diff --git a/src/server_manager/infrastructure/i18n.ts b/src/server_manager/infrastructure/i18n.ts new file mode 100644 index 000000000..5c21dae88 --- /dev/null +++ b/src/server_manager/infrastructure/i18n.ts @@ -0,0 +1,88 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +export class LanguageCode { + private language: string; + private normalizedLanguage: string; + + constructor(languageCodeStr: string) { + this.language = languageCodeStr; + this.normalizedLanguage = languageCodeStr.toLowerCase(); + } + matches(other: LanguageCode): boolean { + return this.normalizedLanguage === other.normalizedLanguage; + } + string(): string { + return this.language; + } + split(): string[] { + return this.language.split('-'); + } +} + +export class LanguageMatcher { + constructor( + private supportedLanguages: LanguageCode[], + private defaultLanguage: LanguageCode = undefined + ) {} + + // Goes over each user language, trying to find the supported language that matches + // the best. We'll trim variants of the user and supported languages in order to find + // a match, but the language base is guaranteed to match. + getBestSupportedLanguage(userLanguages: LanguageCode[]): LanguageCode | undefined { + for (const userLanguage of userLanguages) { + const parts = userLanguage.split(); + while (parts.length > 0) { + const trimmedUserLanguage = new LanguageCode(parts.join('-')); + const supportedLanguage = this.getSupportedLanguage(trimmedUserLanguage); + if (supportedLanguage) { + return supportedLanguage; + } + parts.pop(); + } + } + return this.defaultLanguage; + } + + // Returns the closest supported language that matches the user language. + // We make sure the language matches, but the variant may differ. + private getSupportedLanguage(userLanguage: LanguageCode): LanguageCode | undefined { + for (const supportedLanguage of this.supportedLanguages) { + const parts = supportedLanguage.split(); + while (parts.length > 0) { + const trimmedSupportedLanguage = new LanguageCode(parts.join('-')); + if (userLanguage.matches(trimmedSupportedLanguage)) { + return supportedLanguage; + } + parts.pop(); + } + } + return undefined; + } +} + +export function languageList(languagesAsStr: string[]): LanguageCode[] { + return languagesAsStr.map((l) => new LanguageCode(l)); +} + +// Returns the languages supported by the browser. +export function getBrowserLanguages(): LanguageCode[] { + // Ensure that navigator.languages is defined and not empty, as can be the case with some browsers + // (i.e. Chrome 59 on Electron). + let languages = navigator.languages as string[]; + if (!languages || languages.length === 0) { + languages = [navigator.language]; + } + return languageList(languages); +} diff --git a/src/server_manager/infrastructure/memory_storage.ts b/src/server_manager/infrastructure/memory_storage.ts new file mode 100644 index 000000000..63298186b --- /dev/null +++ b/src/server_manager/infrastructure/memory_storage.ts @@ -0,0 +1,41 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +export class InMemoryStorage implements Storage { + readonly length: number; + [key: string]: {}; + [index: number]: string; + + constructor(private store: Map = new Map()) {} + + clear(): void { + throw new Error('InMemoryStorage.clear not implemented'); + } + + getItem(key: string): string | null { + return this.store.get(key) || null; + } + + key(_index: number): string | null { + throw new Error('InMemoryStorage.key not implemented'); + } + + removeItem(key: string): void { + this.store.delete(key); + } + + setItem(key: string, data: string): void { + this.store.set(key, data); + } +} diff --git a/src/server_manager/infrastructure/path_api.spec.ts b/src/server_manager/infrastructure/path_api.spec.ts new file mode 100644 index 000000000..1e8097273 --- /dev/null +++ b/src/server_manager/infrastructure/path_api.spec.ts @@ -0,0 +1,67 @@ +// Copyright 2022 The Outline Authors +// +// 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. + +import {PathApiClient} from './path_api'; + +describe('PathApi', () => { + // Mock fetcher + let lastRequest: HttpRequest; + let nextResponse: Promise; + + const fetcher = (request: HttpRequest) => { + lastRequest = request; + return nextResponse; + }; + + beforeEach(() => { + lastRequest = undefined; + nextResponse = undefined; + }); + + const api = new PathApiClient('https://asdf.test/foo', fetcher); + + it('GET', async () => { + const response = {status: 200, body: '{"asdf": true}'}; + nextResponse = Promise.resolve(response); + expect(await api.request('bar')).toEqual({asdf: true}); + expect(lastRequest).toEqual({ + url: 'https://asdf.test/foo/bar', + method: 'GET', + }); + }); + + it('PUT form data', async () => { + const response = {status: 200, body: '{"asdf": true}'}; + nextResponse = Promise.resolve(response); + expect(await api.requestForm('bar', 'PUT', {name: 'value'})).toEqual({asdf: true}); + expect(lastRequest).toEqual({ + url: 'https://asdf.test/foo/bar', + method: 'PUT', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: 'name=value', + }); + }); + + it('POST JSON data', async () => { + const response = {status: 200, body: '{"asdf": true}'}; + nextResponse = Promise.resolve(response); + expect(await api.requestJson('bar', 'POST', {key: 'value'})).toEqual({asdf: true}); + expect(lastRequest).toEqual({ + url: 'https://asdf.test/foo/bar', + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: '{"key":"value"}', + }); + }); +}); diff --git a/src/server_manager/infrastructure/path_api.ts b/src/server_manager/infrastructure/path_api.ts new file mode 100644 index 000000000..3d0123e35 --- /dev/null +++ b/src/server_manager/infrastructure/path_api.ts @@ -0,0 +1,136 @@ +// Copyright 2022 The Outline Authors +// +// 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. + +// This file is imported by both the Electron and Renderer process code, +// so it cannot contain any imports that are not available in both +// environments. + +// These type definitions are designed to bridge the differences between +// the Fetch API and the Node.JS HTTP API, while also being compatible +// with the Structured Clone algorithm so that they can be passed between +// the Electron and Renderer processes. + +import {CustomError} from './custom_error'; + +export interface HttpRequest { + url: string; + method: string; + headers?: Record; + body?: string; +} + +export interface HttpResponse { + status: number; + body?: string; +} + +// A Fetcher provides the HTTP client functionality for PathApi. +export type Fetcher = (request: HttpRequest) => Promise; + +// Thrown when an API request fails. +export class ServerApiError extends CustomError { + constructor(message: string, public readonly response?: HttpResponse) { + super(message); + } + + // Returns true if no response was received, i.e. a network error was encountered. + // Can be used to distinguish between client and server-side issues. + isNetworkError() { + return !this.response; + } +} + +/** + * Provides access to an HTTP API of the kind exposed by the Shadowbox server. + * + * An API is defined by a `base` URL, under which all endpoints are defined. + * Request bodies are JSON, HTML-form data, or empty. Response bodies are + * JSON or empty. + * + * If a fingerprint is set, requests are proxied through Node.JS to enable + * certificate pinning. + */ +export class PathApiClient { + /** + * @param base A valid URL + * @param fingerprint A SHA-256 hash of the expected leaf certificate, in binary encoding. + */ + constructor(public readonly base: string, public readonly fetcher: Fetcher) {} + + /** + * Makes a request relative to the base URL with a JSON body. + * + * @param path Relative path (no initial '/') + * @param method HTTP method + * @param body JSON-compatible object + * @returns Response body (JSON or void) + */ + async requestJson(path: string, method: string, body: object): Promise { + return this.request(path, method, 'application/json', JSON.stringify(body)); + } + + /** + * Makes a request relative to the base URL with an HTML-form style body. + * + * @param path Relative path (no initial '/') + * @param method HTTP method + * @param params Form data to send + * @returns Response body (JSON or void) + */ + async requestForm(path: string, method: string, params: Record): Promise { + const body = new URLSearchParams(params); + return this.request(path, method, 'application/x-www-form-urlencoded', body.toString()); + } + + /** + * Makes a request relative to the base URL. + * + * @param path Relative path (no initial '/') + * @param method HTTP method + * @param contentType Content-Type header value + * @param body Request body + * @returns Response body (JSON or void) + */ + async request(path: string, method = 'GET', contentType?: string, body?: string): Promise { + let base = this.base; + if (!base.endsWith('/')) { + base += '/'; + } + const url = base + path; + const request: HttpRequest = {url, method}; + if (contentType) { + request.headers = {'Content-Type': contentType}; + } + if (body) { + request.body = body; + } + let response: HttpResponse; + try { + response = await this.fetcher(request); + } catch (e) { + throw new ServerApiError(`API request to ${path} failed due to network error: ${e.message}`); + } + if (response.status < 200 || response.status >= 300) { + throw new ServerApiError( + `API request to ${path} failed with status ${response.status}`, + response + ); + } + if (!response.body) { + return; + } + // Assume JSON and unsafe cast to `T`. + return JSON.parse(response.body); + } +} diff --git a/src/server_manager/infrastructure/sentry.spec.ts b/src/server_manager/infrastructure/sentry.spec.ts new file mode 100644 index 000000000..e120c53c7 --- /dev/null +++ b/src/server_manager/infrastructure/sentry.spec.ts @@ -0,0 +1,24 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import * as sentry from './sentry'; + +describe('getSentryApiUrl', () => { + it('returns the right URL', () => { + const url = sentry.getSentryApiUrl('https://_key_@_org_.ingest.sentry.io/_project_'); + expect(url).toEqual( + 'https://_org_.ingest.sentry.io/api/_project_/store/?sentry_version=7&sentry_key=_key_' + ); + }); +}); diff --git a/src/server_manager/infrastructure/sentry.ts b/src/server_manager/infrastructure/sentry.ts new file mode 100644 index 000000000..68d7bc15e --- /dev/null +++ b/src/server_manager/infrastructure/sentry.ts @@ -0,0 +1,30 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +// Returns Sentry URL for DSN string or undefined if `sentryDsn` is falsy. +// e.g. for DSN "https://[API_KEY]@[SUBDOMAIN].ingest.sentry.io/[PROJECT_ID]" +// this will return +// "https://[SUBDOMAIN].ingest.sentry.io/api/[PROJECT_ID]/store/?sentry_version=7&sentry_key=[API_KEY]" +export function getSentryApiUrl(sentryDsn?: string): string | undefined { + if (!sentryDsn) { + return undefined; + } + const dsnUrl = new URL(sentryDsn); + const sentryKey = dsnUrl.username; + // Trims leading '/'; + const project = dsnUrl.pathname.substr(1); + return `https://${encodeURIComponent(dsnUrl.hostname)}/api/${encodeURIComponent( + project + )}/store/?sentry_version=7&sentry_key=${sentryKey}`; +} diff --git a/src/server_manager/infrastructure/sleep.ts b/src/server_manager/infrastructure/sleep.ts new file mode 100644 index 000000000..5ccf11b28 --- /dev/null +++ b/src/server_manager/infrastructure/sleep.ts @@ -0,0 +1,17 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/server_manager/infrastructure/value_stream.spec.ts b/src/server_manager/infrastructure/value_stream.spec.ts new file mode 100644 index 000000000..b423d014b --- /dev/null +++ b/src/server_manager/infrastructure/value_stream.spec.ts @@ -0,0 +1,105 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import {ValueStream} from './value_stream'; + +describe('ValueStream', () => { + it('get returns initial value', () => { + const stream = new ValueStream('foo'); + expect(stream.get()).toEqual('foo'); + }); + + it('watch yields initial value', async () => { + const stream = new ValueStream('foo'); + for await (const value of stream.watch()) { + expect(value).toEqual('foo'); + return; + } + fail("Loop didn't run"); + }); + + it('watch on a closed stream yields the final value and exits', async () => { + const stream = new ValueStream('foo'); + stream.close(); + for await (const value of stream.watch()) { + expect(value).toEqual('foo'); + } + }); + + it('closing a stream terminates existing watchers', async () => { + const stream = new ValueStream('foo'); + for await (const value of stream.watch()) { + expect(value).toEqual('foo'); + stream.close(); + } + }); + + it('close can safely be called twice', async () => { + const stream = new ValueStream('foo'); + stream.close(); + stream.close(); + }); + + it('get works after close', () => { + const stream = new ValueStream('foo'); + stream.close(); + expect(stream.get()).toEqual('foo'); + }); + + it('set changes the value', () => { + const stream = new ValueStream('foo'); + stream.set('bar'); + expect(stream.get()).toEqual('bar'); + }); + + it('set updates the generator', async () => { + const stream = new ValueStream('foo'); + for await (const value of stream.watch()) { + if (value === 'foo') { + stream.set('bar'); + } else { + expect(value).toEqual('bar'); + break; + } + } + }); + + it('the last update in a burst is received', async () => { + const stream = new ValueStream('foo'); + let value; + for await (value of stream.watch()) { + if (value === 'foo') { + stream.set('bar'); + stream.set('baz'); + } else if (value === 'baz') { + stream.close(); + } + } + }); + + it('updates can be made during updates', async () => { + const stream = new ValueStream(0); + const stepTo10 = async () => { + for await (const value of stream.watch()) { + if (value === 10) { + break; + } + stream.set(value + 1); + } + }; + + await Promise.all([stepTo10(), stepTo10(), stepTo10()]); + expect(stream.get()).toEqual(10); + }); +}); diff --git a/src/server_manager/infrastructure/value_stream.ts b/src/server_manager/infrastructure/value_stream.ts new file mode 100644 index 000000000..472f8fa70 --- /dev/null +++ b/src/server_manager/infrastructure/value_stream.ts @@ -0,0 +1,69 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +/** + * Represents a value that can change over time, with a generator that + * exposes changes to the value. + * + * Watchers are not guaranteed to see every intermediate value, but are + * guaranteed to see the last value in a series of updates. + */ +export class ValueStream { + private wakers: Array<(closed: boolean) => void> = []; + constructor(private value: T) {} + + get(): T { + return this.value; + } + + set(newValue: T) { + if (this.isClosed()) { + throw new Error('Cannot change a closed value stream'); + } + this.value = newValue; + const wakers = this.wakers; + this.wakers = []; + wakers.forEach((waker) => waker(false)); + } + + close() { + if (this.isClosed()) { + return; + } + const finalWakers = this.wakers; + this.wakers = null; + finalWakers.forEach((waker) => waker(true)); + } + + isClosed() { + return this.wakers === null; + } + + private nextChange(): Promise { + if (this.isClosed()) { + return Promise.resolve(true); + } + return new Promise((resolve) => this.wakers.push(resolve)); + } + + async *watch(): AsyncGenerator { + let closed = false; + while (!closed) { + const nextChange = this.nextChange(); + yield this.value; + closed = await nextChange; + } + yield this.value; + } +} diff --git a/src/server_manager/install_scripts/build_do_install_script_ts.node.js b/src/server_manager/install_scripts/build_do_install_script_ts.node.js new file mode 100644 index 000000000..ad31b25e4 --- /dev/null +++ b/src/server_manager/install_scripts/build_do_install_script_ts.node.js @@ -0,0 +1,29 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +'use strict'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const fs = require('fs'); + +const tarballBinary = fs.readFileSync(process.argv[2]); +const base64Tarball = tarballBinary.toString('base64'); +const scriptText = ` +(base64 --decode | tar --extract --gzip ) < "${SHADOWBOX_DIR}/install-shadowbox-output" + +# Initialize sentry log file. +export SENTRY_LOG_FILE="${SHADOWBOX_DIR}/sentry-log-file.txt" +true > "${SENTRY_LOG_FILE}" +function log_for_sentry() { + echo "[$(date "+%Y-%m-%d@%H:%M:%S")]" "do_install_server.sh" "$@" >> "${SENTRY_LOG_FILE}" +} +function post_sentry_report() { + if [[ -n "${SENTRY_API_URL}" ]]; then + # Get JSON formatted string. This command replaces newlines with literal '\n' + # but otherwise assumes that there are no other characters to escape for JSON. + # If we need better escaping, we can install the jq command line tool. + local -ir SENTRY_PAYLOAD_BYTE_LIMIT=8000 + local SENTRY_PAYLOAD + SENTRY_PAYLOAD="{\"message\": \"Install error:\n$(awk '{printf "%s\\n", $0}' < "${SENTRY_LOG_FILE}" | tail --bytes "${SENTRY_PAYLOAD_BYTE_LIMIT}")\"}" + # See Sentry documentation at: + # https://media.readthedocs.org/pdf/sentry/7.1.0/sentry.pdf + curl "${SENTRY_API_URL}" -H "Origin: shadowbox" --data-binary "${SENTRY_PAYLOAD}" + fi +} + +# For backward-compatibility: +readonly DO_ACCESS_TOKEN="${DO_ACCESS_TOKEN:-ACCESS_TOKEN}" + +if [[ -z "${DO_ACCESS_TOKEN}" ]]; then + echo "Access token must be supplied" + exit 1 +fi + +# DigitalOcean's Metadata API base url. +# This URL only supports HTTP (not HTTPS) requests, however it is a local link +# address so not at risk for man-in-the-middle attacks or eavesdropping. +# More detail at https://serverfault.com/questions/427018/what-is-this-ip-address-169-254-169-254 +readonly DO_METADATA_URL="http://169.254.169.254/metadata/v1" + +function cloud::public_ip() { + curl "${DO_METADATA_URL}/interfaces/public/0/ipv4/address" +} + +# Applies a tag to this droplet. +function cloud::add_tag() { + local -r tag="$1" + local -ar base_flags=(-X POST -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${DO_ACCESS_TOKEN}") + local -r TAGS_URL='https://api.digitalocean.com/v2/tags' + # Create the tag + curl "${base_flags[@]}" -d "{\"name\":\"${tag}\"}" "${TAGS_URL}" + local droplet_id + droplet_id="$(curl "${DO_METADATA_URL}/id")" + printf -v droplet_obj ' +{ + "resources": [{ + "resource_id": "%s", + "resource_type": "droplet" + }] +}' "${droplet_id}" + # Link the tag to this droplet + curl "${base_flags[@]}" -d "${droplet_obj}" "${TAGS_URL}/${tag}/resources" +} + +# Adds a key-value tag to the droplet. +# Takes the key as the only argument and reads the value from stdin. +# add_kv_tag() converts the input value to hex, because (1) DigitalOcean +# tags may only contain letters, numbers, : - and _, and (2) there is +# currently a bug that makes tags case-insensitive, so we can't use base64. +function cloud::add_kv_tag() { + local -r key="$1" + local value + value="$(xxd -p -c 255)" + cloud::add_tag "kv:${key}:${value}" +} + +# Adds a key-value tag where the value is already hex-encoded. +function cloud::add_encoded_kv_tag() { + local -r key="$1" + local value + read -r value + cloud::add_tag "kv:${key}:${value}" +} + +echo "true" | cloud::add_encoded_kv_tag "install-started" + +log_for_sentry "Starting install" + +# DigitalOcean's docker image comes with ufw enabled by default, disable so when +# can serve the shadowbox manager and instances on arbitrary high number ports. +log_for_sentry "Disabling ufw" +ufw disable + +# Recent DigitalOcean Ubuntu droplets have unattended-upgrades configured from +# the outset but we want to enable automatic rebooting so that critical updates +# are applied without the Outline user's intervention. +readonly UNATTENDED_UPGRADES_CONFIG='/etc/apt/apt.conf.d/50unattended-upgrades' +if [[ -f "${UNATTENDED_UPGRADES_CONFIG}" ]]; then + log_for_sentry "Configuring auto-updates" + cat >> "${UNATTENDED_UPGRADES_CONFIG}" << EOF + +// Enabled by Outline manager installer. +Unattended-Upgrade::Automatic-Reboot "true"; +EOF +fi + +# Enable BBR. +# Recent DigitalOcean one-click images are based on Ubuntu 18 and have kernel 4.15+. +log_for_sentry "Enabling BBR" +cat >> /etc/sysctl.conf << EOF + +# Added by Outline. +net.core.default_qdisc=fq +net.ipv4.tcp_congestion_control=bbr +EOF +sysctl -p + +log_for_sentry "Getting SB_PUBLIC_IP" +SB_PUBLIC_IP="$(cloud::public_ip)" +export SB_PUBLIC_IP + +log_for_sentry "Initializing ACCESS_CONFIG" +export ACCESS_CONFIG="${SHADOWBOX_DIR}/access.txt" +true > "${ACCESS_CONFIG}" + +# Set trap which publishes an error tag and sentry report only if there is an error. +function finish { + local -ir INSTALL_SERVER_EXIT_CODE=$? + log_for_sentry "In EXIT trap, exit code ${INSTALL_SERVER_EXIT_CODE}" + if ! ( grep --quiet apiUrl "${ACCESS_CONFIG}" && grep --quiet certSha256 "${ACCESS_CONFIG}" ); then + echo "INSTALL_SCRIPT_FAILED: ${INSTALL_SERVER_EXIT_CODE}" | cloud::add_kv_tag "install-error" + # Post error report to sentry. + post_sentry_report + fi +} +trap finish EXIT + +# Run install script asynchronously, so tags can be written as soon as they are ready. +log_for_sentry "Running install_server.sh" +./install_server.sh& +declare -ir install_pid=$! + +# Save tags for access information. +log_for_sentry "Reading tags from ACCESS_CONFIG" +tail -f "${ACCESS_CONFIG}" "--pid=${install_pid}" | while IFS=: read -r key value; do + case "${key}" in + certSha256) + # Bypass encoding + log_for_sentry "Writing certSha256 tag" + echo "${value}" | cloud::add_encoded_kv_tag "${key}" + ;; + apiUrl) + log_for_sentry "Writing apiUrl tag" + echo -n "${value}" | cloud::add_kv_tag "${key}" + ;; + esac +done + +# Wait for install script to finish, so that if there is any error in install_server.sh, +# the finish trap in this file will be able to access its error code. +wait "${install_pid}" + +# We could install the agents below in the create droplet request, but they add +# over a minute of delay to the droplet readiness. Instead, we do it here. +# Since the server manager looks only for the tags created in the previous +# step, this does not slow down server creation. + +# Install the DigitalOcean Metrics Agent, for improved monitoring: +# https://docs.digitalocean.com/products/monitoring/how-to/install-agent/ +curl -sSL https://repos.insights.digitalocean.com/install.sh | bash + +# Install the DigitalOcean Droplet Agent, for web console integration: +# https://docs.digitalocean.com/products/droplets/how-to/manage-agent/ +curl -sSL https://repos-droplet.digitalocean.com/install.sh | bash diff --git a/src/server_manager/install_scripts/gcp_install_server.sh b/src/server_manager/install_scripts/gcp_install_server.sh new file mode 100755 index 000000000..2d22dcde7 --- /dev/null +++ b/src/server_manager/install_scripts/gcp_install_server.sh @@ -0,0 +1,145 @@ +#!/bin/bash +# +# Copyright 2021 The Outline Authors +# +# 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. + +# Script to install Shadowbox on a GCP Compute Engine instance + +# You may set the following environment variables, overriding their defaults: +# SB_IMAGE: Shadowbox Docker image to install, e.g. quay.io/outline/shadowbox:nightly +# SB_API_PORT: The port number of the management API. +# SENTRY_API_URL: Url to post Sentry report to on error. +# WATCHTOWER_REFRESH_SECONDS: refresh interval in seconds to check for updates, +# defaults to 3600. + +set -euo pipefail + +export SHADOWBOX_DIR="${SHADOWBOX_DIR:-${HOME:-/root}/shadowbox}" +mkdir -p "${SHADOWBOX_DIR}" + +# Save output for debugging +exec &> "${SHADOWBOX_DIR}/install-shadowbox-output" + +function cloud::public_ip() { + curl curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip" +} + +# Initialize sentry log file. +export SENTRY_LOG_FILE="${SHADOWBOX_DIR}/sentry-log-file.txt" +true > "${SENTRY_LOG_FILE}" +function log_for_sentry() { + echo "[$(date "+%Y-%m-%d@%H:%M:%S")]" "gcp_install_server.sh" "$@" >> "${SENTRY_LOG_FILE}" +} + +function post_sentry_report() { + if [[ -n "${SENTRY_API_URL}" ]]; then + # Get JSON formatted string. This command replaces newlines with literal '\n' + # but otherwise assumes that there are no other characters to escape for JSON. + # If we need better escaping, we can install the jq command line tool. + local -ir SENTRY_PAYLOAD_BYTE_LIMIT=8000 + local SENTRY_PAYLOAD + SENTRY_PAYLOAD="{\"message\": \"Install error:\n$(awk '{printf "%s\\n", $0}' < "${SENTRY_LOG_FILE}" | tail --bytes "${SENTRY_PAYLOAD_BYTE_LIMIT}")\"}" + # See Sentry documentation at: + # https://media.readthedocs.org/pdf/sentry/7.1.0/sentry.pdf + curl "${SENTRY_API_URL}" -H "Origin: shadowbox" --data-binary "${SENTRY_PAYLOAD}" + fi +} + +# Applies a guest attribute to the GCE VM. +function cloud::set_guest_attribute() { + local label_key="$1" + local label_value="$2" + + local GUEST_ATTIBUTE_NAMESPACE="outline" + local SET_GUEST_ATTRIBUTE_URL="http://metadata.google.internal/computeMetadata/v1/instance/guest-attributes/${GUEST_ATTIBUTE_NAMESPACE}/${label_key}" + curl -H "Metadata-Flavor: Google" -X PUT -d "${label_value}" "${SET_GUEST_ATTRIBUTE_URL}" +} + +cloud::set_guest_attribute "install-started" "true" + +# Enable BBR. +# Recent DigitalOcean one-click images are based on Ubuntu 18 and have kernel 4.15+. +log_for_sentry "Enabling BBR" +cat >> /etc/sysctl.conf << EOF + +# Added by Outline. +net.core.default_qdisc=fq +net.ipv4.tcp_congestion_control=bbr +EOF +sysctl -p + +log_for_sentry "Initializing ACCESS_CONFIG" +export ACCESS_CONFIG="${SHADOWBOX_DIR}/access.txt" +true > "${ACCESS_CONFIG}" + +# Set trap which publishes an error tag and sentry report only if there is an error. +function finish { + INSTALL_SERVER_EXIT_CODE=$? + log_for_sentry "In EXIT trap, exit code ${INSTALL_SERVER_EXIT_CODE}" + if ! ( grep --quiet apiUrl "${ACCESS_CONFIG}" && grep --quiet certSha256 "${ACCESS_CONFIG}" ); then + echo "INSTALL_SCRIPT_FAILED: ${INSTALL_SERVER_EXIT_CODE}" | cloud::set_guest_attribute "install-error" "true" + # Post error report to sentry. + post_sentry_report + fi +} +trap finish EXIT + +# Docker is not installed by default. If we don't install it here, +# install.sh will download it using the get.docker.com script (much slower). +log_for_sentry "Downloading Docker" +# Following instructions from https://docs.docker.com/engine/install/ubuntu/#install-from-a-package + +declare -ar PACKAGES=( + 'containerd.io_1.4.9-1_amd64.deb' + 'docker-ce_20.10.8~3-0~ubuntu-focal_amd64.deb' + 'docker-ce-cli_20.10.8~3-0~ubuntu-focal_amd64.deb' +) + +declare packages_csv +packages_csv="$(printf ',%s' "${PACKAGES[@]}")" +packages_csv="${packages_csv:1}" +curl --remote-name-all --fail "https://download.docker.com/linux/ubuntu/dists/focal/pool/stable/amd64/{${packages_csv}}" +log_for_sentry "Installing Docker" +dpkg --install "${PACKAGES[@]}" +rm "${PACKAGES[@]}" + +# Run install script asynchronously, so tags can be written as soon as they are ready. +log_for_sentry "Running install_server.sh" +./install_server.sh& +declare -ir install_pid=$! + +# Save tags for access information. +log_for_sentry "Reading tags from ACCESS_CONFIG" +tail -f "${ACCESS_CONFIG}" "--pid=${install_pid}" | while IFS=: read -r key value; do + case "${key}" in + certSha256) + log_for_sentry "Writing certSha256 tag" + echo "case certSha256: ${key}/${value}" + # The value is hex(fingerprint) and Electron expects base64(fingerprint). + hex_fingerprint="${value}" + base64_fingerprint="$(echo -n "${hex_fingerprint}" | xxd -revert -p -c 255 | base64)" + cloud::set_guest_attribute "${key}" "${base64_fingerprint}" + ;; + apiUrl) + log_for_sentry "Writing apiUrl tag" + echo "case apiUrl: ${key}/${value}" + url_value=$(echo -n "${value}") + cloud::set_guest_attribute "${key}" "${url_value}" + ;; + esac +done + +# Wait for install script to finish, so that if there is any error in install_server.sh, +# the finish trap in this file will be able to access its error code. +wait "${install_pid}" diff --git a/src/server_manager/install_scripts/install_server.sh b/src/server_manager/install_scripts/install_server.sh new file mode 100755 index 000000000..146c0b429 --- /dev/null +++ b/src/server_manager/install_scripts/install_server.sh @@ -0,0 +1,566 @@ +#!/bin/bash +# +# Copyright 2018 The Outline Authors +# +# 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. + +# Script to install the Outline Server docker container, a watchtower docker container +# (to automatically update the server), and to create a new Outline user. + +# You may set the following environment variables, overriding their defaults: +# SB_IMAGE: The Outline Server Docker image to install, e.g. quay.io/outline/shadowbox:nightly +# CONTAINER_NAME: Docker instance name for shadowbox (default shadowbox). +# For multiple instances also change SHADOWBOX_DIR to an other location +# e.g. CONTAINER_NAME=shadowbox-inst1 SHADOWBOX_DIR=/opt/outline/inst1 +# SHADOWBOX_DIR: Directory for persistent Outline Server state. +# ACCESS_CONFIG: The location of the access config text file. +# SB_DEFAULT_SERVER_NAME: Default name for this server, e.g. "Outline server New York". +# This name will be used for the server until the admins updates the name +# via the REST API. +# SENTRY_LOG_FILE: File for writing logs which may be reported to Sentry, in case +# of an install error. No PII should be written to this file. Intended to be set +# only by do_install_server.sh. +# WATCHTOWER_REFRESH_SECONDS: refresh interval in seconds to check for updates, +# defaults to 3600. +# +# Deprecated: +# SB_PUBLIC_IP: Use the --hostname flag instead +# SB_API_PORT: Use the --api-port flag instead + +# Requires curl and docker to be installed + +set -euo pipefail + +function display_usage() { + cat <] [--api-port ] [--keys-port ] + + --hostname The hostname to be used to access the management API and access keys + --api-port The port number for the management API + --keys-port The port number for the access keys +EOF +} + +readonly SENTRY_LOG_FILE=${SENTRY_LOG_FILE:-} + +# I/O conventions for this script: +# - Ordinary status messages are printed to STDOUT +# - STDERR is only used in the event of a fatal error +# - Detailed logs are recorded to this FULL_LOG, which is preserved if an error occurred. +# - The most recent error is stored in LAST_ERROR, which is never preserved. +FULL_LOG="$(mktemp -t outline_logXXXXXXXXXX)" +LAST_ERROR="$(mktemp -t outline_last_errorXXXXXXXXXX)" +readonly FULL_LOG LAST_ERROR + +function log_command() { + # Direct STDOUT and STDERR to FULL_LOG, and forward STDOUT. + # The most recent STDERR output will also be stored in LAST_ERROR. + "$@" > >(tee -a "${FULL_LOG}") 2> >(tee -a "${FULL_LOG}" > "${LAST_ERROR}") +} + +function log_error() { + local -r ERROR_TEXT="\033[0;31m" # red + local -r NO_COLOR="\033[0m" + echo -e "${ERROR_TEXT}$1${NO_COLOR}" + echo "$1" >> "${FULL_LOG}" +} + +# Pretty prints text to stdout, and also writes to sentry log file if set. +function log_start_step() { + log_for_sentry "$@" + local -r str="> $*" + local -ir lineLength=47 + echo -n "${str}" + local -ir numDots=$(( lineLength - ${#str} - 1 )) + if (( numDots > 0 )); then + echo -n " " + for _ in $(seq 1 "${numDots}"); do echo -n .; done + fi + echo -n " " +} + +# Prints $1 as the step name and runs the remainder as a command. +# STDOUT will be forwarded. STDERR will be logged silently, and +# revealed only in the event of a fatal error. +function run_step() { + local -r msg="$1" + log_start_step "${msg}" + shift 1 + if log_command "$@"; then + echo "OK" + else + # Propagates the error code + return + fi +} + +function confirm() { + echo -n "> $1 [Y/n] " + local RESPONSE + read -r RESPONSE + RESPONSE=$(echo "${RESPONSE}" | tr '[:upper:]' '[:lower:]') || return + [[ -z "${RESPONSE}" || "${RESPONSE}" == "y" || "${RESPONSE}" == "yes" ]] +} + +function command_exists { + command -v "$@" &> /dev/null +} + +function log_for_sentry() { + if [[ -n "${SENTRY_LOG_FILE}" ]]; then + echo "[$(date "+%Y-%m-%d@%H:%M:%S")] install_server.sh" "$@" >> "${SENTRY_LOG_FILE}" + fi + echo "$@" >> "${FULL_LOG}" +} + +# Check to see if docker is installed. +function verify_docker_installed() { + if command_exists docker; then + return 0 + fi + log_error "NOT INSTALLED" + if ! confirm "Would you like to install Docker? This will run 'curl https://get.docker.com/ | sh'."; then + exit 0 + fi + if ! run_step "Installing Docker" install_docker; then + log_error "Docker installation failed, please visit https://docs.docker.com/install for instructions." + exit 1 + fi + log_start_step "Verifying Docker installation" + command_exists docker +} + +function verify_docker_running() { + local STDERR_OUTPUT + STDERR_OUTPUT="$(docker info 2>&1 >/dev/null)" + local -ir RET=$? + if (( RET == 0 )); then + return 0 + elif [[ "${STDERR_OUTPUT}" == *"Is the docker daemon running"* ]]; then + start_docker + return + fi + return "${RET}" +} + +function fetch() { + curl --silent --show-error --fail "$@" +} + +function install_docker() { + ( + # Change umask so that /usr/share/keyrings/docker-archive-keyring.gpg has the right permissions. + # See https://github.com/Jigsaw-Code/outline-server/issues/951. + # We do this in a subprocess so the umask for the calling process is unaffected. + umask 0022 + fetch https://get.docker.com/ | sh + ) >&2 +} + +function start_docker() { + systemctl enable --now docker.service >&2 +} + +function docker_container_exists() { + docker ps -a --format '{{.Names}}'| grep --quiet "^$1$" +} + +function remove_shadowbox_container() { + remove_docker_container "${CONTAINER_NAME}" +} + +function remove_watchtower_container() { + remove_docker_container watchtower +} + +function remove_docker_container() { + docker rm -f "$1" >&2 +} + +function handle_docker_container_conflict() { + local -r CONTAINER_NAME="$1" + local -r EXIT_ON_NEGATIVE_USER_RESPONSE="$2" + local PROMPT="The container name \"${CONTAINER_NAME}\" is already in use by another container. This may happen when running this script multiple times." + if [[ "${EXIT_ON_NEGATIVE_USER_RESPONSE}" == 'true' ]]; then + PROMPT="${PROMPT} We will attempt to remove the existing container and restart it. Would you like to proceed?" + else + PROMPT="${PROMPT} Would you like to replace this container? If you answer no, we will proceed with the remainder of the installation." + fi + if ! confirm "${PROMPT}"; then + if ${EXIT_ON_NEGATIVE_USER_RESPONSE}; then + exit 0 + fi + return 0 + fi + if run_step "Removing ${CONTAINER_NAME} container" "remove_${CONTAINER_NAME}_container" ; then + log_start_step "Restarting ${CONTAINER_NAME}" + "start_${CONTAINER_NAME}" + return $? + fi + return 1 +} + +# Set trap which publishes error tag only if there is an error. +function finish { + local -ir EXIT_CODE=$? + if (( EXIT_CODE != 0 )); then + if [[ -s "${LAST_ERROR}" ]]; then + log_error "\nLast error: $(< "${LAST_ERROR}")" >&2 + fi + log_error "\nSorry! Something went wrong. If you can't figure this out, please copy and paste all this output into the Outline Manager screen, and send it to us, to see if we can help you." >&2 + log_error "Full log: ${FULL_LOG}" >&2 + else + rm "${FULL_LOG}" + fi + rm "${LAST_ERROR}" +} + +function get_random_port { + local -i num=0 # Init to an invalid value, to prevent "unbound variable" errors. + until (( 1024 <= num && num < 65536)); do + num=$(( RANDOM + (RANDOM % 2) * 32768 )); + done; + echo "${num}"; +} + +function create_persisted_state_dir() { + readonly STATE_DIR="${SHADOWBOX_DIR}/persisted-state" + mkdir -p "${STATE_DIR}" + chmod ug+rwx,g+s,o-rwx "${STATE_DIR}" +} + +# Generate a secret key for access to the Management API and store it in a tag. +# 16 bytes = 128 bits of entropy should be plenty for this use. +function safe_base64() { + # Implements URL-safe base64 of stdin, stripping trailing = chars. + # Writes result to stdout. + # TODO: this gives the following errors on Mac: + # base64: invalid option -- w + # tr: illegal option -- - + local url_safe + url_safe="$(base64 -w 0 - | tr '/+' '_-')" + echo -n "${url_safe%%=*}" # Strip trailing = chars +} + +function generate_secret_key() { + SB_API_PREFIX="$(head -c 16 /dev/urandom | safe_base64)" + readonly SB_API_PREFIX +} + +function generate_certificate() { + # Generate self-signed cert and store it in the persistent state directory. + local -r CERTIFICATE_NAME="${STATE_DIR}/shadowbox-selfsigned" + readonly SB_CERTIFICATE_FILE="${CERTIFICATE_NAME}.crt" + readonly SB_PRIVATE_KEY_FILE="${CERTIFICATE_NAME}.key" + declare -a openssl_req_flags=( + -x509 -nodes -days 36500 -newkey rsa:4096 + -subj "/CN=${PUBLIC_HOSTNAME}" + -keyout "${SB_PRIVATE_KEY_FILE}" -out "${SB_CERTIFICATE_FILE}" + ) + openssl req "${openssl_req_flags[@]}" >&2 +} + +function generate_certificate_fingerprint() { + # Add a tag with the SHA-256 fingerprint of the certificate. + # (Electron uses SHA-256 fingerprints: https://github.com/electron/electron/blob/9624bc140353b3771bd07c55371f6db65fd1b67e/atom/common/native_mate_converters/net_converter.cc#L60) + # Example format: "SHA256 Fingerprint=BD:DB:C9:A4:39:5C:B3:4E:6E:CF:18:43:61:9F:07:A2:09:07:37:35:63:67" + local CERT_OPENSSL_FINGERPRINT + CERT_OPENSSL_FINGERPRINT="$(openssl x509 -in "${SB_CERTIFICATE_FILE}" -noout -sha256 -fingerprint)" || return + # Example format: "BDDBC9A4395CB34E6ECF1843619F07A2090737356367" + local CERT_HEX_FINGERPRINT + CERT_HEX_FINGERPRINT="$(echo "${CERT_OPENSSL_FINGERPRINT#*=}" | tr -d :)" || return + output_config "certSha256:${CERT_HEX_FINGERPRINT}" +} + +function join() { + local IFS="$1" + shift + echo "$*" +} + +function write_config() { + local -a config=() + if (( FLAGS_KEYS_PORT != 0 )); then + config+=("\"portForNewAccessKeys\": ${FLAGS_KEYS_PORT}") + fi + # printf is needed to escape the hostname. + config+=("$(printf '"hostname": "%q"' "${PUBLIC_HOSTNAME}")") + echo "{$(join , "${config[@]}")}" > "${STATE_DIR}/shadowbox_server_config.json" +} + +function start_shadowbox() { + # TODO(fortuna): Write API_PORT to config file, + # rather than pass in the environment. + local -ar docker_shadowbox_flags=( + --name "${CONTAINER_NAME}" --restart always --net host + --label 'com.centurylinklabs.watchtower.enable=true' + -v "${STATE_DIR}:${STATE_DIR}" + -e "SB_STATE_DIR=${STATE_DIR}" + -e "SB_API_PORT=${API_PORT}" + -e "SB_API_PREFIX=${SB_API_PREFIX}" + -e "SB_CERTIFICATE_FILE=${SB_CERTIFICATE_FILE}" + -e "SB_PRIVATE_KEY_FILE=${SB_PRIVATE_KEY_FILE}" + -e "SB_METRICS_URL=${SB_METRICS_URL:-}" + -e "SB_DEFAULT_SERVER_NAME=${SB_DEFAULT_SERVER_NAME:-}" + ) + # By itself, local messes up the return code. + local STDERR_OUTPUT + STDERR_OUTPUT="$(docker run -d "${docker_shadowbox_flags[@]}" "${SB_IMAGE}" 2>&1 >/dev/null)" && return + readonly STDERR_OUTPUT + log_error "FAILED" + if docker_container_exists "${CONTAINER_NAME}"; then + handle_docker_container_conflict "${CONTAINER_NAME}" true + return + else + log_error "${STDERR_OUTPUT}" + return 1 + fi +} + +function start_watchtower() { + # Start watchtower to automatically fetch docker image updates. + # Set watchtower to refresh every 30 seconds if a custom SB_IMAGE is used (for + # testing). Otherwise refresh every hour. + local -ir WATCHTOWER_REFRESH_SECONDS="${WATCHTOWER_REFRESH_SECONDS:-3600}" + local -ar docker_watchtower_flags=(--name watchtower --restart always \ + -v /var/run/docker.sock:/var/run/docker.sock) + # By itself, local messes up the return code. + local STDERR_OUTPUT + STDERR_OUTPUT="$(docker run -d "${docker_watchtower_flags[@]}" containrrr/watchtower --cleanup --label-enable --tlsverify --interval "${WATCHTOWER_REFRESH_SECONDS}" 2>&1 >/dev/null)" && return + readonly STDERR_OUTPUT + log_error "FAILED" + if docker_container_exists watchtower; then + handle_docker_container_conflict watchtower false + return + else + log_error "${STDERR_OUTPUT}" + return 1 + fi +} + +# Waits for the service to be up and healthy +function wait_shadowbox() { + # We use insecure connection because our threat model doesn't include localhost port + # interception and our certificate doesn't have localhost as a subject alternative name + until fetch --insecure "${LOCAL_API_URL}/access-keys" >/dev/null; do sleep 1; done +} + +function create_first_user() { + fetch --insecure --request POST "${LOCAL_API_URL}/access-keys" >&2 +} + +function output_config() { + echo "$@" >> "${ACCESS_CONFIG}" +} + +function add_api_url_to_config() { + output_config "apiUrl:${PUBLIC_API_URL}" +} + +function check_firewall() { + # TODO(JonathanDCohen) This is incorrect if access keys are using more than one port. + local -i ACCESS_KEY_PORT + ACCESS_KEY_PORT=$(fetch --insecure "${LOCAL_API_URL}/access-keys" | + docker exec -i "${CONTAINER_NAME}" node -e ' + const fs = require("fs"); + const accessKeys = JSON.parse(fs.readFileSync(0, {encoding: "utf-8"})); + console.log(accessKeys["accessKeys"][0]["port"]); + ') || return + readonly ACCESS_KEY_PORT + if ! fetch --max-time 5 --cacert "${SB_CERTIFICATE_FILE}" "${PUBLIC_API_URL}/access-keys" >/dev/null; then + log_error "BLOCKED" + FIREWALL_STATUS="\ +You won’t be able to access it externally, despite your server being correctly +set up, because there's a firewall (in this machine, your router or cloud +provider) that is preventing incoming connections to ports ${API_PORT} and ${ACCESS_KEY_PORT}." + else + FIREWALL_STATUS="\ +If you have connection problems, it may be that your router or cloud provider +blocks inbound connections, even though your machine seems to allow them." + fi + FIREWALL_STATUS="\ +${FIREWALL_STATUS} + +Make sure to open the following ports on your firewall, router or cloud provider: +- Management port ${API_PORT}, for TCP +- Access key port ${ACCESS_KEY_PORT}, for TCP and UDP +" +} + +function set_hostname() { + # These are URLs that return the client's apparent IP address. + # We have more than one to try in case one starts failing + # (e.g. https://github.com/Jigsaw-Code/outline-server/issues/776). + local -ar urls=( + 'https://icanhazip.com/' + 'https://ipinfo.io/ip' + 'https://domains.google.com/checkip' + ) + for url in "${urls[@]}"; do + PUBLIC_HOSTNAME="$(fetch --ipv4 "${url}")" && return + done + echo "Failed to determine the server's IP address. Try using --hostname ." >&2 + return 1 +} + +install_shadowbox() { + local MACHINE_TYPE + MACHINE_TYPE="$(uname -m)" + if [[ "${MACHINE_TYPE}" != "x86_64" ]]; then + log_error "Unsupported machine type: ${MACHINE_TYPE}. Please run this script on a x86_64 machine" + exit 1 + fi + + # Make sure we don't leak readable files to other users. + umask 0007 + + export CONTAINER_NAME="${CONTAINER_NAME:-shadowbox}" + + run_step "Verifying that Docker is installed" verify_docker_installed + run_step "Verifying that Docker daemon is running" verify_docker_running + + log_for_sentry "Creating Outline directory" + export SHADOWBOX_DIR="${SHADOWBOX_DIR:-/opt/outline}" + mkdir -p "${SHADOWBOX_DIR}" + chmod u+s,ug+rwx,o-rwx "${SHADOWBOX_DIR}" + + log_for_sentry "Setting API port" + API_PORT="${FLAGS_API_PORT}" + if (( API_PORT == 0 )); then + API_PORT=${SB_API_PORT:-$(get_random_port)} + fi + readonly API_PORT + readonly ACCESS_CONFIG="${ACCESS_CONFIG:-${SHADOWBOX_DIR}/access.txt}" + readonly SB_IMAGE="${SB_IMAGE:-quay.io/outline/shadowbox:stable}" + + PUBLIC_HOSTNAME="${FLAGS_HOSTNAME:-${SB_PUBLIC_IP:-}}" + if [[ -z "${PUBLIC_HOSTNAME}" ]]; then + run_step "Setting PUBLIC_HOSTNAME to external IP" set_hostname + fi + readonly PUBLIC_HOSTNAME + + # If $ACCESS_CONFIG is already populated, make a backup before clearing it. + log_for_sentry "Initializing ACCESS_CONFIG" + if [[ -s "${ACCESS_CONFIG}" ]]; then + # Note we can't do "mv" here as do_install_server.sh may already be tailing + # this file. + cp "${ACCESS_CONFIG}" "${ACCESS_CONFIG}.bak" && true > "${ACCESS_CONFIG}" + fi + + # Make a directory for persistent state + run_step "Creating persistent state dir" create_persisted_state_dir + run_step "Generating secret key" generate_secret_key + run_step "Generating TLS certificate" generate_certificate + run_step "Generating SHA-256 certificate fingerprint" generate_certificate_fingerprint + run_step "Writing config" write_config + + # TODO(dborkan): if the script fails after docker run, it will continue to fail + # as the names shadowbox and watchtower will already be in use. Consider + # deleting the container in the case of failure (e.g. using a trap, or + # deleting existing containers on each run). + run_step "Starting Shadowbox" start_shadowbox + # TODO(fortuna): Don't wait for Shadowbox to run this. + run_step "Starting Watchtower" start_watchtower + + readonly PUBLIC_API_URL="https://${PUBLIC_HOSTNAME}:${API_PORT}/${SB_API_PREFIX}" + readonly LOCAL_API_URL="https://localhost:${API_PORT}/${SB_API_PREFIX}" + run_step "Waiting for Outline server to be healthy" wait_shadowbox + run_step "Creating first user" create_first_user + run_step "Adding API URL to config" add_api_url_to_config + + FIREWALL_STATUS="" + run_step "Checking host firewall" check_firewall + + # Echos the value of the specified field from ACCESS_CONFIG. + # e.g. if ACCESS_CONFIG contains the line "certSha256:1234", + # calling $(get_field_value certSha256) will echo 1234. + function get_field_value { + grep "$1" "${ACCESS_CONFIG}" | sed "s/$1://" + } + + # Output JSON. This relies on apiUrl and certSha256 (hex characters) requiring + # no string escaping. TODO: look for a way to generate JSON that doesn't + # require new dependencies. + cat < 0 )); do + local flag="$1" + shift + case "${flag}" in + --hostname) + FLAGS_HOSTNAME="$1" + shift + ;; + --api-port) + FLAGS_API_PORT=$1 + shift + if ! is_valid_port "${FLAGS_API_PORT}"; then + log_error "Invalid value for ${flag}: ${FLAGS_API_PORT}" >&2 + exit 1 + fi + ;; + --keys-port) + FLAGS_KEYS_PORT=$1 + shift + if ! is_valid_port "${FLAGS_KEYS_PORT}"; then + log_error "Invalid value for ${flag}: ${FLAGS_KEYS_PORT}" >&2 + exit 1 + fi + ;; + --) + break + ;; + *) # This should not happen + log_error "Unsupported flag ${flag}" >&2 + display_usage >&2 + exit 1 + ;; + esac + done + if (( FLAGS_API_PORT != 0 && FLAGS_API_PORT == FLAGS_KEYS_PORT )); then + log_error "--api-port must be different from --keys-port" >&2 + exit 1 + fi + return 0 +} + +function main() { + trap finish EXIT + declare FLAGS_HOSTNAME="" + declare -i FLAGS_API_PORT=0 + declare -i FLAGS_KEYS_PORT=0 + parse_flags "$@" + install_shadowbox +} + +main "$@" diff --git a/src/server_manager/messages/en.json b/src/server_manager/messages/en.json new file mode 100644 index 000000000..ce1352f9d --- /dev/null +++ b/src/server_manager/messages/en.json @@ -0,0 +1,279 @@ +{ + "about-outline": "Outline is an open source project created by Jigsaw to provide a safer way for news organizations and journalists to access the internet.

Outline is powered by Shadowsocks and is still an early stage product. You can contribute to the code on GitHub, and follow us on Reddit and Medium to hear when we expand to more platforms and add new features.", + "about-version": "Version {version}", + "aws-lightsail-firewall-0": "Navigate to the {openLink}Amazon Lightsail{closeLink} instances screen.", + "aws-lightsail-firewall-1": "Click the instance on which you want to host Outline.", + "aws-lightsail-firewall-2": "Navigate to the 'Networking' tab.", + "aws-lightsail-firewall-3": "In the 'Firewall' section, click 'Add another'.", + "aws-lightsail-firewall-4": "Set 'Application' value to 'All TCP+UDP'.", + "aws-lightsail-firewall-5": "Click 'Save'.", + "cancel": "Cancel", + "geo-amsterdam": "Amsterdam", + "geo-bangalore": "Bangalore", + "geo-changhua-county": "Changhua County", + "geo-delhi": "Delhi", + "geo-eemshaven": "Eemshaven", + "geo-frankfurt": "Frankfurt", + "geo-hamina": "Hamina", + "geo-hk": "Hong Kong", + "geo-iowa": "Iowa", + "geo-jakarta": "Jakarta", + "geo-jurong-west": "Jurong West", + "geo-las-vegas": "Las Vegas", + "geo-london": "London", + "geo-los-angeles": "Los Angeles", + "geo-melbourne": "Melbourne", + "geo-montreal": "Montréal", + "geo-mumbai": "Mumbai", + "geo-new-york-city": "New York", + "geo-northern-virginia": "Northern Virginia", + "geo-oregon": "Oregon", + "geo-osaka": "Osaka", + "geo-salt-lake-city": "Salt Lake City", + "geo-san-francisco": "San Francisco", + "geo-sao-paulo": "São Paulo", + "geo-seoul": "Seoul", + "geo-sg": "Singapore", + "geo-south-carolina": "South Carolina", + "geo-st-ghislain": "St. Ghislain", + "geo-sydney": "Sydney", + "geo-tokyo": "Tokyo", + "geo-toronto": "Toronto", + "geo-warsaw": "Warsaw", + "geo-zurich": "Zürich", + "close": "Close", + "confirmation-server-destroy": "Existing users will lose access. This action cannot be undone.", + "confirmation-server-destroy-title": "Destroy Server?", + "confirmation-server-remove": "This action removes your server from the Outline Manager, but does not block proxy access to users. You will still need to manually delete the Outline server from your host machine.", + "confirmation-server-remove-title": "Remove Server?", + "data-limit": "Data Limit", + "data-limit-per-key": "Data limit per key", + "data-limits": "Data limits", + "data-limits-description": "Set a 30 day trailing data transfer limit for access keys on this server.", + "data-limits-dialog-text": "Go to the Settings tab to set a data transfer limit for access keys on this server.", + "data-limits-dialog-title": "Avoid data overages", + "data-limits-disclaimer": "Since you are currently reporting metrics, use of the data limits feature will be included. Please see the {openLink}data collection policy{closeLink} for more details.", + "data-limits-usage": "{used} of {total} used", + "destroy": "Destroy", + "disconnect": "Disconnect", + "digitalocean-disconnect-account": "Disconnect DigitalOcean account", + "digitalocean-unreachable": "This error may be due to a firewall on your network or temporary connectivity issues with digitalocean.com.", + "disabled": "Disabled", + "done": "Done", + "enabled": "Enabled", + "error-connectivity": "We're having trouble connecting to your DigitalOcean account. This is sometimes a temporary problem with DigitalOcean or with your internet connection. If retrying doesn't work, logging into DigitalOcean again should fix the problem.", + "error-connectivity-title": "Connection problem", + "error-do-account-info": "Failed to get DigitalOcean account information", + "error-do-auth": "Authentication with DigitalOcean failed", + "error-do-regions": "Failed to get list of available regions", + "error-do-limit": "Your DigitalOcean account has reached its limit of {num} Droplets. You can request an increase at https://cloud.digitalocean.com/account/team/droplet_limit_increase", + "error-do-warning": "DigitalOcean warning: \"{message}\"", + "error-gcp-auth": "Authentication with Google Cloud Platform failed", + "error-feedback": "Failed to submit feedback. Please try again.", + "error-hostname-invalid": "Must be an IP address or valid hostname.", + "error-key-add": "Failed to add key", + "error-key-remove": "Failed to remove key", + "error-key-rename": "Failed to rename key", + "error-keys-get": "Could not load keys", + "error-keys-port-bad-input": "The port must be an integer between 1 and 65,535.", + "error-keys-port-in-use": "The port is already in use on the server.", + "error-licenses": "Could not load licenses.", + "error-metrics": "Error setting metrics enabled", + "error-network": "A network error occurred.", + "error-not-saved": "Not Saved", + "error-remove-data-limit": "Could not disable default data limit", + "error-remove-per-key-limit": "Could not remove data limit from this access key", + "error-server-creation": "There was an error creating your Outline server.", + "error-server-destroy": "Failed to destroy server", + "error-server-removed": "{serverName} no longer present in your DigitalOcean account.", + "error-server-rename": "Failed to rename server", + "error-server-unreachable": "Your Outline Server was installed correctly, but we are not able to connect to it. Most likely this is because your server's firewall rules are blocking incoming connections. Please review them and make sure to allow incoming TCP connections on ports ranging from 1024 to 65535.", + "error-server-unreachable-title": "Unable to connect to your Outline Server", + "error-servers-removed": "{serverNames} no longer present in your DigitalOcean account.", + "error-set-data-limit": "Could not set default data limit", + "error-set-per-key-limit": "Could not set data limit for this access key", + "error-unexpected": "An unexpected error occurred.", + "experimental": "Experimental", + "experiments": "Experiments", + "experiments-description": "Test new features and provide us with feedback before they are released.", + "experiments-disclaimer": "Experiments are in development and may change or be removed from the app. If you are currently reporting metrics, use of experimental features will be included. Please see the {openLink}data collection policy{closeLink} for more details.", + "experiments-feedback": "Have suggestions? {openLink}Submit feedback here.{closeLink}", + "feedback-cloud-provider": "Select cloud provider", + "feedback-cloud-provider-error": "Please select a cloud provider.", + "feedback-connection": "Can't connect to my server", + "feedback-connection-others": "Others can't connect to my server", + "feedback-disclaimer": "Please note that our team is only able to answer feedback in English.", + "feedback-email": "Email address (optional)", + "feedback-error": "Please enter feedback.", + "feedback-explanation-install": "An error occurred while attempting to install Outline on your server. If you haven't been able to figure out a solution, please consider sending us feedback and telling us your email address (optional) so that we can get back to you.", + "feedback-general": "General feedback", + "feedback-install": "Having trouble installing Outline", + "feedback-label": "Your feedback", + "feedback-management": "Having trouble managing my server", + "feedback-other": "Other", + "feedback-privacy": "Your feedback, email address (if provided) and additional information referred to in the {openLink}privacy policy{closeLink} will be sent to the Outline team.", + "feedback-submit": "Submit", + "feedback-suggestion": "Suggestions", + "feedback-title-generic": "Send Feedback", + "feedback-title-install": "Outline Server Installation Failed", + "gcp-billing-title": "Add billing information to your Google Cloud Platform account.", + "gcp-billing-description": "{openLink}Open the Cloud Console billing page{closeLink} and add an account in order to proceed.", + "gcp-billing-action": "Next", + "gcp-billing-body": "Waiting for you to {openLink}add a billing account on Google Cloud{closeLink}", + "gcp-billing-error": "Unable to retrieve billing information", + "gcp-billing-error-zero": "You must add a billing account before proceeding.", + "gcp-click-create": "Click 'Create'.", + "gcp-create-new-project": "{openLink}Create a new Google Cloud Project{closeLink}.", + "gcp-create-new-vm": "{openLink}Create a new VM instance{closeLink}.", + "gcp-create-project": "Create a Google Cloud project", + "gcp-create-server": "Create your server", + "gcp-create-vm": "Create a VM instance", + "gcp-disconnect-account": "Disconnect Google Cloud Platform account", + "gcp-firewall-create-0": "{openLink}Add a new firewall rule{closeLink} to your Compute Engine project.", + "gcp-firewall-create-1": "Type 'outline' in the 'Name' field.", + "gcp-firewall-create-2": "Type 'outline' in the 'Target tags' field.", + "gcp-firewall-create-3": "Type '0.0.0.0/0' in the 'Source IP ranges' field.", + "gcp-firewall-create-4": "Select 'Allow all' under 'Protocols and ports'.", + "gcp-oauth-connect-title": "Sign in or create an account with Google Cloud Platform.", + "gcp-project-setup-error": "An error occurred while setting up your Google Cloud project", + "gcp-name-your-project": "Name your project in the 'Project name' field.", + "gcp-select-machine-type": "Select 'f1-micro' under 'Machine type'.", + "gcp-select-networking": "Click 'Management, security, disks, networking, sole tenancy', then 'Networking'", + "gcp-select-region": "Select a region close to where the server's users will be under 'Region'.", + "gcp-type-network-tag": "Type 'outline' in the 'Network tags' field.", + "gcp-type-outline-server": "Type 'outline-server' in the 'Name' field.", + "key": "Key {keyId}", + "manager-resources": "Manager Resources", + "manual-server-assign-firewall": "Assign firewall rule", + "manual-server-assign-group": "Assign Security Group", + "manual-server-create-firewall": "Create a firewall rule", + "manual-server-create-group": "Create a Security Group", + "manual-server-description": "These steps will help you install Outline on a {cloudProvider} Linux server.", + "manual-server-firewall": "Configure your firewall", + "manual-server-install-paste": "Paste your installation output here.", + "manual-server-install-run": "Log into your server, and run this command.", + "manual-server-install-step": "Install Outline on to your VM", + "manual-server-instructions": "Instructions", + "manual-server-show-me": "Show me where", + "manual-server-title": "Follow the instructions below", + "metrics-description": "Share anonymized metrics to help improve the reliability and performance of Outline, for you and for those you share your server with. {openLink}Learn more.{closeLink}", + "metrics-share": "Share metrics", + "metrics-skip": "Skip", + "metrics-title": "Metrics sharing", + "nav-about": "About", + "nav-data-collection": "Data collection", + "nav-feedback": "Feedback", + "nav-help": "Help", + "nav-licenses": "Licenses", + "nav-privacy": "Privacy", + "nav-terms": "Terms", + "no-data-limit": "None", + "notification-app-update": "An updated version of the Outline Manager has been downloaded. It will be installed when you restart the application.", + "notification-feedback-thanks": "Thanks for helping us improve! We love hearing from you.", + "notification-key-added": "Key added", + "notification-key-removed": "Key removed", + "notification-server-destroyed": "Server destroyed", + "notification-server-exists": "Server already added", + "notification-server-removed": "Server removed", + "oauth-account-active": "Your DigitalOcean account has been activated.", + "oauth-account-active-tag": "Account activated! Loading server locations...", + "oauth-activate-account": "Activate your DigitalOcean account.", + "oauth-billing": "Enter your billing information on digitalocean.com and return to the app once you are done.", + "oauth-billing-tag": "Enter billing information...", + "oauth-connect-description": "With your account, Outline makes it easy to create a server and get connected.", + "oauth-connect-tag": "Waiting to connect your account...", + "oauth-connect-title": "Sign in or create an account with DigitalOcean.", + "oauth-sign-out": "Sign Out", + "oauth-verify": "Check your inbox for an email from DigitalOcean, and click the link in it to confirm your account.", + "oauth-verify-tag": "Confirm your email...", + "okay": "OK", + "per-key-data-limit-dialog-set-custom": "Set a custom data limit", + "per-key-data-limit-dialog-title": "Data Limit - {keyName}", + "region-description": "This is where your internet experience will come from.", + "region-best-value": "Best Value", + "region-setup": "Set up Outline", + "region-title": "Select the location of your server.", + "remove": "Remove", + "retry": "Retry", + "save": "Save", + "saved": "Saved", + "saving": "Saving...", + "server-access": "Server access", + "server-access-key-new": "Add new key", + "server-access-key-rename": "Rename", + "server-access-keys": "Access keys", + "server-connections": "Connections", + "server-data-transfer": "Data transferred / last 30 days", + "server-data-used": "Allowance used / last 30 days", + "server-destroy": "Destroy server", + "server-help-access-key-description": "Share access keys with friends, so they can connect to your Outline server. They can use the same access key on all their devices.", + "server-help-access-key-next": "Next", + "server-help-access-key-title": "Create keys, share access", + "server-help-connection-description": "Click here to install the Outline client app, using your personal access key to your Outline server.", + "server-help-connection-ok": "Okay, got it!", + "server-help-connection-title": "You are not connected yet!", + "server-keys": "Keys", + "server-my-access-key": "My access key", + "server-name": "Outline Server {serverLocation}", + "server-remove": "Remove server", + "server-settings": "Settings", + "server-unreachable": "Server unreachable", + "server-unreachable-description": "We're having issues connecting to this server.", + "server-unreachable-managed-description": "Try again or remove this server from the application.", + "server-unreachable-manual-description": "Try again or destroy this server and the virtual host.", + "server-usage": "Usage (last 30 days)", + "servers-add": "Add server", + "servers-digitalocean": "DigitalOcean servers", + "servers-gcp": "Google Cloud Platform servers", + "servers-manual": "Servers", + "settings-access-key-port": "Port for new access keys", + "settings-metrics-header": "Share anonymous metrics", + "settings-server-api-url": "Management API URL", + "settings-server-cost": "Monthly cost", + "settings-server-creation": "Created", + "settings-server-hostname": "Hostname", + "settings-server-id": "Server ID", + "settings-server-info": "Server Information", + "settings-server-location": "Server location", + "settings-server-name": "Name", + "settings-server-rename": "Set a new name for your server. Note that this will not be reflected on the devices of the users that you invited to connect to it.", + "settings-server-version": "Server version", + "settings-transfer-limit": "Data transfer allowance", + "setup-action": "Set up", + "setup-advanced": "Advanced", + "setup-anywhere": "Set up Outline anywhere", + "setup-create": "Create server", + "setup-description": "Don't have a server? Create an account with DigitalOcean.", + "setup-cancel": "Cancel at any time", + "setup-do-cost": "Only US$6 a month", + "setup-do-create": "Create a new server with your DigitalOcean account for an additional US$6/30 days for 1 TB of data transfer.", + "setup-do-data": "1 TB data transfer allowance", + "setup-do-description": "This could take several minutes. You can destroy this server at any time.", + "setup-do-easiest": "Easiest setup process", + "setup-do-title": "Setting up Outline.", + "setup-firewall-instructions": "Firewall instructions", + "setup-gcp-easy": "Easy setup process", + "setup-gcp-free-tier": "With {openLinkFreeTier}Free Tier{closeLink}, your first server starts at {openLinkIpPrice}US$3/month{closeLink}", + "setup-gcp-free-trial": "{openLinkFreeTrial}90 day free trial{closeLink} for new users", + "setup-gcp-create": "Create a new server with your Google account. Costs vary by location and usage.", + "setup-gcp-promo": "Try the new automatic Outline server creation process for Google Cloud", + "setup-recommended": "Recommended", + "setup-simple-commands": "Simple install commands", + "setup-step-by-step": "Step-by-step set-up guide", + "setup-tested": "Tested on VULTR, Linode, and Liquid Web", + "setup-title": "Choose a cloud service to set up Outline.", + "share-description": "Copy this invitation and send it from a communication tool you trust. {openLink}Need help?{closeLink}", + "share-invite-access-key-copied": "Copied access key to clipboard", + "share-invite-html": "Use this server to safely access the open internet:

1) Download and install the Outline app for your device:

- iOS: https://itunes.apple.com/app/outline-app/id1356177741
- MacOS: https://itunes.apple.com/app/outline-app/id1356178125
- Windows: https://s3.amazonaws.com/outline-releases/client/windows/stable/Outline-Client.exe
- Linux: https://s3.amazonaws.com/outline-releases/client/linux/stable/Outline-Client.AppImage
- Android: https://play.google.com/store/apps/details?id=org.outline.android.client
- Android alternative link: https://s3.amazonaws.com/outline-releases/client/android/stable/Outline-Client.apk

2) You will receive an access key that starts with ss://. Once your receive it, copy this access key.

3) Open the Outline client app. If your access key is auto-detected, tap \"Connect\" and proceed. If your access key is not auto-detected, then paste it in the field, then tap \"Connect\" and proceed.

You're ready to use the open internet! To make sure you successfully connected to the server, try searching for \"what is my ip\" on Google Search. The IP address shown in Google should match the IP address in the Outline client.

Learn more about Outline here: https://getoutline.org/", + "share-invite-copied": "Copied invitation to clipboard", + "share-invite-copy": "Copy invitation", + "share-invite-copy-access-key": "Copy access key", + "share-invite-instructions": "Follow our invitation instructions on GitHub:", + "share-invite-trouble": "Having trouble accessing the invitation link?", + "share-title": "Share access", + "survey-data-limits-title": "Help us understand how to improve data limits", + "survey-decline": "Decline", + "survey-disclaimer": "By clicking continue you will be sent to a short survey on Google Forms. We recommend taking the survey while connected to Outline.", + "survey-go-to-survey": "Go to survey", + "terms-of-service": "I have read and understood the {openLink}Outline Terms of Service{closeLink}" +} diff --git a/src/server_manager/messages/master_messages.json b/src/server_manager/messages/master_messages.json new file mode 100644 index 000000000..e36880c6b --- /dev/null +++ b/src/server_manager/messages/master_messages.json @@ -0,0 +1,1311 @@ +{ + "about_outline": { + "message": "Outline is an open source project created by $JIGSAW$ to provide a safer way for news organizations and journalists to access the internet.$NEW_LINE$$NEW_LINE$ Outline is powered by $SHADOWSOCKS$ and is still an early stage product. You can contribute to the code on $GITHUB$, and follow us on $REDDIT$ and $MEDIUM$ to hear when we expand to more platforms and add new features.", + "description": "This string appears in a dialog as a paragraph to provide a description of the product. Outline is the product name and should not be translated.", + "placeholders": { + "GITHUB": { + "content": "GitHub" + }, + "JIGSAW": { + "content": "Jigsaw" + }, + "MEDIUM": { + "content": "Medium" + }, + "NEW_LINE": { + "content": "
" + }, + "REDDIT": { + "content": "Reddit" + }, + "SHADOWSOCKS": { + "content": "Shadowsocks" + } + } + }, + "about_version": { + "message": "Version $VERSION$", + "description": "This string appears in a dialog as a header to indicate the version of the application.", + "placeholders": { + "VERSION": { + "content": "{version}", + "example": "1.0.2" + } + } + }, + "aws_lightsail_firewall_0": { + "message": "Navigate to the $START_OF_LINK$Amazon Lightsail$END_OF_LINK$ instances screen.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure firewall rules in Amazon Lightsail. Amazon Lightsail is a product name and should not be translated.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "aws_lightsail_firewall_1": { + "message": "Click the instance on which you want to host Outline.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure firewall rules in Amazon Lightsail." + }, + "aws_lightsail_firewall_2": { + "message": "Navigate to the 'Networking' tab.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure firewall rules in Amazon Lightsail.. 'Networking' should be translated and included untranslated in parentheses." + }, + "aws_lightsail_firewall_3": { + "message": "In the 'Firewall' section, click 'Add another'.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure firewall rules in Amazon Lightsail. Words in quotes should be translated and included untranslated in parentheses." + }, + "aws_lightsail_firewall_4": { + "message": "Set 'Application' value to 'All TCP+UDP'.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure firewall rules in Amazon Lightsail. Words in quotes should be translated and included untranslated in parentheses." + }, + "aws_lightsail_firewall_5": { + "message": "Click 'Save'.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure firewall rules in Amazon Lightsail. Words in quotes should be translated and included untranslated in parentheses." + }, + "cancel": { + "message": "Cancel", + "description": "This string appears across the application as a button. Clicking it aborts the current flow." + }, + "geo_amsterdam": { + "message": "Amsterdam", + "description": "Name of the city in the Netherlands." + }, + "geo_bangalore": { + "message": "Bangalore", + "description": "Name of the city in India." + }, + "geo_changhua_county": { + "message": "Changhua County", + "description": "Name of the county in Taiwan." + }, + "geo_delhi": { + "message": "Delhi", + "description": "Name of the city in India." + }, + "geo_eemshaven": { + "message": "Eemshaven", + "description": "Name of the seaport in the Netherlands." + }, + "geo_frankfurt": { + "message": "Frankfurt", + "description": "Name of the city in Germany." + }, + "geo_hamina": { + "message": "Hamina", + "description": "Name of the town in Finland." + }, + "geo_hk": { + "message": "Hong Kong", + "description": "Name of Hong Kong in China" + }, + "geo_iowa": { + "message": "Iowa", + "description": "Name of the US state." + }, + "geo_jakarta": { + "message": "Jakarta", + "description": "Name of the capital of Indonesia." + }, + "geo_jurong_west": { + "message": "Jurong West", + "description": "Name of the area in Singapore." + }, + "geo_las_vegas": { + "message": "Las Vegas", + "description": "Name of the city in the Nevada, USA." + }, + "geo_london": { + "message": "London", + "description": "Name of the city in England." + }, + "geo_los_angeles": { + "message": "Los Angeles", + "description": "Name of the city in California, USA." + }, + "geo_melbourne": { + "message": "Melbourne", + "description": "Name of the city in Australia." + }, + "geo_montreal": { + "message": "Montréal", + "description": "Name of the city in Canada." + }, + "geo_mumbai": { + "message": "Mumbai", + "description": "Name of the city in India." + }, + "geo_new_york_city": { + "message": "New York", + "description": "Name of the city in the United States." + }, + "geo_northern_virginia": { + "message": "Northern Virginia", + "description": "Name of the area in Virginia, USA." + }, + "geo_oregon": { + "message": "Oregon", + "description": "Name of the US state." + }, + "geo_osaka": { + "message": "Osaka", + "description": "Name of the city in Japan." + }, + "geo_salt_lake_city": { + "message": "Salt Lake City", + "description": "Name of the city in Utah, USA." + }, + "geo_san_francisco": { + "message": "San Francisco", + "description": "Name of the city in the United States." + }, + "geo_sao_paulo": { + "message": "São Paulo", + "description": "Name of the state in Brazil." + }, + "geo_seoul": { + "message": "Seoul", + "description": "Name of the city in South Korea." + }, + "geo_sg": { + "message": "Singapore", + "description": "Name of the country of Singapore" + }, + "geo_south_carolina": { + "message": "South Carolina", + "description": "Name of the US state." + }, + "geo_st_ghislain": { + "message": "St. Ghislain", + "description": "Name of the town in Belgium." + }, + "geo_sydney": { + "message": "Sydney", + "description": "Name of the city in Australia." + }, + "geo_tokyo": { + "message": "Tokyo", + "description": "Name of the city in Japan." + }, + "geo_toronto": { + "message": "Toronto", + "description": "Name of the city in Canada." + }, + "geo_warsaw": { + "message": "Warsaw", + "description": "Name of the city in Poland." + }, + "geo_zurich": { + "message": "Zürich", + "description": "Name of the city in Switzerland." + }, + "close": { + "message": "Close", + "description": "This string appears in dialogs as a button. Clicking it dismisses the dialog." + }, + "confirmation_server_destroy": { + "message": "Existing users will lose access. This action cannot be undone.", + "description": "This string appears in a dialog that requests user confirmation for destroying a server. It informs the user about the consequences of destroying a server. 'Destroy' in this context implies that the server will be deleted." + }, + "confirmation_server_destroy_title": { + "message": "Destroy Server?", + "description": "This string appears in a dialog that requests user confirmation for destroying a server. 'Destroy' in this context implies that the server will be deleted." + }, + "confirmation_server_remove": { + "message": "This action removes your server from the Outline Manager, but does not block proxy access to users. You will still need to manually delete the Outline server from your host machine.", + "description": "This string appears in a dialog that requests user confirmation for removing a server from the application. It informs the user about the consequences of removing a server. 'Remove' in this context does not imply server deletion." + }, + "confirmation_server_remove_title": { + "message": "Remove Server?", + "description": "This string appears in a dialog that requests user confirmation for removing a server from the application. 'Remove' in this context does not imply server deletion." + }, + "data_limit": { + "message": "Data Limit", + "Description": "This string appears in various places related to the data transfer limit for a single access key." + }, + "data_limit_per_key": { + "message": "Data limit per key", + "description": "This string appears as a label to an input to set the default access key data limit for a server." + }, + "data_limits": { + "message": "Data limits", + "description": "This string appears as a title in a section to configure access key data transfer limits." + }, + "data_limits_description": { + "message": "Set a 30 day trailing data transfer limit for access keys on this server.", + "description": "This string appears as an explanation to the access key data transfer limits feature." + }, + "data_limits_dialog_text": { + "message": "Go to the Settings tab to set a data transfer limit for access keys on this server.", + "description": "This string appears as a text in a dialog to advertise the data limits feature." + }, + "data_limits_dialog_title": { + "message": "Avoid data overages", + "description": "This string appears as a title in a dialog to advertise the data limits feature." + }, + "data_limits_disclaimer": { + "message": "Since you are currently reporting metrics, use of the data limits feature will be included. Please see the $START_OF_LINK$data collection policy$END_OF_LINK$ for more details.", + "description": "This string provides a data collection disclaimer to opting into the data limits features.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "data_limits_usage": { + "message": "$USED$ of $TOTAL$ used", + "description": "This string appears in a tooltip when the data limits feature is enabled. It lets the user know how much data an access key has used, relative to the limit.", + "placeholders": { + "USED": { + "content": "{used}", + "example": "10 GB" + }, + "TOTAL": { + "content": "{total}", + "example": "50 GB" + } + } + }, + "destroy": { + "message": "Destroy", + "description": "This string appears as a button in a dialog that requests user confirmation for destroying a server. Clicking the button causes the server to be deleted." + }, + "disconnect": { + "message": "Disconnect", + "description": "This string appears as a button in a dialog to disconnect the user's cloud service account from the application. Clicking it signs the user out of the cloud server provider." + }, + "digitalocean_disconnect_account": { + "message": "Disconnect DigitalOcean account", + "description": "This string appears as a header in a dialog to disconnect the user's DigitalOcean account from the application. DigitalOcean is a cloud server provider name and should not be translated." + }, + "digitalocean_unreachable": { + "message": "This error may be due to a firewall on your network or temporary connectivity issues with digitalocean.com.", + "description": "This string appears in a dialog as a paragraph. It is shown when a DigitalOcean server cannot be reached. DigitalOcean is a cloud server provider." + }, + "disabled": { + "message": "Disabled", + "description": "This string appears across the application as a drop-down menu option. It allows the user to deactivate features." + }, + "done": { + "message": "Done", + "description": "This string appears across the application as a button. It lets the user indicate that an action, such as entering text or reading a dialog, has been completed." + }, + "enabled": { + "message": "Enabled", + "description": "This string appears across the application as a drop-down menu option. It allows the user to activate features." + }, + "error_connectivity": { + "message": "We're having trouble connecting to your DigitalOcean account. This is sometimes a temporary problem with DigitalOcean or with your internet connection. If retrying doesn't work, logging into DigitalOcean again should fix the problem.", + "description": "This string appears in a dialog as a paragraph. The dialog is shown when an operation on a DigitalOcean server fails due to connectivity issues. The dialog displays buttons that allow the user to retry the operation. DigitalOcean is a cloud server provider name and should not be translated." + }, + "error_connectivity_title": { + "message": "Connection problem", + "description": "This string appears in a dialog as a header. The dialog is shown when an operation on a server fails due to connectivity issues. The dialog displays buttons that allow the user to retry the operation." + }, + "error_do_account_info": { + "message": "Failed to get DigitalOcean account information", + "description": "This string appears in an error notification toast. It is shown when there is an error retrieving the user's DigitalOcean account. DigitalOcean is a cloud server provider name and should not be translated." + }, + "error_do_auth": { + "message": "Authentication with DigitalOcean failed", + "description": "This string appears in an error notification toast. It is shown when there is an error when logging in to the user's DigitalOcean account. DigitalOcean is a cloud server provider name and should not be translated." + }, + "error_do_regions": { + "message": "Failed to get list of available regions", + "description": "This string appears in an error notification toast. It is shown when there is an error retrieving the regions available for server deployment." + }, + "error_do_limit": { + "message": "Your DigitalOcean account has reached its limit of $NUM$ Droplets. You can request an increase at https://cloud.digitalocean.com/account/team/droplet_limit_increase", + "description": "This string appears in an error notification toast. It is shown when the user has created the maximum number of allowed servers.", + "placeholders": { + "NUM": { + "content": "{num}", + "example": "3" + } + } + }, + "error_do_warning": { + "message": "DigitalOcean warning: \"$MESSAGE$\"", + "description": "This string appears in an error notification toast when login has succeeded but there is a warning message.", + "placeholders": { + "MESSAGE": { + "content": "{message}", + "example": "Your team has been locked due to improper use of the platform." + } + } + }, + "error_gcp_auth": { + "message": "Authentication with Google Cloud Platform failed", + "description": "This string appears in an error notification toast. It is shown when there is an error when logging in to the user's Google Cloud Platform account. Google Cloud Platform is a cloud server provider name and should not be translated." + }, + "error_feedback": { + "message": "Failed to submit feedback. Please try again.", + "description": "This string appears in an error notification toast. It is shown when there is an error submitting the user's feedback." + }, + "error_hostname_invalid": { + "message": "Must be an IP address or valid hostname.", + "description": "This string appears in an inline error message. It signifies that the user has input an invalid hostname." + }, + "error_key_add": { + "message": "Failed to add key", + "description": "This string appears in an error notification toast. It is shown when there is an error creating a server access key." + }, + "error_key_remove": { + "message": "Failed to remove key", + "description": "This string appears in an error notification toast. It is shown when there is an error deleting a server access key." + }, + "error_key_rename": { + "message": "Failed to rename key", + "description": "This string appears in an error notification toast. It is shown when there is an error renaming a server access key." + }, + "error_keys_get": { + "message": "Could not load keys", + "description": "This string appears in an error notification toast. It is shown when there is an error retrieving a server access keys." + }, + "error_keys_port_bad_input": { + "message": "The port must be an integer between 1 and 65,535.", + "description": "This string appears in an inline error message. It signifies that the input number for the port for new access keys is invalid." + }, + "error_keys_port_in_use": { + "message": "The port is already in use on the server.", + "description": "This string appears in an inline error message. It signifies that the input port for new access keys is already being used on the server and is unavailable." + }, + "error_licenses": { + "message": "Could not load licenses.", + "description": "This string appears in a dialog that shows the application's software licenses. It is shown instead of the licenses' text when loading them fails." + }, + "error_metrics": { + "message": "Error setting metrics enabled", + "description": "This string appears in an error notification toast. It is shown when there is an error enabling or disabling a server's metrics reporting." + }, + "error_network": { + "message": "A network error occurred.", + "description": "This string indicates that an error happened due to network errors like not being connected to the internet." + }, + "error_not_saved": { + "message": "Not Saved", + "description": "This string appears in an error notification toast. It signifies failure to submit user input to change the port number for new access keys." + }, + "error_server_creation": { + "message": "There was an error creating your Outline server.", + "description": "This string appears in an dialog as a paragraph. The dialog is shown when there is an creating a server; the user can retry the operation or destroy the server." + }, + "error_server_destroy": { + "message": "Failed to destroy server", + "description": "This string appears in an error notification toast. It is shown when there is an error destroying a server." + }, + "error_server_removed": { + "message": "$SERVER_NAME$ no longer present in your DigitalOcean account.", + "description": "This string appears in an error notification toast. It is shown when a DigitalOcean server was destroyed outside the application to let the user know that it will not be displayed in the UI. DigitalOcean is a cloud server provider name and should not be translated.", + "placeholders": { + "SERVER_NAME": { + "content": "{serverName}", + "example": "New York Outline Server" + } + } + }, + "error_server_rename": { + "message": "Failed to rename server", + "description": "This string appears in an error notification toast. It is shown when there is an error renaming a server." + }, + "error_server_unreachable": { + "message": "Your Outline Server was installed correctly, but we are not able to connect to it. Most likely this is because your server's firewall rules are blocking incoming connections. Please review them and make sure to allow incoming TCP connections on ports ranging from 1024 to 65535.", + "description": "This string appears in dialog as a paragraph. The dialog is shown when a server is installed successfully but cannot be reached. The dialog provides possible solutions and displays buttons that allow the user to retry reaching the server." + }, + "error_server_unreachable_title": { + "message": "Unable to connect to your Outline Server", + "description": "This string appears in dialog as a header. The dialog is shown when a server is installed successfully but cannot be reached." + }, + "error_servers_removed": { + "message": "$SERVER_NAMES$ no longer present in your DigitalOcean account.", + "description": "This string appears in an error notification toast. It is shown when multiple DigitalOcean servers were destroyed outside the application to let the user know that they will not be displayed in the UI. DigitalOcean is a cloud server provider name and should not be translated.", + "placeholders": { + "SERVER_NAMES": { + "content": "{serverNames}", + "example": "New York Outline Server, Amsterdam Outline Server" + } + } + }, + "error_unexpected": { + "message": "An unexpected error occurred.", + "description": "This string signifies that an error we didn't expect was encountered." + }, + "error_set_data_limit": { + "message": "Could not set default data limit", + "description": "This string appears in an error notification toast. It is shown on failure to set the default data transfer limit on access keys." + }, + "error_remove_data_limit": { + "message": "Could not disable default data limit", + "description": "This string appears in an error notification toast. It is shown on failure to remove the default data transfer limit on access keys." + }, + "error_set_per_key_limit": { + "message": "Could not set data limit for this access key", + "description": "This string appears in an error notification toast. It is shown on failure to set the data transfer limit on a particular access key." + }, + "error_remove_per_key_limit": { + "message": "Could not remove data limit from this access key", + "description": "This string appears in an error notification toast. It is shown on failure to remove the data transfer limit on a particular access key." + }, + "experimental": { + "message": "Experimental", + "description": "This tag appears above a feature that is not yet fully tested." + }, + "experiments": { + "message": "Experiments", + "description": "This string is the title of a section that displays opt-in experimental features." + }, + "experiments_description": { + "message": "Test new features and provide us with feedback before they are released.", + "description": "This string is the description of a section that displays opt-in experimental features." + }, + "experiments_disclaimer": { + "message": "Experiments are in development and may change or be removed from the app. If you are currently reporting metrics, use of experimental features will be included. Please see the $START_OF_LINK$data collection policy$END_OF_LINK$ for more details.", + "description": "This string provides a disclaimer to opting into experimental features.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "experiments_feedback": { + "message": "Have suggestions? $START_OF_LINK$Submit feedback here.$END_OF_LINK$", + "description": "This string appears in a section that allows the user to enable experimental features. Allows the user to submit feedback about a particular feature.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "feedback_cloud_provider": { + "message": "Select cloud provider", + "description": "This string appears in the feedback dialog as an placeholder within a drop-down. Allows the user to select a cloud provider for certain feedback categories." + }, + "feedback_cloud_provider_error": { + "message": "Please select a cloud provider.", + "description": "This string appears in the feedback dialog as an error message within a drop-down. The string appears when the user attempts to submit feedback without selecting a cloud provider." + }, + "feedback_connection": { + "message": "Can't connect to my server", + "description": "This string appears in the feedback dialog as an option within a drop-down of feedback categories. Indicates that the user cannot connect to their server." + }, + "feedback_connection_others": { + "message": "Others can't connect to my server", + "description": "This string appears in the feedback dialog as an option within a drop-down of feedback categories. Indicates that users who have been given access keys cannot connect to the server." + }, + "feedback_disclaimer": { + "message": "Please note that our team is only able to answer feedback in English.", + "description": "This string appears in the feedback dialog as a paragraph under the feedback form. Lets the user know that, although the application is localized, our team can only respond to feedback in English." + }, + "feedback_email": { + "message": "Email address (optional)", + "description": "This string appears in the feedback dialog as a label of the email input form." + }, + "feedback_error": { + "message": "Please enter feedback.", + "description": "This string appears in the feedback dialog as a paragraph below the feedback input form. It is displayed when the user clicks the submit button without having entered feedback in the input form." + }, + "feedback_explanation_install": { + "message": "An error occurred while attempting to install Outline on your server. If you haven't been able to figure out a solution, please consider sending us feedback and telling us your email address (optional) so that we can get back to you.", + "description": "This string appears in the feedback dialog as a paragraph above the feedback input form. The feedback dialog gets automatically displayed with this message when a server installation fails." + }, + "feedback_general": { + "message": "General feedback", + "description": "This string appears in the feedback dialog as an option within a drop-down of feedback categories. Indicates that the user is providing general feedback." + }, + "feedback_install": { + "message": "Having trouble installing Outline", + "description": "This string appears in the feedback dialog as an option within a drop-down of feedback categories. Indicates that the user cannot install Outline on their server." + }, + "feedback_label": { + "message": "Your feedback", + "description": "This string appears in the feedback dialog as a placeholder of the feedback input form." + }, + "feedback_management": { + "message": "Having trouble managing my server", + "description": "This string appears in the feedback dialog as an option within a drop-down of feedback categories. Indicates that the user is having issues managing their server." + }, + "feedback_other": { + "message": "Other", + "description": "This string appears in the feedback dialog as an option within a drop-down of cloud providers. Indicates that the user has a server on an unlisted cloud provider." + }, + "feedback_privacy": { + "message": "Your feedback, email address (if provided) and additional information referred to in the $START_OF_LINK$privacy policy$END_OF_LINK$ will be sent to the Outline team.", + "description": "This string appears in the feedback dialog as paragraph below the feedback input form. Lets the user know how their feedback and personal information will be handled.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "feedback_submit": { + "message": "Submit", + "description": "This string appears in the feedback dialog as a button. Clicking it submits the feedback form." + }, + "feedback_suggestion": { + "message": "Suggestions", + "description": "This string appears in the feedback dialog as an option within a drop-down of feedback categories. Indicates that the user is providing a product suggestion." + }, + "feedback_title_generic": { + "message": "Send Feedback", + "description": "This string appears in the feedback dialog as a header." + }, + "feedback_title_install": { + "message": "Outline Server Installation Failed", + "description": "This string appears in the feedback dialog as a header. The feedback dialog gets automatically displayed with this header when a server installation fails." + }, + "gcp_billing_description": { + "message": "$START_OF_LINK$Open the Cloud Console billing page$END_OF_LINK$ and add an account in order to proceed.", + "description": "This instruction is a clarification regarding the title of the billing page.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "gcp_billing_action": { + "message": "Next", + "description": "Button label for a button that opens the billing page in the web browser (i.e. next step).", + "meaning": "gcp_billing_action" + }, + "gcp_billing_body": { + "message": "Waiting for you to $START_OF_LINK$add a billing account on Google Cloud$END_OF_LINK$", + "description": "Body text on the billing page", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "gcp_billing_error": { + "message": "Unable to retrieve billing information", + "description": "Error popup message in response to a user click" + }, + "gcp_billing_error_zero": { + "message": "You must add a billing account before proceeding.", + "description": "Error popup message in response to a user click" + }, + "gcp_click_create": { + "message": "Click 'Create'.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure a firewall in Google Cloud Platform. 'Create' should be translated and included untranslated in parentheses." + }, + "gcp_create_new_project": { + "message": "$START_OF_LINK$Create a new Google Cloud Project$END_OF_LINK$.", + "description": "This string appears in the server setup view as a card header for creating a new Google Cloud project", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "gcp_create_new_vm": { + "message": "$START_OF_LINK$Create a new VM instance$END_OF_LINK$.", + "description": "This string appears in the server setup view as an section header for instructions for creating a VM on Google Cloud.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "gcp_create_project": { + "message": "Create a Google Cloud project", + "description": "This string appears as a header of a set of instructions for creating a new Google Cloud project" + }, + "gcp_create_server": { + "message": "Create your Google Cloud Project", + "description": "This string appears in the server setup view as a section header for creating a new Google Cloud project" + }, + "gcp_create_vm": { + "message": "Create a VM Instance", + "description": "This string appears as a header for a set of instructions for creating a new Google Cloud VM" + }, + "gcp_disconnect_account": { + "message": "Disconnect Google Cloud Platform account", + "description": "This string appears as a header in a dialog to disconnect the user's Google Cloud Platform account from the application. Google Cloud Platform is a cloud server provider name and should not be translated." + }, + "gcp_firewall_create_0": { + "message": "$START_OF_LINK$Add a new firewall rule$END_OF_LINK$ to your Compute Engine project.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure a firewall in Google Cloud Platform. Compute Engine is a product of Google Cloud Platform and should not be translated.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "gcp_firewall_create_1": { + "message": "Type 'outline' in the 'Name' field.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure a firewall in Google Cloud Platform. 'Name' should be translated and included untranslated in parentheses." + }, + "gcp_firewall_create_2": { + "message": "Type 'outline' in the 'Target tags' field.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure a firewall in Google Cloud Platform. 'Target tags' should be translated and included untranslated in parentheses." + }, + "gcp_firewall_create_3": { + "message": "Type '0.0.0.0/0' in the 'Source IP ranges' field.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure a firewall in Google Cloud Platform. 'Source IP ranges' should be translated and included untranslated in parentheses." + }, + "gcp_firewall_create_4": { + "message": "Select 'Allow all' under 'Protocols and ports'.", + "description": "This string appears within the server setup view as an item of a list that provides instructions to configure a firewall in Google Cloud Platform. 'Allow all' and 'Protocols and ports' should be translated and included untranslated in parentheses." + }, + "gcp_name_your_project": { + "message": "Name your project in the 'Project name' field.", + "description": "This string appears as instructions for naming a new Google Cloud Project. 'Project Name' should be translated and include untranslated in parentheses" + }, + "gcp_project_setup_error": { + "message": "An error occurred while setting up your Google Cloud project", + "description": "Error message, shown in a popup when server creation fails" + }, + "gcp_select_machine_type": { + "message": "Select 'f1-micro' under 'Machine type'", + "description": "This string appears as an instruction for selecting the machine type for a new Google Cloud VM. 'Machine type' should be translated and include untranslated in parentheses." + }, + "gcp_select_networking": { + "message": "Click 'Management, security, disks, networking, sole tenancy', then 'Networking'", + "description": "This string appears as an instruction to find the networking options when creating a new Google Cloud VM. 'Management, security, disks, networking, sole tenancy' and 'Networking' should match that used in the Google Cloud Console should be translated and included untranslated in parentheses." + }, + "gcp_select_region": { + "message": "Select a region close to where the server's users will be under 'Region'.", + "description": "This string appears as an instruction for selecting a region for a new Google Cloud VM. 'Region' should be translated and include untranslated in parentheses." + }, + "gcp_type_network_tag": { + "message": "Type 'outline' in the 'Network tags' field", + "description": "This string appears as an instruction for specifying a network tag in a new Google Cloud VM. 'Network tags' should be translated and include untranslated in parentheses" + }, + "gcp_type_outline_server": { + "message": "Type 'outline-server' in the 'Name' field.", + "description": "This string is an instruction in directions for creating a new Google Cloud VM. 'Name' should be translated and include untranslated in parentheses" + }, + "key": { + "message": "Key $KEY_ID$", + "description": "This string appears in the server view as a placeholder for a newly created access key with id number KEY_ID.", + "placeholders": { + "KEY_ID": { + "content": "{keyId}", + "example": "1" + } + } + }, + "manager_resources": { + "message": "Manager Resources", + "description": "This string appears within the manager sidebar as a link to the manager documentation." + }, + "manual_server_assign_firewall": { + "message": "Assign firewall rule", + "description": "This string appears in the server setup view as a sub-header of a section that provides instructions to configure the server's firewall." + }, + "manual_server_assign_group": { + "message": "Assign Security Group", + "description": "This string appears in the server setup view as a sub-header of a section that provides instructions to configure the server's firewall." + }, + "manual_server_create_firewall": { + "message": "Create a firewall rule", + "description": "This string appears in the server setup view as a sub-header of a section that provides instructions to configure the server's firewall." + }, + "manual_server_create_group": { + "message": "Create a Security Group", + "description": "This string appears in the server setup view as a sub-header of a section that provides instructions to configure the server's firewall." + }, + "manual_server_description": { + "message": "These steps will help you install Outline on a $CLOUD_PROVIDER$ Linux server.", + "description": "This string appears in the server setup view as a header. Lets the user know that the following sections provide instructions on how to install Outline on their server.", + "placeholders": { + "CLOUD_PROVIDER": { + "content": "{cloudProvider}", + "example": "Amazon Web Services" + } + } + }, + "manual_server_firewall": { + "message": "Configure your firewall", + "description": "This string appears in the server setup view as the header of a section that provides instructions to configure the server's firewall." + }, + "manual_server_install_paste": { + "message": "Paste your installation output here.", + "description": "This string appears in the server setup view as a header of a section that contains an input form. The user must enter the output of the server installation script to manage their server in the application." + }, + "manual_server_install_run": { + "message": "Log into your server, and run this command.", + "description": "This string appears in the server setup view as a header of a section that displays a command to install Outline on their server." + }, + "manual_server_instructions": { + "message": "Instructions", + "description": "This string appears in the server setup view as a toggle of a section that provides instructions to configure the server's firewall." + }, + "manual_server_show_me": { + "message": "Show me where", + "description": "This string appears in the server setup view as a button. Clicking the button opens a link to the cloud server provider management console." + }, + "manual_server_title": { + "message": "Follow the instructions below", + "description": "This string appears in the server setup view as a header. Communicates to the user that they need to perform a series of actions to install Outline on their server." + }, + "metrics_description": { + "message": "Share anonymized metrics to help improve the reliability and performance of Outline, for you and for those you share your server with. $START_OF_LINK$Learn more.$END_OF_LINK$", + "description": "This string appears in the server settings view as a paragraph next to a toggle that enables/disables server metrics reporting.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "metrics_share": { + "message": "Share metrics", + "description": "This string appears as a button of a dialog that prompts the user to share server metrics. Clicking it enables server metrics reporting." + }, + "metrics_skip": { + "message": "Skip", + "description": "This string appears as a button of a dialog that prompts the user to share server metrics. Clicking it dismisses the dialog." + }, + "metrics_title": { + "message": "Metrics sharing", + "description": "This string appears as the header of a dialog that prompts the user to share server metrics." + }, + "nav_about": { + "message": "About", + "description": "This string appears in an application drawer as a navigation link. Clicking it opens a dialog with information about Outline." + }, + "no_data_limit": { + "message": "None", + "description": "This string appears alongside each access key in the data transfer stats section if it is under no data limit. Example: 450 MB / None" + }, + "nav_data_collection": { + "message": "Data collection", + "description": "This string appears in an application drawer as a navigation link. Clicking it opens Outline's data collection policy in the browser." + }, + "nav_feedback": { + "message": "Feedback", + "description": "This string appears in an application drawer as a navigation link. Clicking it opens a dialog that allows the user to submit feedback." + }, + "nav_help": { + "message": "Help", + "description": "This string appears in an application drawer as a navigation link. Clicking it opens Outline's support website." + }, + "nav_licenses": { + "message": "Licenses", + "description": "This string appears in an application drawer as a navigation link. Clicking it opens a dialog listing Outline's software licenses." + }, + "nav_privacy": { + "message": "Privacy", + "description": "This string appears in an application drawer as a navigation link. Clicking it opens Outline's privacy policy in the browser." + }, + "nav_terms": { + "message": "Terms", + "description": "This string appears in an application drawer as a navigation link. Clicking it opens Outline's terms of service in the browser." + }, + "notification_app_update": { + "message": "An updated version of the Outline Manager has been downloaded. It will be installed when you restart the application.", + "description": "This string appears in a transient notification toast. It is shown when an update to the application is ready to install." + }, + "notification_feedback_thanks": { + "message": "Thanks for helping us improve! We love hearing from you.", + "description": "This string appears in a transient notification toast. It is shown when the user has successfully submitted feedback." + }, + "notification_key_added": { + "message": "Key added", + "description": "This string appears in a transient notification toast. It is shown when a server access key has been successfully created." + }, + "notification_key_removed": { + "message": "Key removed", + "description": "This string appears in a transient notification toast. It is shown when a server access key has been successfully removed." + }, + "notification_server_destroyed": { + "message": "Server destroyed", + "description": "This string appears in a transient notification toast. It is shown when a server has been successfully destroyed." + }, + "notification_server_exists": { + "message": "Server already added", + "description": "This string appears in a transient notification toast. It is shown when the user attempts to add server that is already present in the application." + }, + "notification_server_removed": { + "message": "Server removed", + "description": "This string appears in a transient notification toast. It is shown when a server has been successfully removed from the application." + }, + "oauth_account_active": { + "message": "Your DigitalOcean account has been activated.", + "description": "This string appears in the DigitalOcean account creation flow as a sub-header. Lets the user know their DigitalOcean account is active. DigitalOcean is a cloud provider name and should not be translated." + }, + "oauth_account_active_tag": { + "message": "Account activated! Loading server locations...", + "description": "This string appears in the DigitalOcean account creation flow as a paragraph. Displayed when the user's DigitalOcean account has been successfully activated." + }, + "oauth_activate_account": { + "message": "Activate your DigitalOcean account.", + "description": "This string appears in the DigitalOcean account creation flow as a header. Displayed when the account has been created but not yet active. DigitalOcean is a cloud provider name and should not be translated." + }, + "oauth_billing": { + "message": "Enter your billing information on digitalocean.com and return to the app once you are done.", + "description": "This string appears in the DigitalOcean account creation flow as a sub-header. Prompts the user to enter their billing information on DigitalOcean's website." + }, + "oauth_billing_tag": { + "message": "Enter billing information...", + "description": "This string appears in the DigitalOcean account creation flow as a paragraph. Prompts the user to enter their billing information on DigitalOcean's website." + }, + "oauth_connect_description": { + "message": "With your account, Outline makes it easy to create a server and get connected.", + "description": "This string appears in the DigitalOcean account creation flow as a sub-header. Displayed when the user has not yet authorized Outline to access their DigitalOcean account." + }, + "oauth_connect_tag": { + "message": "Waiting to connect your account...", + "description": "This string appears in the DigitalOcean account creation flow as a paragraph. Displayed when the user has not yet authorized Outline to access their DigitalOcean account." + }, + "oauth_connect_title": { + "message": "Sign in or create an account with DigitalOcean.", + "description": "This string appears in the DigitalOcean account creation flow as a header. Displayed when the user has not yet signed in to DigitalOcean or created an account." + }, + "oauth_sign_out": { + "message": "Sign Out", + "description": "This string appears in the DigitalOcean account creation flow as a button. Displayed when the user has authorized Outline to access their account. Clicking it aborts the account creation flow and signs out the user." + }, + "oauth_verify": { + "message": "Check your inbox for an email from DigitalOcean, and click the link in it to confirm your account.", + "description": "This string appears in the DigitalOcean account creation flow as a sub-header. Displayed when the user has not yet entered billing information to activate their DigitalOcean account. DigitalOcean is a cloud provider name and should not be translated." + }, + "oauth_verify_tag": { + "message": "Confirm your email...", + "description": "This string appears in the DigitalOcean account creation flow as a paragraph. Displayed when the user has not yet confirmed the registered email for their DigitalOcean account." + }, + "okay": { + "message": "OK", + "description": "This string appears across the application as a button. It lets the user acknowledge displayed information. Clicking dismisses the enclosing UI element." + }, + "per_key_data_limit_dialog_set_custom": { + "message": "Set a custom data limit", + "description": "This string appears next to a checkbox in the per-key data limit dialog which, when selected, shows the input to add a data limit to an access key" + }, + "per_key_data_limit_dialog_title": { + "message": "Data Limit - $KEY_NAME$", + "description": "This string appears as the title of the per-key data limit dialog.", + "placeholders": { + "KEY_NAME": { + "content": "{keyName}", + "Example": "Key 1" + } + } + }, + "region_description": { + "message": "This is where your internet experience will come from.", + "description": "This string appears within the server creation flow, as a sub-header of the server selection view. Lets the user know about the implications of selecting a server location." + }, + "region_best_value": { + "message": "Best Value", + "description": "Appears within the server creation flow, indicating that a server option costs less than the other options." + }, + "region_setup": { + "message": "Set up Outline", + "description": "This string appears within the server creation flow, as a button of the server selection view. Clicking it creates an Outline server at the selected location. 'Set up' means install and run the Outline server software." + }, + "region_title": { + "message": "Select the location of your server.", + "description": "This string appears within the server creation flow, as the header of the server selection view." + }, + "remove": { + "message": "Remove", + "description": "This string appears across the application as a button. It allows the user to confirm the removal of a server or access key." + }, + "retry": { + "message": "Retry", + "description": "This string appears across the application as a button. It allows the user to retry a failed operation." + }, + "save": { + "message": "Save", + "description": "This string appears across the application as a button. It allows the user to submit some input" + }, + "saved": { + "message": "Saved", + "description": "This string appears across the application as a transient notification toast. It appears when user input has been accepted." + }, + "saving": { + "message": "Saving...", + "description": "This string appears across the application as a transient notification toast. It appears when user input is being processed." + }, + "server_access": { + "message": "Server access", + "description": "This string appears within the server view as the header of a card that displays the number of server access keys." + }, + "server_access_key_new": { + "message": "Add new key", + "description": "This string appears within the server view as a button. Clicking it creates a new server access key." + }, + "server_access_key_rename": { + "message": "Rename", + "description": "This string appears within the server view as a button of a drop-down to manage a server access key. Clicking it renames the access key." + }, + "server_access_keys": { + "message": "Access keys", + "description": "This string appears within the server view as a header of a table column that displays server access keys." + }, + "server_connections": { + "message": "Connections", + "description": "This string appears within the server view as a header of the section that displays server information and access keys." + }, + "server_data_transfer": { + "message": "Data transferred / last 30 days", + "description": "This string appears within the server view as the header of a card that displays the amount of data transferred by the server." + }, + "server_data_used": { + "message": "Allowance used / last 30 days", + "description": "This string appears within the server view as the header of a card that displays the amount of data transferred by the server as a percentage of the total available data." + }, + "server_destroy": { + "message": "Destroy server", + "description": "This string appears within the server view as a button of a drop-down to manage a server. Clicking it opens a confirmation dialog to destroy the server." + }, + "server_help_access_key_description": { + "message": "Share access keys with friends, so they can connect to your Outline server. They can use the same access key on all their devices.", + "description": "This string appears within a help bubble as a paragraph. The help bubble is displayed when a server is installed. Informs to the user how to share access to their server." + }, + "server_help_access_key_next": { + "message": "Next", + "description": "This string appears within a help bubble as a button. Clicking it displays the next help bubble." + }, + "server_help_access_key_title": { + "message": "Create keys, share access", + "description": "This string appears within a help bubble as a header. The help bubble provides information about sharing access to the server through access keys." + }, + "server_help_connection_description": { + "message": "Click here to install the Outline client app, using your personal access key to your Outline server.", + "description": "This string appears within a help bubble as a paragraph. Informs the user how to install the Outline clients and get connected to the server." + }, + "server_help_connection_ok": { + "message": "Okay, got it!", + "description": "This string appears within a help bubble as a button. Clicking it dismisses the help bubble." + }, + "server_help_connection_title": { + "message": "You are not connected yet!", + "description": "This string appears within a help bubble as a header. The help bubble provides information about connecting to the server." + }, + "server_keys": { + "message": "Keys", + "description": "This string appears within the server view as the unit of a card that displays the number of server access keys." + }, + "server_my_access_key": { + "message": "My access key", + "description": "This string appears within the server view as the header for the default server access key. This key is meant to be used by the server administrator." + }, + "server_name": { + "message": "Outline Server $SEVER_LOCATION$", + "description": "This string appears within the server view and in the application drawer. It is the default name for a newly created server.", + "placeholders": { + "SEVER_LOCATION": { + "content": "{serverLocation}", + "example": "New York" + } + } + }, + "server_remove": { + "message": "Remove server", + "description": "This string appears within the server view as a button of a drop-down to manage a server. Clicking it opens a confirmation dialog to remove the server from the application." + }, + "server_settings": { + "message": "Settings", + "description": "This string appears within the server view as a header of the section that displays server settings." + }, + "server_unreachable": { + "message": "Server unreachable", + "description": "This string appears within a server view as a header. Displayed when a server's information cannot be displayed because it is not reachable." + }, + "server_unreachable_description": { + "message": "We're having issues connecting to this server.", + "description": "This string appears within a server view as a paragraph. Informs the user that a server's information cannot be displayed because it is not reachable." + }, + "server_unreachable_managed_description": { + "message": "Try again or remove this server from the application.", + "description": "This string appears within a server view as a header. Displayed when a server's information cannot be displayed because it is not reachable. Prompts the user to retry contacting the server or remove it from the application." + }, + "server_unreachable_manual_description": { + "message": "Try again or destroy this server and the virtual host.", + "description": "This string appears within a server view as a header. Displayed when a server's information cannot be displayed because it is not reachable. Prompts the user to retry contacting the server or destroy it." + }, + "server_usage": { + "message": "Usage (last 30 days)", + "description": "This string appears within the server view as a header of a table column that displays the breakdown of data usage by access key." + }, + "servers_add": { + "message": "Add server", + "description": "This string appears in an application drawer as a button. Clicking it starts the server setup flow." + }, + "servers_digitalocean": { + "message": "DigitalOcean servers", + "description": "This string appears in an application drawer as the header of a section that displays the list of DigitalOcean servers. DigitalOcean is a cloud server provider name and should not be translated." + }, + "servers_gcp": { + "message": "Google Cloud Platform servers", + "description": "This string appears in an application drawer as the header of a section that displays the list of Google Cloud Platform servers. Google Cloud Platform is a cloud server provider name and should not be translated." + }, + "servers_manual": { + "message": "Servers", + "description": "This string appears in an application drawer as the header of a section that displays a list of servers." + }, + "settings_access_key_port": { + "message": "Port for new access keys", + "description": "This string appears in the server settings view as a label for the server port number used to create new access keys." + }, + "settings_metrics_header": { + "message": "Share anonymous metrics", + "description": "This string appears in the server settings view as a header of the section to configure server metrics reporting." + }, + "settings_server_api_url": { + "message": "Management API URL", + "description": "This string appears in the server settings view as a label for the server's management API URL. API stands for application programming interface; the acronym should not be translated." + }, + "settings_server_cost": { + "message": "Monthly cost", + "description": "This string appears in the server settings view as a label for the server's monthly cost." + }, + "settings_server_creation": { + "message": "Created", + "description": "This string appears in the server settings view as a label for the server's creation date." + }, + "settings_server_hostname": { + "message": "Hostname", + "description": "This string appears in the server settings view as a label for the server's IP address or domain name." + }, + "settings_server_id": { + "message": "Server ID", + "description": "This string appears in the server settings view as a label for the server's unique identifier." + }, + "settings_server_info": { + "message": "Server Information", + "description": "This string appears in the server settings view as a header of the section that displays server details." + }, + "settings_server_location": { + "message": "Server location", + "description": "This string appears in the server settings view as a label for the server's location." + }, + "settings_server_name": { + "message": "Name", + "description": "This string appears in the server settings view as a label for the server's name." + }, + "settings_server_rename": { + "message": "Set a new name for your server. Note that this will not be reflected on the devices of the users that you invited to connect to it.", + "description": "This string appears in the server settings view as a paragraph below the server's name. Informs the user about the implications of renaming a server." + }, + "settings_server_version": { + "message": "Server version", + "description": "This string appears in the server settings view as a label for the server version." + }, + "settings_transfer_limit": { + "message": "Data transfer allowance", + "description": "This string appears in the server settings view as a label for the server's data transfer allowance." + }, + "setup_action": { + "message": "Set up", + "description": "This string appears in the server setup view as a button. Clicking it starts the server setup flow. 'Set up' means install and run the Outline server software." + }, + "setup_advanced": { + "message": "Advanced", + "description": "This string appears in the server setup view as a label. Advanced in this context implies that the user will have to follow technical instructions to install Outline." + }, + "setup_anywhere": { + "message": "Set up Outline anywhere", + "description": "This string appears in the server setup view as a header within a card. The card provides information about installing Outline on an arbitrary cloud server provider." + }, + "setup_create": { + "message": "Create server", + "description": "This string appears in the server setup view as a button. Clicking it starts the server creation flow." + }, + "setup_description": { + "message": "Don't have a server? Create an account with DigitalOcean.", + "description": "This string appears in the server setup view as a sub-header. Prompts the user to create a DigitalOcean account to deploy Outline servers. DigitalOcean is a cloud provider name and should not be translated." + }, + "setup_cancel": { + "message": "Cancel at any time", + "description": "This string appears in the server setup view as an item of a list describing Outline's features." + }, + "setup_do_cost": { + "message": "Only US$6 a month", + "description": "This string appears in the server setup view as an item of a list describing Outline's features." + }, + "setup_do_create": { + "message": "Create a new server with your DigitalOcean account for an additional US$6/30 days for 1 TB of data transfer.", + "description": "This string appears in the server setup view as a paragraph within a card. Describes Outline servers' features when deployed on DigitalOcean. DigitalOcean is a cloud provider name and should not be translated; TB is an abbreviation for terabyte and should not be translated" + }, + "setup_do_data": { + "message": "1 TB data transfer allowance", + "description": "This string appears in the server setup view as an item of a list describing Outline's features. Refers to the monthly amount of transfer data offered by a cloud server provider. TB is an abbreviation for terabyte and should not be translated." + }, + "setup_do_description": { + "message": "This could take several minutes. You can destroy this server at any time.", + "description": "This string appears in the server setup view as a sub-header. Displayed when a server is being created along a progress bar." + }, + "setup_do_easiest": { + "message": "Easiest setup process", + "description": "This string appears in the server setup view as an item of a list describing Outline's features." + }, + "setup_do_title": { + "message": "Setting up Outline.", + "description": "This string appears in the server setup view as a header. Displayed when a server is being created along a progress bar." + }, + "setup_gcp_easy": { + "message": "Easy setup process", + "description": "This string appears in the server setup view as an item of a list describing Outline's features." + }, + "setup_gcp_free_tier": { + "message": "With $START_OF_LINK1$Free Tier$END_OF_LINK$, your first server starts at $START_OF_LINK2$US$3/month$END_OF_LINK$", + "description": "Describes the benefits of the GCP Free Tier program (https://cloud.google.com/free/docs/gcp-free-tier)", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK1": { + "content": "{openLinkFreeTier}" + }, + "START_OF_LINK2": { + "content": "{openLinkIpPrice}" + } + } + }, + "setup_gcp_free_trial": { + "message": "$START_OF_LINK$90 day free trial$END_OF_LINK$ for new users", + "description": "Describes the benefits of the GCP Free Trial program (https://cloud.google.com/free/docs/gcp-free-tier/#free-trial)", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLinkFreeTrial}" + } + } + }, + "setup_gcp_create": { + "message": "Create a new server with your Google account. Costs vary by location and usage.", + "description": "This string appears in the server setup view as a paragraph within a card. Describes Outline servers' features when deployed on Google Cloud Platform." + }, + "setup_gcp_promo": { + "message": "Try the new automatic Outline server creation process for Google Cloud", + "description": "Link text that takes users to an easier setup procedure" + }, + "setup_firewall_instructions": { + "message": "Firewall instructions", + "description": "This string appears in the server setup view as the header of a section that provides instructions to configure the server's firewall." + }, + "setup_recommended": { + "message": "Recommended", + "description": "This string appears in the server setup view as a label. Describes Outline servers deployment on DigitalOcean, which is easier from a user perspective." + }, + "setup_simple_commands": { + "message": "Simple install commands", + "description": "This string appears in the server setup view as an item of a list describing Outline's features." + }, + "setup_step-by-step": { + "message": "Step-by-step set-up guide", + "description": "This string appears in the server setup view as an item of a list describing Outline's features." + }, + "setup_tested": { + "message": "Tested on VULTR, Linode, and Liquid Web", + "description": "This string appears in the server setup view as an item of a list describing Outline's features. VULTR, Linode, and Liquid Web a cloud provider names and should not be translated." + }, + "setup_title": { + "message": "Choose a cloud service to set up Outline.", + "description": "This string appears in the server setup view as a header. Prompts the user to create deploy Outline servers on the cloud provider of their choice. 'Set up' means install and run the Outline server software." + }, + "share_description": { + "message": "Copy this invitation and send it from a communication tool you trust. $START_OF_LINK$Need help?$END_OF_LINK$", + "description": "This string appears as a sub-header of a dialog that provides instructions on how to share access to a server.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + }, + "share_invite_html": { + "message": "Use this server to safely access the open internet:$LINE_BREAK$$LINE_BREAK$1) Download and install the Outline app for your device:$LINE_BREAK$$LINE_BREAK$ - iOS: https://itunes.apple.com/app/outline-app/id1356177741$LINE_BREAK$ - MacOS: https://itunes.apple.com/app/outline-app/id1356178125$LINE_BREAK$ - Windows: https://s3.amazonaws.com/outline-releases/client/windows/stable/Outline-Client.exe$LINE_BREAK$ - Linux: https://s3.amazonaws.com/outline-releases/client/linux/stable/Outline-Client.AppImage$LINE_BREAK$ - Android: https://play.google.com/store/apps/details?id=org.outline.android.client$LINE_BREAK$ - Android alternative link: https://s3.amazonaws.com/outline-releases/client/android/stable/Outline-Client.apk$LINE_BREAK$$LINE_BREAK$2) You will receive an access key that starts with ss://. Once your receive it, copy this access key.$LINE_BREAK$$LINE_BREAK$3) Open the Outline client app. If your access key is auto-detected, tap \"Connect\" and proceed. If your access key is not auto-detected, then paste it in the field, then tap \"Connect\" and proceed.$LINE_BREAK$$LINE_BREAK$You're ready to use the open internet! To make sure you successfully connected to the server, try searching for \"what is my ip\" on Google Search. The IP address shown in Google should match the IP address in the Outline client.$LINE_BREAK$$LINE_BREAK$Learn more about Outline here: https://getoutline.org/", + "description": "This string appears as an email invitation instructing new Outline users how to set up and connect to Outline.", + "placeholders": { + "LINE_BREAK": { + "content": "
" + } + } + }, + "share_invite_copied": { + "message": "Copied invitation to clipboard", + "description": "This string appears as a label within a dialog that provides an invitation to share access to a server. Displayed when the user clicks a button to copy the invitation text to the clipboard." + }, + "share_invite_copy": { + "message": "Copy invitation", + "description": "This string appears as a button within a dialog that provides an invitation to share access to a server. Clicking it copies the invitation instructions text to the clipboard." + }, + "share_invite_copy_access_key": { + "message": "Copy access key", + "description": "This string appears as a button within a dialog that provides an access key to use a server. Clicking it copies the access key to the clipboard." + }, + "share_invite_access_key_copied": { + "message": "Copied access key to clipboard", + "description": "This string appears as a label within a dialog that provides an access key to use a server. Displayed when the user clicks a button to copy this access key to the clipboard." + }, + "share_invite_instructions": { + "message": "Follow our invitation instructions on GitHub:", + "description": "This string appears as a list item within a dialog that provides an invitation to share access to a server. Belongs to the instructions for the user with whom access is being shared." + }, + "share_invite_trouble": { + "message": "Having trouble accessing the invitation link?", + "description": "This string appears as a paragraph within a dialog that provides an invitation to share access to a server. Belongs to the instructions for the user with whom access is being shared." + }, + "share_title": { + "message": "Share access", + "description": "This string appears as the header of a dialog that provides an invitation to share access to a server." + }, + "survey_data_limits_title": { + "message": "Help us understand how to improve data limits", + "description": "This string appears as the title of a dialog that prompts the user to complete a survey about the data limits feature." + }, + "survey_decline": { + "message": "Decline", + "description": "This string appears in a dialog that prompts the user to complete a survey. It is shown in a button that dismisses the dialog." + }, + "survey_disclaimer": { + "message": "By clicking continue you will be sent to a short survey on Google Forms. We recommend taking the survey while connected to Outline.", + "description": "This string appears in a dialog that prompts the user to complete a survey as a disclaimer." + }, + "survey_go_to_survey": { + "message": "Go to survey", + "description": "This string appears in a dialog that prompts the user to complete a survey. It is shown in a button that takes the user to the online survey." + }, + "terms_of_service": { + "message": "I have read and understood the $START_OF_LINK$Outline Terms of Service$END_OF_LINK$", + "description": "This string appears within an overlay the first time the user runs the app. Prompts the user to review and accept Outline's terms of service.", + "placeholders": { + "END_OF_LINK": { + "content": "{closeLink}" + }, + "START_OF_LINK": { + "content": "{openLink}" + } + } + } +} diff --git a/src/server_manager/model/accounts.ts b/src/server_manager/model/accounts.ts new file mode 100644 index 000000000..4bee8bf80 --- /dev/null +++ b/src/server_manager/model/accounts.ts @@ -0,0 +1,60 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import * as digitalocean from './digitalocean'; +import * as gcp from './gcp'; + +export interface CloudAccounts { + /** + * Connects a DigitalOcean account. + * + * Only one DigitalOcean account can be connected at any given time. + * Subsequent calls to this method will overwrite any previously connected + * DigtialOcean account. + * + * @param accessToken: The DigitalOcean access token. + */ + connectDigitalOceanAccount(accessToken: string): digitalocean.Account; + + /** + * Connects a Google Cloud Platform account. + * + * Only one Google Cloud Platform account can be connected at any given time. + * Subsequent calls to this method will overwrite any previously connected + * Google Cloud Platform account. + * + * @param refreshToken: The GCP refresh token. + */ + connectGcpAccount(refreshToken: string): gcp.Account; + + /** + * Disconnects the DigitalOcean account. + */ + disconnectDigitalOceanAccount(): void; + + /** + * Disconnects the Google Cloud Platform account. + */ + disconnectGcpAccount(): void; + + /** + * @returns the connected DigitalOcean account (or null if none exists). + */ + getDigitalOceanAccount(): digitalocean.Account; + + /** + * @returns the connected Google Cloud Platform account (or null if none exists). + */ + getGcpAccount(): gcp.Account; +} diff --git a/src/server_manager/model/digitalocean.ts b/src/server_manager/model/digitalocean.ts new file mode 100644 index 000000000..4e88d7f8a --- /dev/null +++ b/src/server_manager/model/digitalocean.ts @@ -0,0 +1,70 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import * as location from './location'; +import {ManagedServer} from './server'; + +// A DigitalOcean Region, e.g. "NYC2". +export class Region implements location.CloudLocation { + private static readonly LOCATION_MAP: {readonly [cityId: string]: location.GeoLocation} = { + ams: location.AMSTERDAM, + blr: location.BANGALORE, + fra: location.FRANKFURT, + lon: location.LONDON, + nyc: location.NEW_YORK_CITY, + sfo: location.SAN_FRANCISCO, + sgp: location.SINGAPORE, + syd: location.SYDNEY, + tor: location.TORONTO, + }; + constructor(public readonly id: string) {} + + get location(): location.GeoLocation { + return Region.LOCATION_MAP[this.id.substring(0, 3).toLowerCase()]; + } +} + +export interface RegionOption extends location.CloudLocationOption { + readonly cloudLocation: Region; +} + +export interface Status { + // The account has not had any billing info added yet. + readonly needsBillingInfo: boolean; + // The account has not had an email address added yet. + readonly needsEmailVerification: boolean; + // The maximum number of droplets this account can create. + readonly dropletLimit: number; + // The account cannot add any more droplets. + readonly hasReachedLimit: boolean; + // A warning message from DigitalOcean, in English. + readonly warning?: string; +} + +export interface Account { + // Gets a globally unique identifier for this Account. + getId(): string; + // Returns a user-friendly name (email address) associated with the account. + getName(): Promise; + // Returns the status of the account. + getStatus(): Promise; + // Lists all existing Shadowboxes. If `fetchFromHost` is true, performs a network request to + // retrieve the servers; otherwise resolves with a cached server list. + listServers(fetchFromHost?: boolean): Promise; + // Return a list of regions with info about whether they are available for use. + listLocations(): Promise>; + // Creates a server and returning it when it becomes active (i.e. the server has + // created, not necessarily once shadowbox installation has finished). + createServer(region: Region, name: string): Promise; +} diff --git a/src/server_manager/model/gcp.ts b/src/server_manager/model/gcp.ts new file mode 100644 index 000000000..226160290 --- /dev/null +++ b/src/server_manager/model/gcp.ts @@ -0,0 +1,140 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import * as location from './location'; +import {ManagedServer} from './server'; + +export class Zone implements location.CloudLocation { + /** @see https://cloud.google.com/compute/docs/regions-zones */ + private static readonly LOCATION_MAP: {readonly [regionId: string]: location.GeoLocation} = { + 'asia-east1': location.CHANGHUA_COUNTY, + 'asia-east2': location.HONG_KONG, + 'asia-northeast1': location.TOKYO, + 'asia-northeast2': location.OSAKA, + 'asia-northeast3': location.SEOUL, + 'asia-south1': location.MUMBAI, + 'asia-south2': location.DELHI, + 'asia-southeast1': location.JURONG_WEST, + 'asia-southeast2': location.JAKARTA, + 'australia-southeast1': location.SYDNEY, + 'australia-southeast2': location.MELBOURNE, + 'europe-north1': location.HAMINA, + 'europe-west1': location.ST_GHISLAIN, + 'europe-west2': location.LONDON, + 'europe-west3': location.FRANKFURT, + 'europe-west4': location.EEMSHAVEN, + 'europe-west6': location.ZURICH, + 'europe-central2': location.WARSAW, + 'northamerica-northeast1': location.MONTREAL, + 'northamerica-northeast2': location.TORONTO, + 'southamerica-east1': location.SAO_PAULO, + 'us-central1': location.IOWA, + 'us-east1': location.SOUTH_CAROLINA, + 'us-east4': location.NORTHERN_VIRGINIA, + 'us-west1': location.OREGON, + 'us-west2': location.LOS_ANGELES, + 'us-west3': location.SALT_LAKE_CITY, + 'us-west4': location.LAS_VEGAS, + }; + + /** ID is a GCP Zone ID like "us-central1-a". */ + constructor(public readonly id: string) {} + + /** Returns a region ID like "us-central1". */ + get regionId(): string { + return this.id.substring(0, this.id.lastIndexOf('-')); + } + + get location(): location.GeoLocation { + return Zone.LOCATION_MAP[this.regionId]; + } +} + +export interface ZoneOption extends location.CloudLocationOption { + readonly cloudLocation: Zone; +} + +export type Project = { + id: string; + name: string; +}; + +export type BillingAccount = { + id: string; + name: string; +}; + +/** + * The Google Cloud Platform account model. + */ +export interface Account { + /** + * Returns a globally unique identifier for this Account. + */ + getId(): string; + + /** + * Returns a user-friendly name associated with the account. + */ + getName(): Promise; + + /** + * Creates an Outline server on a Google Compute Engine VM instance. + * + * This method returns after the VM instance has been created. The Shadowbox + * Outline server may not be fully installed. See {@link ManagedServer#waitOnInstall} + * to be notified when the server installation has completed. + * + * @param projectId - The GCP project ID. + * @param name - The name to be given to the server. + * @param zone - The GCP zone to create the server in. + */ + createServer(projectId: string, name: string, zone: Zone): Promise; + + /** + * Lists the Outline servers in a given GCP project. + * + * @param projectId - The GCP project ID. + */ + listServers(projectId: string): Promise; + + /** + * Lists the Google Compute Engine locations available to given GCP project. + * + * @param projectId - The GCP project ID. + */ + listLocations(projectId: string): Promise>; + + /** + * Creates a new Google Cloud Platform project. + * + * The project ID must conform to the following: + * - must be 6 to 30 lowercase letters, digits, or hyphens + * - must start with a letter + * - no trailing hyphens + * + * @param id - The project ID. + * @param billingAccount - The billing account ID. + */ + createProject(id: string, billingAccountId: string): Promise; + + /** Lists the Google Cloud Platform projects available with the user. */ + listProjects(): Promise; + + /** + * Lists the active Google Cloud Platform billing accounts associated with + * the user. + */ + listOpenBillingAccounts(): Promise; +} diff --git a/src/server_manager/model/location.ts b/src/server_manager/model/location.ts new file mode 100644 index 000000000..25b6738b4 --- /dev/null +++ b/src/server_manager/model/location.ts @@ -0,0 +1,90 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +/** + * Unified server location model for all cloud providers. + * + * Keys are GeoIds, identifying the location. Values are ISO country codes. + * + * Each key identifies a location as displayed in the Outline + * user interface. To minimize confusion, Outline attempts to + * present each location in a manner consistent with the cloud + * provider's own interface and documentation. When cloud providers + * present a location in similar fashion, they may share an element + * (e.g. 'frankfurt' for GCP and DO), but if they present a similar + * location in different terms, they will need to be represented + * separately (e.g. 'SG' for DO, 'jurong-west' for GCP). + * + * When the key and value are equal, this indicates that they are redundant. + */ +export class GeoLocation { + constructor(public readonly id: string, public readonly countryCode: string) {} + + countryIsRedundant(): boolean { + return this.countryCode === this.id; + } +} + +export const AMSTERDAM = new GeoLocation('amsterdam', 'NL'); +export const NORTHERN_VIRGINIA = new GeoLocation('northern-virginia', 'US'); +export const BANGALORE = new GeoLocation('bangalore', 'IN'); +export const IOWA = new GeoLocation('iowa', 'US'); +export const CHANGHUA_COUNTY = new GeoLocation('changhua-county', 'TW'); +export const DELHI = new GeoLocation('delhi', 'IN'); +export const EEMSHAVEN = new GeoLocation('eemshaven', 'NL'); +export const FRANKFURT = new GeoLocation('frankfurt', 'DE'); +export const HAMINA = new GeoLocation('hamina', 'FI'); +export const HONG_KONG = new GeoLocation('HK', 'HK'); +export const JAKARTA = new GeoLocation('jakarta', 'ID'); +export const JURONG_WEST = new GeoLocation('jurong-west', 'SG'); +export const LAS_VEGAS = new GeoLocation('las-vegas', 'US'); +export const LONDON = new GeoLocation('london', 'GB'); +export const LOS_ANGELES = new GeoLocation('los-angeles', 'US'); +export const OREGON = new GeoLocation('oregon', 'US'); +export const MELBOURNE = new GeoLocation('melbourne', 'AU'); +export const MONTREAL = new GeoLocation('montreal', 'CA'); +export const MUMBAI = new GeoLocation('mumbai', 'IN'); +export const NEW_YORK_CITY = new GeoLocation('new-york-city', 'US'); +export const SAN_FRANCISCO = new GeoLocation('san-francisco', 'US'); +export const SINGAPORE = new GeoLocation('SG', 'SG'); +export const OSAKA = new GeoLocation('osaka', 'JP'); +export const SAO_PAULO = new GeoLocation('sao-paulo', 'BR'); +export const SALT_LAKE_CITY = new GeoLocation('salt-lake-city', 'US'); +export const SEOUL = new GeoLocation('seoul', 'KR'); +export const ST_GHISLAIN = new GeoLocation('st-ghislain', 'BE'); +export const SYDNEY = new GeoLocation('sydney', 'AU'); +export const SOUTH_CAROLINA = new GeoLocation('south-carolina', 'US'); +export const TOKYO = new GeoLocation('tokyo', 'JP'); +export const TORONTO = new GeoLocation('toronto', 'CA'); +export const WARSAW = new GeoLocation('warsaw', 'PL'); +export const ZURICH = new GeoLocation('zurich', 'CH'); + +export interface CloudLocation { + /** + * The cloud-specific ID used for this location, or null to represent + * a GeoId that lacks a usable datacenter. + */ + readonly id: string; + + /** + * The physical location of this datacenter, or null if its location is + * unknown. + */ + readonly location: GeoLocation; +} + +export interface CloudLocationOption { + readonly cloudLocation: CloudLocation; + readonly available: boolean; +} diff --git a/src/server_manager/model/server.ts b/src/server_manager/model/server.ts new file mode 100644 index 000000000..2b36bc4df --- /dev/null +++ b/src/server_manager/model/server.ts @@ -0,0 +1,191 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import {CustomError} from '../infrastructure/custom_error'; +import {CloudLocation} from './location'; + +export interface Server { + // Gets a globally unique identifier for this Server. THIS MUST NOT make a network request, as + // it's used to identify unreachable servers. + getId(): string; + + // Gets the server's name for display. + getName(): string; + + // Gets the version of the shadowbox binary the server is running + getVersion(): string; + + // Updates the server name. + setName(name: string): Promise; + + // Return access key + getAccessKey(accessKeyId: AccessKeyId): Promise; + + // Lists the access keys for this server, including the admin. + listAccessKeys(): Promise; + + // Returns stats for bytes transferred across all access keys of this server. + getDataUsage(): Promise; + + // Adds a new access key to this server. + addAccessKey(): Promise; + + // Renames the access key given by id. + renameAccessKey(accessKeyId: AccessKeyId, name: string): Promise; + + // Removes the access key given by id. + removeAccessKey(accessKeyId: AccessKeyId): Promise; + + // Sets a default access key data transfer limit over a 30 day rolling window for all access keys. + // This limit is overridden by per-key data limits. Forces enforcement of all data limits, + // including per-key data limits. + setDefaultDataLimit(limit: DataLimit): Promise; + + // Returns the server default access key data transfer limit, or undefined if it has not been set. + getDefaultDataLimit(): DataLimit | undefined; + + // Removes the server default data limit. Per-key data limits are still enforced. Traffic is + // tracked for if the limit is re-enabled. Forces enforcement of all data limits, including + // per-key limits. + removeDefaultDataLimit(): Promise; + + // Sets the custom data limit for a specific key. This limit overrides the server default limit + // if it exists. Forces enforcement of the chosen key's data limit. + setAccessKeyDataLimit(accessKeyId: AccessKeyId, limit: DataLimit): Promise; + + // Removes the custom data limit for a specific key. The key is still bound by the server default + // limit if it exists. Forces enforcement of the chosen key's data limit. + removeAccessKeyDataLimit(accessKeyId: AccessKeyId): Promise; + + // Returns whether metrics are enabled. + getMetricsEnabled(): boolean; + + // Updates whether metrics are enabled. + setMetricsEnabled(metricsEnabled: boolean): Promise; + + // Gets the ID used for metrics reporting. + getMetricsId(): string; + + // Checks if the server is healthy. + isHealthy(): Promise; + + // Gets the date when this server was created. + getCreatedDate(): Date; + + // Returns the server's domain name or IP address. + getHostnameForAccessKeys(): string; + + // Changes the hostname for shared access keys. + setHostnameForAccessKeys(hostname: string): Promise; + + // Returns the server's management API URL. + getManagementApiUrl(): string; + + // Returns the port number for new access keys. + // Returns undefined if the server doesn't have a port set. + getPortForNewAccessKeys(): number | undefined; + + // Changes the port number for new access keys. + setPortForNewAccessKeys(port: number): Promise; +} + +// Manual servers are servers which the user has independently setup to run +// shadowbox, and can be on any cloud provider. +export interface ManualServer extends Server { + getCertificateFingerprint(): string | undefined; + + forget(): void; +} + +// Error thrown when monitoring an installation that the user canceled. +export class ServerInstallCanceledError extends CustomError { + constructor(message?: string) { + super(message); + } +} + +// Error thrown when server installation failed. +export class ServerInstallFailedError extends CustomError { + constructor(message?: string) { + super(message); + } +} + +// Managed servers are servers created by the Outline Manager through our +// "magic" user experience, e.g. DigitalOcean. +export interface ManagedServer extends Server { + // Yields how far installation has progressed (0.0 to 1.0). + // Exits when installation has completed. Throws ServerInstallFailedError or + // ServerInstallCanceledError if installation fails or is canceled. + monitorInstallProgress(): AsyncGenerator; + // Returns server host object. + getHost(): ManagedServerHost; +} + +// The managed machine where the Outline Server is running. +export interface ManagedServerHost { + // Returns the monthly outbound transfer limit. + getMonthlyOutboundTransferLimit(): DataAmount; + // Returns the monthly cost. + getMonthlyCost(): MonetaryCost; + // Returns the server location + getCloudLocation(): CloudLocation; + // Deletes the server - cannot be undone. + delete(): Promise; +} + +export class DataAmount { + terabytes: number; +} + +export class MonetaryCost { + // Value in US dollars. + usd: number; +} + +// Configuration for manual servers. This is the output emitted from the +// shadowbox install script, which is needed for the manager connect to +// shadowbox. +export interface ManualServerConfig { + apiUrl: string; + certSha256?: string; +} + +// Repository of ManualServer objects. These are servers the user has setup +// themselves, and configured to run shadowbox, outside of the manager. +export interface ManualServerRepository { + // Lists all existing Shadowboxes. + listServers(): Promise; + // Adds a manual server using the config (e.g. user input). + addServer(config: ManualServerConfig): Promise; + // Retrieves a server with `config`. + findServer(config: ManualServerConfig): ManualServer | undefined; +} + +export type AccessKeyId = string; + +export interface AccessKey { + id: AccessKeyId; + name: string; + accessUrl: string; + dataLimit?: DataLimit; +} + +export type BytesByAccessKey = Map; + +// Data transfer allowance, measured in bytes. +// NOTE: Must be kept in sync with the definition in src/shadowbox/access_key.ts. +export interface DataLimit { + readonly bytes: number; +} diff --git a/src/server_manager/model/survey.ts b/src/server_manager/model/survey.ts new file mode 100644 index 000000000..e412a9752 --- /dev/null +++ b/src/server_manager/model/survey.ts @@ -0,0 +1,20 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +export interface Surveys { + // Displays a survey when a server default data limit is set. + presentDataLimitsEnabledSurvey(): Promise; + // Displays a survey when a server default data limit is removed. + presentDataLimitsDisabledSurvey(): Promise; +} diff --git a/src/server_manager/package.json b/src/server_manager/package.json new file mode 100644 index 000000000..3f981b2ed --- /dev/null +++ b/src/server_manager/package.json @@ -0,0 +1,113 @@ +{ + "name": "outline-manager", + "productName": "Outline Manager", + "version": "0.0.0-debug", + "description": "Create and manage access to Outline servers", + "homepage": "https://getoutline.org/", + "author": { + "name": "The Outline authors", + "email": "info@getoutline.org" + }, + "build": { + "afterSign": "src/server_manager/electron_app/release/notarize.js", + "mac": { + "hardenedRuntime": true, + "entitlements": "src/server_manager/electron_app/release/macos.entitlements", + "entitlementsInherit": "src/server_manager/electron_app/release/macos.entitlements" + } + }, + "dependencies": { + "@polymer/app-layout": "^3.0.0", + "@polymer/app-localize-behavior": "^3.0.0", + "@polymer/font-roboto": "^3.0.0", + "@polymer/iron-autogrow-textarea": "^3.0.0", + "@polymer/iron-collapse": "^3.0.0", + "@polymer/iron-fit-behavior": "^3.0.0", + "@polymer/iron-icon": "^3.0.1", + "@polymer/iron-icons": "^3.0.0", + "@polymer/iron-pages": "^3.0.0", + "@polymer/paper-button": "^3.0.0", + "@polymer/paper-checkbox": "^3.0.0", + "@polymer/paper-dialog": "^3.0.0", + "@polymer/paper-dialog-scrollable": "^3.0.0", + "@polymer/paper-dropdown-menu": "^3.0.0", + "@polymer/paper-icon-button": "^3.0.0", + "@polymer/paper-input": "^3.0.0", + "@polymer/paper-item": "^3.0.0", + "@polymer/paper-listbox": "^3.0.0", + "@polymer/paper-progress": "^3.0.0", + "@polymer/paper-tabs": "^3.0.0", + "@polymer/paper-toast": "^3.0.0", + "@polymer/paper-tooltip": "^3.0.0", + "@sentry/electron": "^4.14.0", + "@webcomponents/webcomponentsjs": "^2.0.0", + "circle-flags": "https://github.com/HatScripts/circle-flags", + "clipboard-polyfill": "^2.4.6", + "dotenv": "~8.2.0", + "electron-updater": "^4.1.2", + "express": "^4.17.1", + "google-auth-library": "^8.0.2", + "intl-messageformat": "^7", + "jsonic": "^0.3.1", + "lit-element": "^2.3.1", + "node-forge": "^1.3.1", + "request": "^2.87.0", + "web-animations-js": "^2.3.1" + }, + "comments": { + "config": { + "PUPPETEER_CHROMIUM_REVISION": [ + "The Chromium revision number used by Karma. This should always be the revision number of", + "the bundled Chromium in our version of Electron. Whenever upgrading Electron, run the", + "server_manager tests. You'll get a failure that looks like .", + "Set PUPPETEER_CHROMIUM_REVISION to the first of those numbers to get the correct revision", + "and `npm ci` to re-install puppeteer, causing it to download the new", + "Chromium version." + ] + } + }, + "config": { + "PUPPETEER_CHROMIUM_REVISION": 992738 + }, + "devDependencies": { + "@types/node": "^16.11.29", + "@types/node-forge": "^1.0.2", + "@types/polymer": "^1.2.9", + "@types/puppeteer": "^5.4.2", + "@types/request": "^2.47.1", + "@types/semver": "^5.5.0", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "electron": "19.1.9", + "electron-builder": "^23.6.0", + "electron-icon-maker": "^0.0.4", + "electron-notarize": "^1.2.1", + "electron-to-chromium": "^1.4.328", + "gulp": "^4.0.0", + "gulp-posthtml": "^3.0.4", + "gulp-replace": "^1.0.0", + "html-webpack-plugin": "^5.5.3", + "karma": "^6.3.16", + "karma-chrome-launcher": "^3.1.0", + "karma-jasmine": "^4.0.1", + "karma-webpack": "^5.0.0", + "node-jq": "^1.11.2", + "postcss": "^7.0.29", + "postcss-rtl": "^1.7.3", + "posthtml-postcss": "^0.2.6", + "puppeteer": "^13.6.0", + "style-loader": "^3.3.3", + "ts-loader": "^9.5.0", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1", + "webpack-node-externals": "^3.0.0" + }, + "resolutions": { + "inherits": "2.0.3", + "samsam": "1.1.3", + "supports-color": "3.1.2", + "type-detect": "1.0.0" + }, + "license": "Apache" +} diff --git a/src/server_manager/scripts/fill_packaging_opts.sh b/src/server_manager/scripts/fill_packaging_opts.sh new file mode 100755 index 000000000..e14be8d11 --- /dev/null +++ b/src/server_manager/scripts/fill_packaging_opts.sh @@ -0,0 +1,57 @@ +#!/bin/bash -eu +# +# Copyright 2020 The Outline Authors +# +# 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. + +# Collects common packaging options. Meant to be called from the diffrent package_foo and +# release_foo scripts. +# Usage from a packaging script: +# source src/server_manager/electron_app/package_$PLATFORM $0 $@ +# +# Note that you MUST use "source" in order to run the script in the same process as the calling +# script, allowing fill_packaging_opts.sh to fill variables for the caller. + +# Input: "/absolute/path/src/server_manager/electron_app/something_action.sh" +# Output: "npm run action server_manager/electron_app/something" +readonly ELECTRON_PATH='server_manager/electron_app/' +readonly RELATIVE="${1#*/src/${ELECTRON_PATH}}" +readonly NPM_COMMAND="npm run action ${ELECTRON_PATH}${RELATIVE%.action.sh}" +shift + +function usage () { + echo "Usage:" 1>&2 + echo "${NPM_COMMAND} [-s stagingPercentage]" 1>&2 + echo " -s: The staged rollout percentage for this release. Must be in the interval (0, 100]. Defaults to 100" 1>&2 + echo " -h: this help message" 1>&2 + echo 1>&2 + echo "Examples:" 1>&2 + echo "Releases the beta of version 1.2.3 to 10% of users listening on the beta channel" 1>&2 + echo '$ '"jq -r '.version' src/server_manager/package.json'" 1>&2 + echo "1.2.3-beta" 1>&2 + echo '$ '"${YARN_COMMAND} -s 10" 1>&2 + exit 1 +} + +STAGING_PERCENTAGE=100 +while getopts s:? opt; do + case ${opt} in + s) STAGING_PERCENTAGE=${OPTARG} ;; + *) usage ;; + esac +done + +if ((STAGING_PERCENTAGE <= 0)) || ((STAGING_PERCENTAGE > 100)); then + echo "Staging percentage must be greater than 0 and no more than 100" 1>&2 + exit 1 +fi diff --git a/src/server_manager/scripts/finish_info_files.sh b/src/server_manager/scripts/finish_info_files.sh new file mode 100755 index 000000000..81491e69f --- /dev/null +++ b/src/server_manager/scripts/finish_info_files.sh @@ -0,0 +1,33 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +PLATFORM="-$1" +if [[ "${PLATFORM}" == "-win" ]]; then + PLATFORM="" +fi +readonly STAGING_PERCENTAGE="$2" +readonly BUILD_DIR='build/server_manager/electron_app/static' + +INFO_FILE_CHANNEL=$(src/server_manager/scripts/get_manager_release_channel.sh) +echo "stagingPercentage: ${STAGING_PERCENTAGE}" >> "${BUILD_DIR}/dist/${INFO_FILE_CHANNEL}${PLATFORM}.yml" + +# If we cut a staged mainline release, beta testers will take the update as well. +if [[ "${INFO_FILE_CHANNEL}" == "latest" ]]; then + echo "stagingPercentage: ${STAGING_PERCENTAGE}" >> "${BUILD_DIR}/dist/beta${PLATFORM}.yml" +fi + +# We don't support alpha releases +rm -f "${BUILD_DIR}/dist/alpha${PLATFORM}.yml" diff --git a/src/server_manager/scripts/get_electron_build_flags.mjs b/src/server_manager/scripts/get_electron_build_flags.mjs new file mode 100644 index 000000000..42338781f --- /dev/null +++ b/src/server_manager/scripts/get_electron_build_flags.mjs @@ -0,0 +1,59 @@ +// Copyright 2022 The Outline Authors +// +// 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. +import minimist from 'minimist'; +import url from 'url'; + +export async function getElectronBuildFlags(platform, buildMode) { + let buildFlags = [ + '--projectDir=build/server_manager/electron_app/static', + '--config=../../../../src/server_manager/electron_app/electron_builder.json', + '--publish=never', + ]; + + switch (platform) { + case 'linux': + buildFlags = ['--linux', ...buildFlags]; + break; + case 'windows': + buildFlags = ['--win', '--ia32', ...buildFlags]; + break; + case 'macos': + buildFlags = ['--mac', ...buildFlags]; + } + + if (buildMode === 'release') { + buildFlags = [ + ...buildFlags, + '--config.generateUpdatesFilesForAllChannels=true', + '--config.publish.provider=generic', + '--config.publish.url=https://s3.amazonaws.com/outline-releases/manager/', + ]; + } + + return buildFlags; +} + +async function main() { + const {_, buildMode} = minimist(process.argv); + + const platform = _[2]; + + console.log((await getElectronBuildFlags(platform, buildMode)).join(' ')); +} + +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + (async function () { + return main(); + })(); +} diff --git a/src/server_manager/scripts/get_manager_release_channel.sh b/src/server_manager/scripts/get_manager_release_channel.sh new file mode 100755 index 000000000..750cb0f14 --- /dev/null +++ b/src/server_manager/scripts/get_manager_release_channel.sh @@ -0,0 +1,22 @@ +#!/bin/bash -eu +# +# Copyright 2020 The Outline Authors +# +# 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. + +# If this isn't an alpha or beta build, `cut -s` will return an empty string +INFO_FILE_CHANNEL=$(node_modules/node-jq/bin/jq -r '.version' src/server_manager/package.json | cut -s -d'-' -f2) +if [[ -z "${INFO_FILE_CHANNEL}" ]]; then + INFO_FILE_CHANNEL=latest +fi +echo "${INFO_FILE_CHANNEL}" diff --git a/src/server_manager/scripts/get_version.mjs b/src/server_manager/scripts/get_version.mjs new file mode 100644 index 000000000..4f4711819 --- /dev/null +++ b/src/server_manager/scripts/get_version.mjs @@ -0,0 +1,36 @@ +// Copyright 2022 The Outline Authors +// +// 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. + +import url from "url"; +import path from "path"; +import {readFile} from "fs/promises"; + +const __filename = url.fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export async function get_version() { + const {version} = JSON.parse(await readFile(path.resolve(__dirname, "../package.json"))); + + return version; +} + +async function main() { + console.log(await get_version()); +} + +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + (async function() { + return main(); + })(); +} diff --git a/src/server_manager/test.action.sh b/src/server_manager/test.action.sh new file mode 100755 index 000000000..f592d39d8 --- /dev/null +++ b/src/server_manager/test.action.sh @@ -0,0 +1,28 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +readonly TEST_DIR="${BUILD_DIR}/js/server_manager/" +rm -rf "${TEST_DIR}" + +npm run action server_manager/web_app/build_install_script + +# Use commonjs modules, jasmine runs in node. +tsc -p "${ROOT_DIR}/src/server_manager" --outDir "${TEST_DIR}" --module commonjs +jasmine --config="${ROOT_DIR}/jasmine.json" + +npm run action server_manager/web_app/test + +rm -rf "${TEST_DIR}" diff --git a/src/server_manager/tsconfig.json b/src/server_manager/tsconfig.json new file mode 100644 index 000000000..b7a46b211 --- /dev/null +++ b/src/server_manager/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2018" + }, + "extends": "../../tsconfig.json", + "rootDir": ".", + "include": ["**/*.ts"], + "exclude": [ + "node_modules", + "web_app/gallery_app", + // FIXME: these tests fail with a runtime error because app.ts depends on + // polymer, which targets the browser and uses ES6 imports. + "web_app/app.spec.ts" + ] +} diff --git a/src/server_manager/types/electron-to-chromium.d.ts b/src/server_manager/types/electron-to-chromium.d.ts new file mode 100644 index 000000000..0ee32b18f --- /dev/null +++ b/src/server_manager/types/electron-to-chromium.d.ts @@ -0,0 +1,20 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +// Typings for: +// https://www.npmjs.com/package/electron-to-chromium + +declare module 'electron-to-chromium' { + export function electronToChromium(s: string): string; +} diff --git a/src/server_manager/types/jsonic.d.ts b/src/server_manager/types/jsonic.d.ts new file mode 100644 index 000000000..4a712ae13 --- /dev/null +++ b/src/server_manager/types/jsonic.d.ts @@ -0,0 +1,22 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +// Typings for: +// https://www.npmjs.com/package/jsonic + +declare module 'jsonic' { + function parse(s: string): {}; + namespace parse {} + export = parse; +} diff --git a/src/server_manager/types/preload.d.ts b/src/server_manager/types/preload.d.ts new file mode 100644 index 000000000..339e24b09 --- /dev/null +++ b/src/server_manager/types/preload.d.ts @@ -0,0 +1,44 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +// Functions made available to the renderer process via preload.ts. + +type SentryBreadcrumb = import('@sentry/electron').Breadcrumb; +declare function redactSentryBreadcrumbUrl(breadcrumb: SentryBreadcrumb): SentryBreadcrumb; + +type HttpRequest = import('../infrastructure/path_api').HttpRequest; +type HttpResponse = import('../infrastructure/path_api').HttpResponse; + +declare function fetchWithPin(request: HttpRequest, fingerprint: string): Promise; +declare function openImage(basename: string): void; +declare function onUpdateDownloaded(callback: () => void): void; + +// TODO: Move this back to digitalocean_oauth.ts, where it really belongs. +interface OauthSession { + // Resolves with the OAuth token if authentication was successful, otherwise rejects. + result: Promise; + // Returns true iff the session has been cancelled. + isCancelled(): boolean; + // Cancels the session, causing the result promise to reject and isCancelled to return true. + cancel(): void; +} + +declare function runDigitalOceanOauth(): OauthSession; + +declare function runGcpOauth(): OauthSession; + +declare function bringToFront(): void; + +// From base.webpack.js. +declare const outline: {gcpAuthEnabled: boolean}; diff --git a/src/server_manager/web_app/app.spec.ts b/src/server_manager/web_app/app.spec.ts new file mode 100644 index 000000000..fc62dbc73 --- /dev/null +++ b/src/server_manager/web_app/app.spec.ts @@ -0,0 +1,155 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import './ui_components/app-root'; + +import * as accounts from '../model/accounts'; +import * as server from '../model/server'; + +import {App, LAST_DISPLAYED_SERVER_STORAGE_KEY} from './app'; +import { + FakeCloudAccounts, + FakeDigitalOceanAccount, + FakeManualServerRepository, +} from './testing/models'; +import {AppRoot} from './ui_components/app-root'; +import {Region} from '../model/digitalocean'; + +// Define functions from preload.ts. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(global as any).onUpdateDownloaded = () => {}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(global as any).bringToFront = () => {}; + +// Inject app-root element into DOM once before each test. +beforeEach(() => { + document.body.innerHTML = ""; +}); + +describe('App', () => { + it('shows intro when starting with no manual servers or DigitalOcean token', async () => { + const appRoot = document.getElementById('appRoot') as AppRoot; + const app = createTestApp(appRoot); + await app.start(); + expect(appRoot.currentPage).toEqual('intro'); + }); + + it('will not create a manual server with invalid input', async () => { + // Create a new app with no existing servers or DigitalOcean token. + const appRoot = document.getElementById('appRoot') as AppRoot; + const app = createTestApp(appRoot); + await app.start(); + expect(appRoot.currentPage).toEqual('intro'); + await expectAsync(app.createManualServer('bad input')).toBeRejectedWithError(); + }); + + it('creates a manual server with valid input', async () => { + // Create a new app with no existing servers or DigitalOcean token. + const appRoot = document.getElementById('appRoot') as AppRoot; + const app = createTestApp(appRoot); + await app.start(); + expect(appRoot.currentPage).toEqual('intro'); + await app.createManualServer(JSON.stringify({certSha256: 'cert', apiUrl: 'url'})); + expect(appRoot.currentPage).toEqual('serverView'); + }); + + it('initially shows servers', async () => { + // Create fake servers and simulate their metadata being cached before creating the app. + const fakeAccount = new FakeDigitalOceanAccount(); + await fakeAccount.createServer(new Region('_fake-region-id')); + const cloudAccounts = new FakeCloudAccounts(fakeAccount); + + const manualServerRepo = new FakeManualServerRepository(); + await manualServerRepo.addServer({certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-1'}); + await manualServerRepo.addServer({certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-2'}); + + const appRoot = document.getElementById('appRoot') as AppRoot; + expect(appRoot.serverList.length).toEqual(0); + const app = createTestApp(appRoot, cloudAccounts, manualServerRepo); + + await app.start(); + // Validate that server metadata is shown. + const managedServers = await fakeAccount.listServers(); + expect(managedServers.length).toEqual(1); + const manualServers = await manualServerRepo.listServers(); + expect(manualServers.length).toEqual(2); + await appRoot.getServerView(''); + const serverList = appRoot.serverList; + + console.log(`managedServers.length: ${managedServers.length}`); + console.log(`manualServers.length: ${manualServers.length}`); + + expect(serverList.length).toEqual(manualServers.length + managedServers.length); + expect(serverList).toContain(jasmine.objectContaining({id: 'fake-manual-server-api-url-1'})); + expect(serverList).toContain(jasmine.objectContaining({id: 'fake-manual-server-api-url-2'})); + expect(serverList).toContain(jasmine.objectContaining({id: '_fake-region-id'})); + }); + + it('initially shows the last selected server', async () => { + const LAST_DISPLAYED_SERVER_ID = 'fake-manual-server-api-url-1'; + const manualServerRepo = new FakeManualServerRepository(); + const lastDisplayedServer = await manualServerRepo.addServer({ + certSha256: 'cert', + apiUrl: LAST_DISPLAYED_SERVER_ID, + }); + await manualServerRepo.addServer({certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-2'}); + localStorage.setItem('lastDisplayedServer', LAST_DISPLAYED_SERVER_ID); + const appRoot = document.getElementById('appRoot') as AppRoot; + const app = createTestApp(appRoot, null, manualServerRepo); + await app.start(); + expect(appRoot.currentPage).toEqual('serverView'); + expect(appRoot.selectedServerId).toEqual(lastDisplayedServer.getManagementApiUrl()); + }); + + it('shows progress screen once DigitalOcean droplets are created', async () => { + // Start the app with a fake DigitalOcean token. + const appRoot = document.getElementById('appRoot') as AppRoot; + const cloudAccounts = new FakeCloudAccounts(new FakeDigitalOceanAccount()); + const app = createTestApp(appRoot, cloudAccounts); + await app.start(); + await app.createDigitalOceanServer(new Region('_fake-region-id')); + expect(appRoot.currentPage).toEqual('serverView'); + const view = await appRoot.getServerView(appRoot.selectedServerId); + expect(view.selectedPage).toEqual('progressView'); + }); + + it('shows progress screen when starting with DigitalOcean servers still being created', async () => { + const appRoot = document.getElementById('appRoot') as AppRoot; + const fakeAccount = new FakeDigitalOceanAccount(); + const server = await fakeAccount.createServer(new Region('_fake-region-id')); + const cloudAccounts = new FakeCloudAccounts(fakeAccount); + const app = createTestApp(appRoot, cloudAccounts, null); + // Sets last displayed server. + localStorage.setItem(LAST_DISPLAYED_SERVER_STORAGE_KEY, server.getId()); + await app.start(); + expect(appRoot.currentPage).toEqual('serverView'); + const view = await appRoot.getServerView(appRoot.selectedServerId); + expect(view.selectedPage).toEqual('progressView'); + }); +}); + +function createTestApp( + appRoot: AppRoot, + cloudAccounts?: accounts.CloudAccounts, + manualServerRepo?: server.ManualServerRepository +) { + const VERSION = '0.0.1'; + if (!cloudAccounts) { + cloudAccounts = new FakeCloudAccounts(); + } + if (!manualServerRepo) { + manualServerRepo = new FakeManualServerRepository(); + } + return new App(appRoot, VERSION, manualServerRepo, cloudAccounts); +} diff --git a/src/server_manager/web_app/app.ts b/src/server_manager/web_app/app.ts new file mode 100644 index 000000000..ecc8fed00 --- /dev/null +++ b/src/server_manager/web_app/app.ts @@ -0,0 +1,1315 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as Sentry from '@sentry/electron/renderer'; +import * as semver from 'semver'; + +import * as digitalocean_api from '../cloud/digitalocean_api'; +import * as path_api from '../infrastructure/path_api'; +import {sleep} from '../infrastructure/sleep'; +import * as accounts from '../model/accounts'; +import * as digitalocean from '../model/digitalocean'; +import * as gcp from '../model/gcp'; +import * as server_model from '../model/server'; + +import {DisplayDataAmount, displayDataAmountToBytes} from './data_formatting'; +import {filterOptions, getShortName} from './location_formatting'; +import {parseManualServerConfig} from './management_urls'; +import {HttpError} from '../cloud/gcp_api'; + +import type {CloudLocation} from '../model/location'; +import type {AppRoot, ServerListEntry} from './ui_components/app-root'; +import type {FeedbackDetail} from './ui_components/outline-feedback-dialog'; +import type {DisplayAccessKey, ServerView} from './ui_components/outline-server-view'; +import {CustomError} from '../infrastructure/custom_error'; + +// The Outline DigitalOcean team's referral code: +// https://www.digitalocean.com/help/referral-program/ +//const UNUSED_DIGITALOCEAN_REFERRAL_CODE = '5ddb4219b716'; + +const CHANGE_KEYS_PORT_VERSION = '1.0.0'; +const DATA_LIMITS_VERSION = '1.1.0'; +const CHANGE_HOSTNAME_VERSION = '1.2.0'; +const KEY_SETTINGS_VERSION = '1.6.0'; +const MAX_ACCESS_KEY_DATA_LIMIT_BYTES = 50 * 10 ** 9; // 50GB +const CANCELLED_ERROR = new Error('Cancelled'); +export const LAST_DISPLAYED_SERVER_STORAGE_KEY = 'lastDisplayedServer'; + +// todo (#1311): we are referencing `@sentry/electron` which won't work for +// web_app. It's ok for now cuz we don't need to enable Sentry in +// web_app, but a better solution is to have separate two entry +// points: electron_main (uses `@sentry/electron`) and web_main +// (uses `@sentry/browser`). +// For all other Sentry config see the main process. +Sentry.init({ + beforeBreadcrumb: + typeof redactSentryBreadcrumbUrl === 'function' ? redactSentryBreadcrumbUrl : null, +}); + +function displayDataAmountToDataLimit( + dataAmount: DisplayDataAmount +): server_model.DataLimit | null { + if (!dataAmount) { + return null; + } + + return {bytes: displayDataAmountToBytes(dataAmount)}; +} + +// Compute the suggested data limit based on the server's transfer capacity and number of access +// keys. +async function computeDefaultDataLimit( + server: server_model.Server, + accessKeys?: server_model.AccessKey[] +): Promise { + try { + // Assume non-managed servers have a data transfer capacity of 1TB. + let serverTransferCapacity: server_model.DataAmount = {terabytes: 1}; + if (isManagedServer(server)) { + serverTransferCapacity = + server.getHost().getMonthlyOutboundTransferLimit() ?? serverTransferCapacity; + } + if (!accessKeys) { + accessKeys = await server.listAccessKeys(); + } + let dataLimitBytes = (serverTransferCapacity.terabytes * 10 ** 12) / (accessKeys.length || 1); + if (dataLimitBytes > MAX_ACCESS_KEY_DATA_LIMIT_BYTES) { + dataLimitBytes = MAX_ACCESS_KEY_DATA_LIMIT_BYTES; + } + return {bytes: dataLimitBytes}; + } catch (e) { + console.error(`Failed to compute default access key data limit: ${e}`); + return {bytes: MAX_ACCESS_KEY_DATA_LIMIT_BYTES}; + } +} + +// Returns whether the user has seen a notification for the updated feature metrics data collection +// policy. +function hasSeenFeatureMetricsNotification(): boolean { + return ( + !!window.localStorage.getItem('dataLimitsHelpBubble-dismissed') && + !!window.localStorage.getItem('dataLimits-feature-collection-notification') + ); +} + +async function showHelpBubblesOnce(serverView: ServerView) { + if (!window.localStorage.getItem('getConnectedHelpBubble-dismissed')) { + await serverView.showGetConnectedHelpBubble(); + window.localStorage.setItem('getConnectedHelpBubble-dismissed', 'true'); + } + if (!window.localStorage.getItem('addAccessKeyHelpBubble-dismissed')) { + await serverView.showAddAccessKeyHelpBubble(); + window.localStorage.setItem('addAccessKeyHelpBubble-dismissed', 'true'); + } + if ( + !window.localStorage.getItem('dataLimitsHelpBubble-dismissed') && + serverView.supportsDefaultDataLimit + ) { + await serverView.showDataLimitsHelpBubble(); + window.localStorage.setItem('dataLimitsHelpBubble-dismissed', 'true'); + } +} + +function isManagedServer( + testServer: server_model.Server +): testServer is server_model.ManagedServer { + return !!(testServer as server_model.ManagedServer).getHost; +} + +function isManualServer(testServer: server_model.Server): testServer is server_model.ManualServer { + return !!(testServer as server_model.ManualServer).forget; +} + +// Error thrown when a shadowbox server cannot be reached (e.g. due to Firewall) +class UnreachableServerError extends CustomError { + constructor(message?: string) { + super(message); + } +} + +export class App { + private digitalOceanAccount: digitalocean.Account; + private gcpAccount: gcp.Account; + private selectedServer: server_model.Server; + private idServerMap = new Map(); + + constructor( + private appRoot: AppRoot, + private readonly version: string, + private manualServerRepository: server_model.ManualServerRepository, + private cloudAccounts: accounts.CloudAccounts + ) { + appRoot.setAttribute('outline-version', this.version); + + appRoot.addEventListener('ConnectDigitalOceanAccountRequested', (_: CustomEvent) => { + this.handleConnectDigitalOceanAccountRequest(); + }); + appRoot.addEventListener('CreateDigitalOceanServerRequested', (_: CustomEvent) => { + const digitalOceanAccount = this.cloudAccounts.getDigitalOceanAccount(); + if (digitalOceanAccount) { + this.showDigitalOceanCreateServer(digitalOceanAccount); + } else { + console.error('Access token not found for server creation'); + this.handleConnectDigitalOceanAccountRequest(); + } + }); + appRoot.addEventListener('ConnectGcpAccountRequested', async (_: CustomEvent) => + this.handleConnectGcpAccountRequest() + ); + appRoot.addEventListener('CreateGcpServerRequested', async (_: CustomEvent) => { + this.appRoot.getAndShowGcpCreateServerApp().start(this.gcpAccount); + }); + appRoot.addEventListener('GcpServerCreated', (event: CustomEvent) => { + const server = event.detail.server; + this.addServer(this.gcpAccount.getId(), server); + this.showServer(server); + }); + appRoot.addEventListener('DigitalOceanSignOutRequested', (_: CustomEvent) => { + this.disconnectDigitalOceanAccount(); + this.showIntro(); + }); + appRoot.addEventListener('GcpSignOutRequested', (_: CustomEvent) => { + this.disconnectGcpAccount(); + this.showIntro(); + }); + + appRoot.addEventListener('SetUpDigitalOceanServerRequested', (event: CustomEvent) => { + this.createDigitalOceanServer(event.detail.region); + }); + + appRoot.addEventListener('DeleteServerRequested', (event: CustomEvent) => { + this.deleteServer(event.detail.serverId); + }); + + appRoot.addEventListener('ForgetServerRequested', (event: CustomEvent) => { + this.forgetServer(event.detail.serverId); + }); + + appRoot.addEventListener('AddAccessKeyRequested', (_: CustomEvent) => { + this.addAccessKey(); + }); + + appRoot.addEventListener('RemoveAccessKeyRequested', (event: CustomEvent) => { + this.removeAccessKey(event.detail.accessKeyId); + }); + + appRoot.addEventListener( + 'OpenPerKeyDataLimitDialogRequested', + this.openPerKeyDataLimitDialog.bind(this) + ); + + appRoot.addEventListener('RenameAccessKeyRequested', (event: CustomEvent) => { + this.renameAccessKey(event.detail.accessKeyId, event.detail.newName, event.detail.entry); + }); + + appRoot.addEventListener('SetDefaultDataLimitRequested', (event: CustomEvent) => { + this.setDefaultDataLimit(displayDataAmountToDataLimit(event.detail.limit)); + }); + + appRoot.addEventListener('RemoveDefaultDataLimitRequested', (_: CustomEvent) => { + this.removeDefaultDataLimit(); + }); + + appRoot.addEventListener('ChangePortForNewAccessKeysRequested', (event: CustomEvent) => { + this.setPortForNewAccessKeys(event.detail.validatedInput, event.detail.ui); + }); + + appRoot.addEventListener('ChangeHostnameForAccessKeysRequested', (event: CustomEvent) => { + this.setHostnameForAccessKeys(event.detail.validatedInput, event.detail.ui); + }); + + // The UI wants us to validate a server management URL. + // "Reply" by setting a field on the relevant template. + appRoot.addEventListener('ManualServerEdited', (event: CustomEvent) => { + let isValid = true; + try { + parseManualServerConfig(event.detail.userInput); + } catch (e) { + isValid = false; + } + const manualServerEntryEl = appRoot.getManualServerEntry(); + manualServerEntryEl.enableDoneButton = isValid; + }); + + appRoot.addEventListener('ManualServerEntered', (event: CustomEvent) => { + const userInput = event.detail.userInput; + const manualServerEntryEl = appRoot.getManualServerEntry(); + this.createManualServer(userInput) + .then(() => { + // Clear fields on outline-manual-server-entry (e.g. dismiss the connecting popup). + manualServerEntryEl.clear(); + }) + .catch((e: Error) => { + // Remove the progress indicator. + manualServerEntryEl.showConnection = false; + // Display either error dialog or feedback depending on error type. + if (e instanceof UnreachableServerError) { + const errorTitle = appRoot.localize('error-server-unreachable-title'); + const errorMessage = appRoot.localize('error-server-unreachable'); + this.appRoot.showManualServerError(errorTitle, errorMessage); + } else { + // TODO(alalama): with UI validation, this code path never gets executed. Remove? + let errorMessage = ''; + if (e.message) { + errorMessage += `${e.message}\n`; + } + if (userInput) { + errorMessage += userInput; + } + appRoot.openManualInstallFeedback(errorMessage); + } + }); + }); + + appRoot.addEventListener('EnableMetricsRequested', (_: CustomEvent) => { + this.setMetricsEnabled(true); + }); + + appRoot.addEventListener('DisableMetricsRequested', (_: CustomEvent) => { + this.setMetricsEnabled(false); + }); + + appRoot.addEventListener('SubmitFeedback', (event: CustomEvent) => { + const detail: FeedbackDetail = event.detail; + try { + Sentry.captureEvent({ + message: detail.userFeedback, + user: {email: detail.userEmail}, + tags: {category: detail.feedbackCategory, cloudProvider: detail.cloudProvider}, + }); + appRoot.showNotification(appRoot.localize('notification-feedback-thanks')); + } catch (e) { + console.error(`Failed to submit feedback: ${e}`); + appRoot.showError(appRoot.localize('error-feedback')); + } + }); + + appRoot.addEventListener('SetLanguageRequested', (event: CustomEvent) => { + this.setAppLanguage(event.detail.languageCode, event.detail.languageDir); + }); + + appRoot.addEventListener('ServerRenameRequested', (event: CustomEvent) => { + this.renameServer(event.detail.newName); + }); + + appRoot.addEventListener('CancelServerCreationRequested', (_: CustomEvent) => { + this.cancelServerCreation(this.selectedServer); + }); + + appRoot.addEventListener('OpenImageRequested', (event: CustomEvent) => { + openImage(event.detail.imagePath); + }); + + appRoot.addEventListener('OpenShareDialogRequested', (event: CustomEvent) => { + const accessKey = event.detail.accessKey; + this.appRoot.openShareDialog(accessKey, this.getS3InviteUrl(accessKey)); + }); + + appRoot.addEventListener('OpenGetConnectedDialogRequested', (event: CustomEvent) => { + this.appRoot.openGetConnectedDialog(this.getS3InviteUrl(event.detail.accessKey, true)); + }); + + appRoot.addEventListener('ShowServerRequested', (event: CustomEvent) => { + const server = this.getServerById(event.detail.displayServerId); + if (server) { + this.showServer(server); + } else { + // This should never happen if we are managine the list correctly. + console.error( + `Could not find server for display server ID ${event.detail.displayServerId}` + ); + } + }); + + onUpdateDownloaded(this.displayAppUpdateNotification.bind(this)); + } + + // Shows the intro screen with overview and options to sign in or sign up. + private showIntro() { + this.appRoot.showIntro(); + } + + private displayAppUpdateNotification() { + this.appRoot.showNotification(this.appRoot.localize('notification-app-update'), 60000); + } + + async start(): Promise { + this.showIntro(); + + // Load connected accounts and servers. + await Promise.all([ + this.loadDigitalOceanAccount(this.cloudAccounts.getDigitalOceanAccount()), + this.loadGcpAccount(this.cloudAccounts.getGcpAccount()), + this.loadManualServers(), + ]); + + // Show last displayed server, if any. + const serverIdToSelect = localStorage.getItem(LAST_DISPLAYED_SERVER_STORAGE_KEY); + if (serverIdToSelect) { + const serverToShow = this.getServerById(serverIdToSelect); + if (serverToShow) { + this.showServer(serverToShow); + } + } + } + + private async loadDigitalOceanAccount( + digitalOceanAccount: digitalocean.Account + ): Promise { + if (!digitalOceanAccount) { + return []; + } + let showedWarning = false; + try { + this.digitalOceanAccount = digitalOceanAccount; + this.appRoot.digitalOceanAccount = { + id: this.digitalOceanAccount.getId(), + name: await this.digitalOceanAccount.getName(), + }; + const status = await this.digitalOceanAccount.getStatus(); + if (status.warning) { + this.showDigitalOceanWarning(status); + showedWarning = true; + } + const servers = await this.digitalOceanAccount.listServers(); + for (const server of servers) { + this.addServer(this.digitalOceanAccount.getId(), server); + } + return servers; + } catch (error) { + // TODO(fortuna): Handle expired token. + if (!showedWarning) { + this.appRoot.showError(this.appRoot.localize('error-do-account-info')); + } + console.error('Failed to load DigitalOcean Account:', error); + } + return []; + } + + private showDigitalOceanWarning(status: digitalocean.Status) { + this.appRoot.showError(this.appRoot.localize('error-do-warning', 'message', status.warning)); + } + + private async loadGcpAccount(gcpAccount: gcp.Account): Promise { + if (!gcpAccount) { + return []; + } + + this.gcpAccount = gcpAccount; + this.appRoot.gcpAccount = { + id: this.gcpAccount.getId(), + name: await this.gcpAccount.getName(), + }; + + const result = []; + const gcpProjects = await this.gcpAccount.listProjects(); + for (const gcpProject of gcpProjects) { + try { + const servers = await this.gcpAccount.listServers(gcpProject.id); + for (const server of servers) { + this.addServer(this.gcpAccount.getId(), server); + result.push(server); + } + } catch (e) { + if (e instanceof HttpError && e.getStatusCode() === 403) { + // listServers() throws an HTTP 403 if the outline project has been + // created but the billing account has been removed, which can + // easily happen after the free trial period expires. This is + // harmless, because a project with no billing cannot contain any + // servers, and the GCP server creation flow will check and correct + // the billing account setup. + console.warn(`Ignoring HTTP 403 for GCP project "${gcpProject.id}"`); + } else { + throw e; + } + } + } + return result; + } + + private async loadManualServers() { + for (const server of await this.manualServerRepository.listServers()) { + this.addServer(null, server); + } + } + + private makeServerListEntry(accountId: string, server: server_model.Server): ServerListEntry { + return { + id: server.getId(), + accountId, + name: this.makeDisplayName(server), + isSynced: !!server.getName(), + }; + } + + private makeDisplayName(server: server_model.Server): string { + let name = server.getName() ?? server.getHostnameForAccessKeys(); + if (!name) { + let cloudLocation = null; + // Newly created servers will not have a name. + if (isManagedServer(server)) { + cloudLocation = server.getHost().getCloudLocation(); + } + name = this.makeLocalizedServerName(cloudLocation); + } + return name; + } + + private addServer(accountId: string, server: server_model.Server): void { + console.log('Loading server', server); + this.idServerMap.set(server.getId(), server); + const serverEntry = this.makeServerListEntry(accountId, server); + this.appRoot.serverList = this.appRoot.serverList.concat([serverEntry]); + + if (isManagedServer(server)) { + this.setServerProgressView(server); + } + + // Once the server is added to the list, do the rest asynchronously. + setTimeout(async () => { + // Wait for server config to load, then update the server view and list. + if (isManagedServer(server)) { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _ of server.monitorInstallProgress()) { + /* empty */ + } + } catch (error) { + if (error instanceof server_model.ServerInstallCanceledError) { + // User clicked "Cancel" on the loading screen. + return; + } + console.log('Server creation failed', error); + this.appRoot.showError(this.appRoot.localize('error-server-creation')); + } + } + await this.updateServerView(server); + // This has to run after updateServerView because it depends on the isHealthy() call. + // TODO(fortuna): Better handle state changes. + this.updateServerEntry(server); + }, 0); + } + + private removeServer(serverId: string): void { + this.idServerMap.delete(serverId); + this.appRoot.serverList = this.appRoot.serverList.filter((ds) => ds.id !== serverId); + if (this.appRoot.selectedServerId === serverId) { + this.appRoot.selectedServerId = ''; + this.selectedServer = null; + localStorage.removeItem(LAST_DISPLAYED_SERVER_STORAGE_KEY); + } + } + + private updateServerEntry(server: server_model.Server): void { + this.appRoot.serverList = this.appRoot.serverList.map((ds) => + ds.id === server.getId() ? this.makeServerListEntry(ds.accountId, server) : ds + ); + } + + private getServerById(serverId: string): server_model.Server { + return this.idServerMap.get(serverId); + } + + // Returns a promise that resolves when the account is active. + // Throws CANCELLED_ERROR on cancellation, and the error on failure. + private async ensureActiveDigitalOceanAccount( + digitalOceanAccount: digitalocean.Account + ): Promise { + let cancelled = false; + let activatingAccount = false; + + // TODO(fortuna): Provide a cancel action instead of sign out. + const signOutAction = () => { + cancelled = true; + this.disconnectDigitalOceanAccount(); + }; + const oauthUi = this.appRoot.getDigitalOceanOauthFlow(signOutAction); + for (;;) { + const status = await this.digitalOceanRetry(async () => { + if (cancelled) { + throw CANCELLED_ERROR; + } + return await digitalOceanAccount.getStatus(); + }); + if (status.needsBillingInfo) { + oauthUi.showBilling(); + } else if (status.needsEmailVerification) { + oauthUi.showEmailVerification(); + } else { + if (status.warning) { + this.showDigitalOceanWarning(status); + } + bringToFront(); + if (activatingAccount) { + // Show the 'account active' screen for a few seconds if the account was activated + // during this session. + oauthUi.showAccountActive(); + await sleep(1500); + } + return; + } + this.appRoot.showDigitalOceanOauthFlow(); + activatingAccount = true; + await sleep(1000); + if (this.appRoot.currentPage !== 'digitalOceanOauth') { + // The user navigated away. + cancelled = true; + } + if (cancelled) { + throw CANCELLED_ERROR; + } + } + } + + // Intended to add a "retry or re-authenticate?" prompt to DigitalOcean + // operations. Specifically, any operation rejecting with an digitalocean_api.XhrError will + // result in a dialog asking the user whether to retry the operation or + // re-authenticate against DigitalOcean. + // This is necessary because an access token may expire or be revoked at + // any time and there's no way to programmatically distinguish network errors + // from CORS-type errors (see the comments in DigitalOceanSession for more + // information). + // TODO: It would be great if, once the user has re-authenticated, we could + // return the UI to its exact prior state. Fortunately, the most likely + // time to discover an invalid access token is when the application + // starts. + private digitalOceanRetry = (f: () => Promise): Promise => { + return f().catch((e) => { + if (!(e instanceof digitalocean_api.XhrError)) { + return Promise.reject(e); + } + + return new Promise((resolve, reject) => { + this.appRoot.showConnectivityDialog((retry: boolean) => { + if (retry) { + this.digitalOceanRetry(f).then(resolve, reject); + } else { + this.disconnectDigitalOceanAccount(); + reject(e); + } + }); + }); + }); + }; + + // Runs the DigitalOcean OAuth flow and returns the API access token. + // Throws CANCELLED_ERROR on cancellation, or the error in case of failure. + private async runDigitalOceanOauthFlow(): Promise { + const oauth = runDigitalOceanOauth(); + const handleOauthFlowCancelled = () => { + oauth.cancel(); + this.disconnectDigitalOceanAccount(); + this.showIntro(); + }; + this.appRoot.getAndShowDigitalOceanOauthFlow(handleOauthFlowCancelled); + try { + // DigitalOcean tokens expire after 30 days, unless they are manually + // revoked by the user. After 30 days the user will have to sign into + // DigitalOcean again. Note we cannot yet use DigitalOcean refresh + // tokens, as they require a client_secret to be stored on a server and + // not visible to end users in client-side JS. More details at: + // https://developers.digitalocean.com/documentation/oauth/#refresh-token-flow + return await oauth.result; + } catch (error) { + if (oauth.isCancelled()) { + throw CANCELLED_ERROR; + } else { + throw error; + } + } + } + + // Runs the GCP OAuth flow and returns the API refresh token (which can be + // exchanged for an access token). + // Throws CANCELLED_ERROR on cancellation, or the error in case of failure. + private async runGcpOauthFlow(): Promise { + const oauth = runGcpOauth(); + const handleOauthFlowCancelled = () => { + oauth.cancel(); + this.disconnectGcpAccount(); + this.showIntro(); + }; + this.appRoot.getAndShowGcpOauthFlow(handleOauthFlowCancelled); + try { + return await oauth.result; + } catch (error) { + if (oauth.isCancelled()) { + throw CANCELLED_ERROR; + } else { + throw error; + } + } + } + + private async handleConnectDigitalOceanAccountRequest(): Promise { + let digitalOceanAccount: digitalocean.Account = null; + try { + const accessToken = await this.runDigitalOceanOauthFlow(); + bringToFront(); + digitalOceanAccount = this.cloudAccounts.connectDigitalOceanAccount(accessToken); + } catch (error) { + this.disconnectDigitalOceanAccount(); + this.showIntro(); + bringToFront(); + if (error !== CANCELLED_ERROR) { + console.error(`DigitalOcean authentication failed: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-do-auth')); + } + return; + } + + const doServers = await this.loadDigitalOceanAccount(digitalOceanAccount); + if (doServers.length > 0) { + this.showServer(doServers[0]); + } else { + await this.showDigitalOceanCreateServer(this.digitalOceanAccount); + } + } + + private async handleConnectGcpAccountRequest(): Promise { + let gcpAccount: gcp.Account = null; + try { + const refreshToken = await this.runGcpOauthFlow(); + bringToFront(); + gcpAccount = this.cloudAccounts.connectGcpAccount(refreshToken); + } catch (error) { + this.disconnectGcpAccount(); + this.showIntro(); + bringToFront(); + if (error !== CANCELLED_ERROR) { + console.error(`GCP authentication failed: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-gcp-auth')); + } + return; + } + + const gcpServers = await this.loadGcpAccount(gcpAccount); + if (gcpServers.length > 0) { + this.showServer(gcpServers[0]); + } else { + this.appRoot.getAndShowGcpCreateServerApp().start(this.gcpAccount); + } + } + + // Clears the DigitalOcean credentials and returns to the intro screen. + private disconnectDigitalOceanAccount(): void { + if (!this.digitalOceanAccount) { + // Not connected. + return; + } + const accountId = this.digitalOceanAccount.getId(); + this.cloudAccounts.disconnectDigitalOceanAccount(); + this.digitalOceanAccount = null; + for (const serverEntry of this.appRoot.serverList) { + if (serverEntry.accountId === accountId) { + this.removeServer(serverEntry.id); + } + } + this.appRoot.digitalOceanAccount = null; + } + + // Clears the GCP credentials and returns to the intro screen. + private disconnectGcpAccount(): void { + if (!this.gcpAccount) { + // Not connected. + return; + } + const accountId = this.gcpAccount.getId(); + this.cloudAccounts.disconnectGcpAccount(); + this.gcpAccount = null; + for (const serverEntry of this.appRoot.serverList) { + if (serverEntry.accountId === accountId) { + this.removeServer(serverEntry.id); + } + } + this.appRoot.gcpAccount = null; + } + + // Opens the screen to create a server. + private async showDigitalOceanCreateServer( + digitalOceanAccount: digitalocean.Account + ): Promise { + try { + await this.ensureActiveDigitalOceanAccount(digitalOceanAccount); + } catch (error) { + if (this.appRoot.currentPage === 'digitalOceanOauth') { + this.showIntro(); + } + if (error !== CANCELLED_ERROR) { + console.error('Failed to validate DigitalOcean account', error); + this.appRoot.showError(this.appRoot.localize('error-do-account-info')); + } + return; + } + + try { + const status = await digitalOceanAccount.getStatus(); + if (status.hasReachedLimit) { + this.appRoot.showError(this.appRoot.localize('error-do-limit', 'num', status.dropletLimit)); + return; // Don't proceed to the region picker. + } + } catch (e) { + console.error('Failed to check droplet limit status', e); + } + + try { + const regionPicker = this.appRoot.getAndShowRegionPicker(); + const map = await this.digitalOceanRetry(() => { + return this.digitalOceanAccount.listLocations(); + }); + regionPicker.options = filterOptions(map); + } catch (e) { + console.error(`Failed to get list of available regions: ${e}`); + this.appRoot.showError(this.appRoot.localize('error-do-regions')); + } + } + + // Returns a promise which fulfills once the DigitalOcean droplet is created. + // Shadowbox may not be fully installed once this promise is fulfilled. + public async createDigitalOceanServer(region: digitalocean.Region): Promise { + try { + const serverName = this.makeLocalizedServerName(region); + const server = await this.digitalOceanRetry(() => { + return this.digitalOceanAccount.createServer(region, serverName); + }); + this.addServer(this.digitalOceanAccount.getId(), server); + this.showServer(server); + } catch (error) { + console.error('Error from createDigitalOceanServer', error); + this.appRoot.showError(this.appRoot.localize('error-server-creation')); + } + } + + private makeLocalizedServerName(cloudLocation: CloudLocation): string { + const placeName = getShortName(cloudLocation, this.appRoot.localize as (id: string) => string); + return this.appRoot.localize('server-name', 'serverLocation', placeName); + } + + public showServer(server: server_model.Server): void { + this.selectedServer = server; + this.appRoot.selectedServerId = server.getId(); + localStorage.setItem(LAST_DISPLAYED_SERVER_STORAGE_KEY, server.getId()); + this.appRoot.showServerView(); + } + + private async updateServerView(server: server_model.Server): Promise { + if (await server.isHealthy()) { + this.setServerManagementView(server); + } else { + this.setServerUnreachableView(server); + } + } + + // Show the server management screen. Assumes the server is healthy. + private async setServerManagementView(server: server_model.Server): Promise { + // Show view and initialize fields from selectedServer. + const view = await this.appRoot.getServerView(server.getId()); + const version = server.getVersion(); + view.selectedPage = 'managementView'; + view.metricsId = server.getMetricsId(); + view.serverHostname = server.getHostnameForAccessKeys(); + view.serverManagementApiUrl = server.getManagementApiUrl(); + view.serverPortForNewAccessKeys = server.getPortForNewAccessKeys(); + view.serverCreationDate = server.getCreatedDate(); + view.serverVersion = version; + view.defaultDataLimitBytes = server.getDefaultDataLimit()?.bytes; + view.isDefaultDataLimitEnabled = view.defaultDataLimitBytes !== undefined; + view.showFeatureMetricsDisclaimer = + server.getMetricsEnabled() && + !server.getDefaultDataLimit() && + !hasSeenFeatureMetricsNotification(); + + if (version) { + view.isAccessKeyPortEditable = semver.gte(version, CHANGE_KEYS_PORT_VERSION); + view.supportsDefaultDataLimit = semver.gte(version, DATA_LIMITS_VERSION); + view.isHostnameEditable = semver.gte(version, CHANGE_HOSTNAME_VERSION); + view.hasPerKeyDataLimitDialog = semver.gte(version, KEY_SETTINGS_VERSION); + } + + if (isManagedServer(server)) { + const host = server.getHost(); + view.monthlyCost = host.getMonthlyCost()?.usd; + view.monthlyOutboundTransferBytes = + host.getMonthlyOutboundTransferLimit()?.terabytes * 10 ** 12; + view.cloudLocation = host.getCloudLocation(); + } + + view.metricsEnabled = server.getMetricsEnabled(); + + // Asynchronously load "My Connection" and other access keys in order to not block showing the + // server. + setTimeout(async () => { + this.showMetricsOptInWhenNeeded(server); + try { + const serverAccessKeys = await server.listAccessKeys(); + view.accessKeyRows = serverAccessKeys.map(this.convertToUiAccessKey.bind(this)); + if (view.defaultDataLimitBytes === undefined) { + view.defaultDataLimitBytes = ( + await computeDefaultDataLimit(server, serverAccessKeys) + )?.bytes; + } + // Show help bubbles once the page has rendered. + setTimeout(() => { + showHelpBubblesOnce(view); + }, 250); + } catch (error) { + console.error(`Failed to load access keys: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-keys-get')); + } + this.showTransferStats(server, view); + }, 0); + } + + private async setServerUnreachableView(server: server_model.Server): Promise { + // Display the unreachable server state within the server view. + const serverId = server.getId(); + const serverView = await this.appRoot.getServerView(serverId); + serverView.selectedPage = 'unreachableView'; + serverView.retryDisplayingServer = async () => { + await this.updateServerView(server); + }; + } + + private async setServerProgressView(server: server_model.ManagedServer): Promise { + const view = await this.appRoot.getServerView(server.getId()); + view.serverName = this.makeDisplayName(server); + view.selectedPage = 'progressView'; + try { + for await (view.installProgress of server.monitorInstallProgress()) { + /* empty */ + } + } catch { + // Ignore any errors; they will be handled by `this.addServer`. + } + } + + private showMetricsOptInWhenNeeded(selectedServer: server_model.Server) { + const showMetricsOptInOnce = () => { + // Sanity check to make sure the running server is still displayed, i.e. + // it hasn't been deleted. + if (this.selectedServer !== selectedServer) { + return; + } + // Show the metrics opt in prompt if the server has not already opted in, + // and if they haven't seen the prompt yet according to localStorage. + const storageKey = selectedServer.getMetricsId() + '-prompted-for-metrics'; + if (!selectedServer.getMetricsEnabled() && !localStorage.getItem(storageKey)) { + this.appRoot.showMetricsDialogForNewServer(); + localStorage.setItem(storageKey, 'true'); + } + }; + + // Calculate milliseconds passed since server creation. + const createdDate = selectedServer.getCreatedDate(); + const now = new Date(); + const msSinceCreation = now.getTime() - createdDate.getTime(); + + // Show metrics opt-in once ONE_DAY_IN_MS has passed since server creation. + const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000; + if (msSinceCreation >= ONE_DAY_IN_MS) { + showMetricsOptInOnce(); + } else { + setTimeout(showMetricsOptInOnce, ONE_DAY_IN_MS - msSinceCreation); + } + } + + private async refreshTransferStats(selectedServer: server_model.Server, serverView: ServerView) { + try { + const usageMap = await selectedServer.getDataUsage(); + const keyTransfers = [...usageMap.values()]; + let totalInboundBytes = 0; + for (const accessKeyBytes of keyTransfers) { + totalInboundBytes += accessKeyBytes; + } + serverView.totalInboundBytes = totalInboundBytes; + + // Update all the displayed access keys, even if usage didn't change, in case data limits did. + let keyTransferMax = 0; + let dataLimitMax = selectedServer.getDefaultDataLimit()?.bytes ?? 0; + for (const key of await selectedServer.listAccessKeys()) { + serverView.updateAccessKeyRow(key.id, { + transferredBytes: usageMap.get(key.id) ?? 0, + dataLimitBytes: key.dataLimit?.bytes, + }); + keyTransferMax = Math.max(keyTransferMax, usageMap.get(key.id) ?? 0); + dataLimitMax = Math.max(dataLimitMax, key.dataLimit?.bytes ?? 0); + } + serverView.baselineDataTransfer = Math.max(keyTransferMax, dataLimitMax); + } catch (e) { + // Since failures are invisible to users we generally want exceptions here to bubble + // up and trigger a Sentry report. The exception is network errors, about which we can't + // do much (note: ShadowboxServer generates a breadcrumb for failures regardless which + // will show up when someone explicitly submits feedback). + // TODO(fortuna): the model is leaking implementation details here. We should clean this up + // Perhaps take a more event-based approach. + if (e instanceof path_api.ServerApiError && e.isNetworkError()) { + return; + } + throw e; + } + } + + private showTransferStats(selectedServer: server_model.Server, serverView: ServerView) { + this.refreshTransferStats(selectedServer, serverView); + // Get transfer stats once per minute for as long as server is selected. + const statsRefreshRateMs = 60 * 1000; + const intervalId = setInterval(() => { + if (this.selectedServer !== selectedServer) { + // Server is no longer running, stop interval + clearInterval(intervalId); + return; + } + this.refreshTransferStats(selectedServer, serverView); + }, statsRefreshRateMs); + } + + private getS3InviteUrl(accessUrl: string, isAdmin = false) { + // TODO(alalama): display the invite in the user's preferred language. + const adminParam = isAdmin ? '?admin_embed' : ''; + return `https://s3.amazonaws.com/outline-vpn/invite.html${adminParam}#${encodeURIComponent( + accessUrl + )}`; + } + + // Converts the access key model to the format used by outline-server-view. + private convertToUiAccessKey(remoteAccessKey: server_model.AccessKey): DisplayAccessKey { + return { + id: remoteAccessKey.id, + placeholderName: this.appRoot.localize('key', 'keyId', remoteAccessKey.id), + name: remoteAccessKey.name, + accessUrl: remoteAccessKey.accessUrl, + transferredBytes: 0, + dataLimitBytes: remoteAccessKey.dataLimit?.bytes, + }; + } + + private async addAccessKey() { + const server = this.selectedServer; + try { + const serverAccessKey = await server.addAccessKey(); + const uiAccessKey = this.convertToUiAccessKey(serverAccessKey); + const serverView = await this.appRoot.getServerView(server.getId()); + serverView.addAccessKey(uiAccessKey); + this.appRoot.showNotification(this.appRoot.localize('notification-key-added')); + } catch (error) { + console.error(`Failed to add access key: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-key-add')); + } + } + + private renameAccessKey(accessKeyId: string, newName: string, entry: polymer.Base) { + this.selectedServer + .renameAccessKey(accessKeyId, newName) + .then(() => { + entry.commitName(); + }) + .catch((error) => { + console.error(`Failed to rename access key: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-key-rename')); + entry.revertName(); + }); + } + + private async setDefaultDataLimit(limit: server_model.DataLimit) { + if (!limit) { + return; + } + const previousLimit = this.selectedServer.getDefaultDataLimit(); + if (previousLimit && limit.bytes === previousLimit.bytes) { + return; + } + const serverView = await this.appRoot.getServerView(this.appRoot.selectedServerId); + try { + await this.selectedServer.setDefaultDataLimit(limit); + this.appRoot.showNotification(this.appRoot.localize('saved')); + serverView.defaultDataLimitBytes = limit?.bytes; + serverView.isDefaultDataLimitEnabled = true; + this.refreshTransferStats(this.selectedServer, serverView); + // Don't display the feature collection disclaimer anymore. + serverView.showFeatureMetricsDisclaimer = false; + window.localStorage.setItem('dataLimits-feature-collection-notification', 'true'); + } catch (error) { + console.error(`Failed to set server default data limit: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-set-data-limit')); + const defaultLimit = previousLimit || (await computeDefaultDataLimit(this.selectedServer)); + serverView.defaultDataLimitBytes = defaultLimit?.bytes; + serverView.isDefaultDataLimitEnabled = !!previousLimit; + } + } + + private async removeDefaultDataLimit() { + const serverView = await this.appRoot.getServerView(this.appRoot.selectedServerId); + const previousLimit = this.selectedServer.getDefaultDataLimit(); + try { + await this.selectedServer.removeDefaultDataLimit(); + serverView.isDefaultDataLimitEnabled = false; + this.appRoot.showNotification(this.appRoot.localize('saved')); + this.refreshTransferStats(this.selectedServer, serverView); + } catch (error) { + console.error(`Failed to remove server default data limit: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-remove-data-limit')); + serverView.isDefaultDataLimitEnabled = !!previousLimit; + } + } + + private openPerKeyDataLimitDialog( + event: CustomEvent<{ + keyId: string; + keyDataLimitBytes: number | undefined; + keyName: string; + serverId: string; + defaultDataLimitBytes: number | undefined; + }> + ) { + const detail = event.detail; + const onDataLimitSet = this.savePerKeyDataLimit.bind(this, detail.serverId, detail.keyId); + const onDataLimitRemoved = this.removePerKeyDataLimit.bind(this, detail.serverId, detail.keyId); + const activeDataLimitBytes = detail.keyDataLimitBytes ?? detail.defaultDataLimitBytes; + this.appRoot.openPerKeyDataLimitDialog( + detail.keyName, + activeDataLimitBytes, + onDataLimitSet, + onDataLimitRemoved + ); + } + + private async savePerKeyDataLimit( + serverId: string, + keyId: string, + dataLimitBytes: number + ): Promise { + this.appRoot.showNotification(this.appRoot.localize('saving')); + const server = this.idServerMap.get(serverId); + const serverView = await this.appRoot.getServerView(server.getId()); + try { + await server.setAccessKeyDataLimit(keyId, {bytes: dataLimitBytes}); + this.refreshTransferStats(server, serverView); + this.appRoot.showNotification(this.appRoot.localize('saved')); + return true; + } catch (error) { + console.error(`Failed to set data limit for access key ${keyId}: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-set-per-key-limit')); + return false; + } + } + + private async removePerKeyDataLimit(serverId: string, keyId: string): Promise { + this.appRoot.showNotification(this.appRoot.localize('saving')); + const server = this.idServerMap.get(serverId); + const serverView = await this.appRoot.getServerView(server.getId()); + try { + await server.removeAccessKeyDataLimit(keyId); + this.refreshTransferStats(server, serverView); + this.appRoot.showNotification(this.appRoot.localize('saved')); + return true; + } catch (error) { + console.error(`Failed to remove data limit from access key ${keyId}: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-remove-per-key-limit')); + return false; + } + } + + private async setHostnameForAccessKeys(hostname: string, serverSettings: polymer.Base) { + this.appRoot.showNotification(this.appRoot.localize('saving')); + try { + await this.selectedServer.setHostnameForAccessKeys(hostname); + this.appRoot.showNotification(this.appRoot.localize('saved')); + serverSettings.enterSavedState(); + } catch (error) { + this.appRoot.showError(this.appRoot.localize('error-not-saved')); + if (error.isNetworkError()) { + serverSettings.enterErrorState(this.appRoot.localize('error-network')); + return; + } + const message = error.response.status === 400 ? 'error-hostname-invalid' : 'error-unexpected'; + serverSettings.enterErrorState(this.appRoot.localize(message)); + } + } + + private async setPortForNewAccessKeys(port: number, serverSettings: polymer.Base) { + this.appRoot.showNotification(this.appRoot.localize('saving')); + try { + await this.selectedServer.setPortForNewAccessKeys(port); + this.appRoot.showNotification(this.appRoot.localize('saved')); + serverSettings.enterSavedState(); + } catch (error) { + this.appRoot.showError(this.appRoot.localize('error-not-saved')); + if (error.isNetworkError()) { + serverSettings.enterErrorState(this.appRoot.localize('error-network')); + return; + } + const code = error.response.status; + if (code === 409) { + serverSettings.enterErrorState(this.appRoot.localize('error-keys-port-in-use')); + return; + } + serverSettings.enterErrorState(this.appRoot.localize('error-unexpected')); + } + } + + // Returns promise which fulfills when the server is created successfully, + // or rejects with an error message that can be displayed to the user. + public async createManualServer(userInput: string): Promise { + let serverConfig: server_model.ManualServerConfig; + try { + serverConfig = parseManualServerConfig(userInput); + } catch (e) { + // This shouldn't happen because the UI validates the URL before enabling the DONE button. + const msg = `could not parse server config: ${e.message}`; + console.error(msg); + throw new Error(msg); + } + + // Don't let `ManualServerRepository.addServer` throw to avoid redundant error handling if we + // are adding an existing server. Query the repository instead to treat the UI accordingly. + const storedServer = this.manualServerRepository.findServer(serverConfig); + if (storedServer) { + this.appRoot.showNotification(this.appRoot.localize('notification-server-exists'), 5000); + this.showServer(storedServer); + return; + } + const manualServer = await this.manualServerRepository.addServer(serverConfig); + if (await manualServer.isHealthy()) { + this.addServer(null, manualServer); + this.showServer(manualServer); + } else { + // Remove inaccessible manual server from local storage if it was just created. + manualServer.forget(); + console.error('Manual server installed but unreachable.'); + throw new UnreachableServerError(); + } + } + + private async removeAccessKey(accessKeyId: string) { + const server = this.selectedServer; + try { + await server.removeAccessKey(accessKeyId); + (await this.appRoot.getServerView(server.getId())).removeAccessKey(accessKeyId); + this.appRoot.showNotification(this.appRoot.localize('notification-key-removed')); + } catch (error) { + console.error(`Failed to remove access key: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-key-remove')); + } + } + + private deleteServer(serverId: string) { + const serverToDelete = this.getServerById(serverId); + if (!isManagedServer(serverToDelete)) { + const msg = 'cannot delete non-ManagedServer'; + console.error(msg); + throw new Error(msg); + } + + const confirmationTitle = this.appRoot.localize('confirmation-server-destroy-title'); + const confirmationText = this.appRoot.localize('confirmation-server-destroy'); + const confirmationButton = this.appRoot.localize('destroy'); + this.appRoot.getConfirmation(confirmationTitle, confirmationText, confirmationButton, () => { + this.digitalOceanRetry(() => { + // TODO: Add an activity indicator in OutlineServerView during deletion. + return serverToDelete.getHost().delete(); + }).then( + () => { + this.removeServer(serverId); + this.showIntro(); + this.appRoot.showNotification(this.appRoot.localize('notification-server-destroyed')); + }, + (e) => { + // Don't show a toast on the login screen. + if (!(e instanceof digitalocean_api.XhrError)) { + console.error(`Failed destroy server: ${e}`); + this.appRoot.showError(this.appRoot.localize('error-server-destroy')); + } + } + ); + }); + } + + private forgetServer(serverId: string) { + const serverToForget = this.getServerById(serverId); + if (!isManualServer(serverToForget)) { + const msg = 'cannot forget non-ManualServer'; + console.error(msg); + throw new Error(msg); + } + const confirmationTitle = this.appRoot.localize('confirmation-server-remove-title'); + const confirmationText = this.appRoot.localize('confirmation-server-remove'); + const confirmationButton = this.appRoot.localize('remove'); + this.appRoot.getConfirmation(confirmationTitle, confirmationText, confirmationButton, () => { + serverToForget.forget(); + this.removeServer(serverId); + this.showIntro(); + this.appRoot.showNotification(this.appRoot.localize('notification-server-removed')); + }); + } + + private async setMetricsEnabled(metricsEnabled: boolean) { + const serverView = await this.appRoot.getServerView(this.appRoot.selectedServerId); + try { + await this.selectedServer.setMetricsEnabled(metricsEnabled); + this.appRoot.showNotification(this.appRoot.localize('saved')); + // Change metricsEnabled property on polymer element to update display. + serverView.metricsEnabled = metricsEnabled; + } catch (error) { + console.error(`Failed to set metrics enabled: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-metrics')); + serverView.metricsEnabled = !metricsEnabled; + } + } + + private async renameServer(newName: string) { + const serverToRename = this.selectedServer; + const serverId = this.appRoot.selectedServerId; + const view = await this.appRoot.getServerView(serverId); + try { + await serverToRename.setName(newName); + view.serverName = newName; + this.updateServerEntry(serverToRename); + } catch (error) { + console.error(`Failed to rename server: ${error}`); + this.appRoot.showError(this.appRoot.localize('error-server-rename')); + const oldName = this.selectedServer.getName(); + view.serverName = oldName; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (view.$.serverSettings as any).serverName = oldName; + } + } + + private cancelServerCreation(serverToCancel: server_model.Server): void { + if (!isManagedServer(serverToCancel)) { + const msg = 'cannot cancel non-ManagedServer'; + console.error(msg); + throw new Error(msg); + } + // TODO: Make the cancel button show an immediate state transition, + // indicate that deletion is in-progress, and allow the user to return + // to server creation in the meantime. + serverToCancel + .getHost() + .delete() + .then(() => { + this.removeServer(serverToCancel.getId()); + this.showIntro(); + }); + } + + private async setAppLanguage(languageCode: string, languageDir: 'rtl' | 'ltr') { + try { + await this.appRoot.setLanguage(languageCode, languageDir); + document.documentElement.setAttribute('dir', languageDir); + window.localStorage.setItem('overrideLanguage', languageCode); + } catch (error) { + this.appRoot.showError(this.appRoot.localize('error-unexpected')); + } + } +} diff --git a/src/server_manager/web_app/browser_main.ts b/src/server_manager/web_app/browser_main.ts new file mode 100644 index 000000000..2dd197327 --- /dev/null +++ b/src/server_manager/web_app/browser_main.ts @@ -0,0 +1,62 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(window as any).fetchWithPin = (_request: HttpRequest, _fingerprint: string) => { + return Promise.reject(new Error('Fingerprint pins are not supported')); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(window as any).openImage = (basename: string) => { + window.open(`./images/${basename})`); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(window as any).onUpdateDownloaded = (_callback: () => void) => { + console.info(`Requested registration of callbak for update download`); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(window as any).runDigitalOceanOauth = () => { + let isCancelled = false; + const rejectWrapper = {reject: (_error: Error) => {}}; + const result = new Promise((resolve, reject) => { + rejectWrapper.reject = reject; + window.open('https://cloud.digitalocean.com/account/api/tokens/new', 'noopener,noreferrer'); + const apiToken = window.prompt('Please enter your DigitalOcean API token'); + if (apiToken) { + resolve(apiToken); + } else { + reject(new Error('No api token entered')); + } + }); + return { + result, + isCancelled() { + return isCancelled; + }, + cancel() { + console.log('Session cancelled'); + isCancelled = true; + rejectWrapper.reject(new Error('Authentication cancelled')); + }, + }; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(window as any).bringToFront = () => { + console.info(`Requested bringToFront`); +}; + +import './main'; diff --git a/src/server_manager/web_app/build.action.sh b/src/server_manager/web_app/build.action.sh new file mode 100755 index 000000000..45f0fbf5e --- /dev/null +++ b/src/server_manager/web_app/build.action.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# +# Copyright 2018 The Outline Authors +# +# 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. + +set -eu + +readonly OUT_DIR="${BUILD_DIR}/server_manager/web_app" +rm -rf "${OUT_DIR}" + +run_action server_manager/web_app/build_install_script + +# Node.js on Cygwin doesn't like absolute Unix-style paths. +# So, we use a relative path as input to webpack. +pushd "${ROOT_DIR}" > /dev/null +# Notice that we forward the build environment if defined. +webpack --config=src/server_manager/electron_renderer.webpack.js ${BUILD_ENV:+--mode=${BUILD_ENV}} +popd > /dev/null diff --git a/src/server_manager/web_app/build_install_script.action.sh b/src/server_manager/web_app/build_install_script.action.sh new file mode 100755 index 000000000..ce5966c67 --- /dev/null +++ b/src/server_manager/web_app/build_install_script.action.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# +# Copyright 2020 The Outline Authors +# +# 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. + +set -eu + +readonly OUT_DIR="${BUILD_DIR}/server_manager/web_app/sh/" +rm -rf "${OUT_DIR}" + +# Create do_install_script.ts, which has a variable with the content of do_install_server.sh. +mkdir -p "${OUT_DIR}" + +pushd "${ROOT_DIR}/src/server_manager/install_scripts" > /dev/null +tar --create --gzip -f "${OUT_DIR}/do_scripts.tgz" ./install_server.sh ./do_install_server.sh +tar --create --gzip -f "${OUT_DIR}/gcp_scripts.tgz" ./install_server.sh ./gcp_install_server.sh + +# Node.js on Cygwin doesn't like absolute Unix-style paths. +# So, we use a relative path as input. +cd "${ROOT_DIR}" +node src/server_manager/install_scripts/build_do_install_script_ts.node.js \ + build/server_manager/web_app/sh/do_scripts.tgz > "${ROOT_DIR}/src/server_manager/install_scripts/do_install_script.ts" +node src/server_manager/install_scripts/build_gcp_install_script_ts.node.js \ + build/server_manager/web_app/sh/gcp_scripts.tgz > "${ROOT_DIR}/src/server_manager/install_scripts/gcp_install_script.ts" +popd > /dev/null diff --git a/src/server_manager/web_app/cloud_accounts.spec.ts b/src/server_manager/web_app/cloud_accounts.spec.ts new file mode 100644 index 000000000..09948ebd8 --- /dev/null +++ b/src/server_manager/web_app/cloud_accounts.spec.ts @@ -0,0 +1,113 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import {InMemoryStorage} from '../infrastructure/memory_storage'; + +import {CloudAccounts} from './cloud_accounts'; + +describe('CloudAccounts', () => { + it('get account methods return null when no cloud accounts are connected', () => { + const cloudAccounts = createCloudAccount(); + expect(cloudAccounts.getDigitalOceanAccount()).toBeNull(); + expect(cloudAccounts.getGcpAccount()).toBeNull(); + }); + + it('load connects account that exist in local storage', () => { + const storage = createInMemoryStorage('fake-access-token', 'fake-refresh-token'); + const cloudAccounts = createCloudAccount(storage); + expect(cloudAccounts.getDigitalOceanAccount()).not.toBeNull(); + expect(cloudAccounts.getGcpAccount()).not.toBeNull(); + }); + + it('connects accounts when connect methods are invoked', () => { + const cloudAccounts = createCloudAccount(); + + expect(cloudAccounts.getDigitalOceanAccount()).toBeNull(); + cloudAccounts.connectDigitalOceanAccount('fake-access-token'); + expect(cloudAccounts.getDigitalOceanAccount()).not.toBeNull(); + + expect(cloudAccounts.getGcpAccount()).toBeNull(); + cloudAccounts.connectGcpAccount('fake-access-token'); + expect(cloudAccounts.getGcpAccount()).not.toBeNull(); + }); + + it('removes account when disconnect is invoked', () => { + const storage = createInMemoryStorage('fake-access-token', 'fake-refresh-token'); + const cloudAccounts = createCloudAccount(storage); + + expect(cloudAccounts.getDigitalOceanAccount()).not.toBeNull(); + cloudAccounts.disconnectDigitalOceanAccount(); + expect(cloudAccounts.getDigitalOceanAccount()).toBeNull(); + + expect(cloudAccounts.getGcpAccount()).not.toBeNull(); + cloudAccounts.disconnectGcpAccount(); + expect(cloudAccounts.getGcpAccount()).toBeNull(); + }); + + it('functional noop on calling disconnect when accounts are not connected', () => { + const cloudAccounts = createCloudAccount(); + + expect(cloudAccounts.getDigitalOceanAccount()).toBeNull(); + cloudAccounts.disconnectDigitalOceanAccount(); + expect(cloudAccounts.getDigitalOceanAccount()).toBeNull(); + + expect(cloudAccounts.getGcpAccount()).toBeNull(); + cloudAccounts.disconnectGcpAccount(); + expect(cloudAccounts.getGcpAccount()).toBeNull(); + }); + + it('migrates existing legacy DigitalOcean access token on load', () => { + const storage = new InMemoryStorage(); + storage.setItem('LastDOToken', 'legacy-digitalocean-access-token'); + const cloudAccounts = createCloudAccount(storage); + + expect(cloudAccounts.getDigitalOceanAccount()).not.toBeNull(); + }); + + it('updates legacy DigitalOcean access token when account reconnected', () => { + const storage = new InMemoryStorage(); + storage.setItem('LastDOToken', 'legacy-digitalocean-access-token'); + const cloudAccounts = createCloudAccount(storage); + + expect(storage.getItem('LastDOToken')).toEqual('legacy-digitalocean-access-token'); + cloudAccounts.connectDigitalOceanAccount('new-digitalocean-access-token'); + expect(storage.getItem('LastDOToken')).toEqual('new-digitalocean-access-token'); + }); +}); + +function createInMemoryStorage( + digitalOceanAccessToken?: string, + gcpRefreshToken?: string +): Storage { + const storage = new InMemoryStorage(); + if (digitalOceanAccessToken) { + storage.setItem( + 'accounts.digitalocean', + JSON.stringify({accessToken: digitalOceanAccessToken}) + ); + } + if (gcpRefreshToken) { + storage.setItem('accounts.gcp', JSON.stringify({refreshToken: gcpRefreshToken})); + } + return storage; +} + +function createCloudAccount(storage = createInMemoryStorage()): CloudAccounts { + const shadowboxSettings = { + imageId: 'fake-image-id', + metricsUrl: 'fake-metrics-url', + sentryApiUrl: 'fake-sentry-api', + }; + return new CloudAccounts(shadowboxSettings, true, storage); +} diff --git a/src/server_manager/web_app/cloud_accounts.ts b/src/server_manager/web_app/cloud_accounts.ts new file mode 100644 index 000000000..31665da08 --- /dev/null +++ b/src/server_manager/web_app/cloud_accounts.ts @@ -0,0 +1,159 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import * as accounts from '../model/accounts'; +import * as digitalocean from '../model/digitalocean'; +import * as gcp from '../model/gcp'; +import {DigitalOceanAccount} from './digitalocean_account'; +import {GcpAccount} from './gcp_account'; +import {ShadowboxSettings} from './server_install'; + +type DigitalOceanAccountJson = { + accessToken: string; +}; + +type GcpAccountJson = { + refreshToken: string; +}; + +/** + * Manages connected cloud provider accounts. + */ +export class CloudAccounts implements accounts.CloudAccounts { + private readonly LEGACY_DIGITALOCEAN_STORAGE_KEY = 'LastDOToken'; + private readonly DIGITALOCEAN_ACCOUNT_STORAGE_KEY = 'accounts.digitalocean'; + private readonly GCP_ACCOUNT_STORAGE_KEY = 'accounts.gcp'; + + private digitalOceanAccount: DigitalOceanAccount = null; + private gcpAccount: GcpAccount = null; + + constructor( + private shadowboxSettings: ShadowboxSettings, + private isDebugMode: boolean, + private storage = localStorage + ) { + this.load(); + } + + /** See {@link CloudAccounts#connectDigitalOceanAccount} */ + connectDigitalOceanAccount(accessToken: string): digitalocean.Account { + this.digitalOceanAccount = this.createDigitalOceanAccount(accessToken); + this.save(); + return this.digitalOceanAccount; + } + + /** See {@link CloudAccounts#connectGcpAccount} */ + connectGcpAccount(refreshToken: string): gcp.Account { + this.gcpAccount = this.createGcpAccount(refreshToken); + this.save(); + return this.gcpAccount; + } + + /** See {@link CloudAccounts#disconnectDigitalOceanAccount} */ + disconnectDigitalOceanAccount(): void { + // TODO(fortuna): Revoke access token. + this.digitalOceanAccount = null; + this.save(); + } + + /** See {@link CloudAccounts#disconnectGcpAccount} */ + disconnectGcpAccount(): void { + // TODO(fortuna): Revoke access token. + this.gcpAccount = null; + this.save(); + } + + /** See {@link CloudAccounts#getDigitalOceanAccount} */ + getDigitalOceanAccount(): digitalocean.Account { + return this.digitalOceanAccount; + } + + /** See {@link CloudAccounts#getGcpAccount} */ + getGcpAccount(): gcp.Account { + return this.gcpAccount; + } + + /** Loads the saved cloud accounts from disk. */ + private load(): void { + const digitalOceanAccountJsonString = this.storage.getItem( + this.DIGITALOCEAN_ACCOUNT_STORAGE_KEY + ); + if (!digitalOceanAccountJsonString) { + const digitalOceanToken = this.loadLegacyDigitalOceanToken(); + if (digitalOceanToken) { + this.digitalOceanAccount = this.createDigitalOceanAccount(digitalOceanToken); + this.save(); + } + } else { + const digitalOceanAccountJson: DigitalOceanAccountJson = JSON.parse( + digitalOceanAccountJsonString + ); + this.digitalOceanAccount = this.createDigitalOceanAccount( + digitalOceanAccountJson.accessToken + ); + } + + const gcpAccountJsonString = this.storage.getItem(this.GCP_ACCOUNT_STORAGE_KEY); + if (gcpAccountJsonString) { + const gcpAccountJson: GcpAccountJson = JSON.parse( + this.storage.getItem(this.GCP_ACCOUNT_STORAGE_KEY) + ); + this.gcpAccount = this.createGcpAccount(gcpAccountJson.refreshToken); + } + } + + /** Loads legacy DigitalOcean access token. */ + private loadLegacyDigitalOceanToken(): string { + return this.storage.getItem(this.LEGACY_DIGITALOCEAN_STORAGE_KEY); + } + + /** Replace the legacy DigitalOcean access token. */ + private saveLegacyDigitalOceanToken(accessToken?: string): void { + if (accessToken) { + this.storage.setItem(this.LEGACY_DIGITALOCEAN_STORAGE_KEY, accessToken); + } else { + this.storage.removeItem(this.LEGACY_DIGITALOCEAN_STORAGE_KEY); + } + } + + private createDigitalOceanAccount(accessToken: string): DigitalOceanAccount { + return new DigitalOceanAccount('do', accessToken, this.shadowboxSettings, this.isDebugMode); + } + + private createGcpAccount(refreshToken: string): GcpAccount { + return new GcpAccount('gcp', refreshToken, this.shadowboxSettings); + } + + private save(): void { + if (this.digitalOceanAccount) { + const accessToken = this.digitalOceanAccount.getAccessToken(); + const digitalOceanAccountJson: DigitalOceanAccountJson = {accessToken}; + this.storage.setItem( + this.DIGITALOCEAN_ACCOUNT_STORAGE_KEY, + JSON.stringify(digitalOceanAccountJson) + ); + this.saveLegacyDigitalOceanToken(accessToken); + } else { + this.storage.removeItem(this.DIGITALOCEAN_ACCOUNT_STORAGE_KEY); + this.saveLegacyDigitalOceanToken(null); + } + if (this.gcpAccount) { + const refreshToken = this.gcpAccount.getRefreshToken(); + const gcpAccountJson: GcpAccountJson = {refreshToken}; + this.storage.setItem(this.GCP_ACCOUNT_STORAGE_KEY, JSON.stringify(gcpAccountJson)); + } else { + this.storage.removeItem(this.GCP_ACCOUNT_STORAGE_KEY); + } + } +} diff --git a/src/server_manager/web_app/data_formatting.spec.ts b/src/server_manager/web_app/data_formatting.spec.ts new file mode 100644 index 000000000..4834eb065 --- /dev/null +++ b/src/server_manager/web_app/data_formatting.spec.ts @@ -0,0 +1,100 @@ +/* + Copyright 2020 The Outline Authors + + 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. +*/ + +import * as formatting from './data_formatting'; + +describe('formatBytesParts', () => { + if (process?.versions?.node) { + it("doesn't run on Node", () => { + expect(() => formatting.formatBytesParts(0, 'en')).toThrow(); + }); + } else { + it('extracts the unit string and value separately', () => { + const english = formatting.formatBytesParts(0, 'en'); + expect(english.unit).toEqual('B'); + expect(english.value).toEqual('0'); + + const korean = formatting.formatBytesParts(2, 'kr'); + expect(korean.unit).toEqual('B'); + expect(korean.value).toEqual('2'); + + const russian = formatting.formatBytesParts(3000, 'ru'); + expect(russian.unit).toEqual('кБ'); + expect(russian.value).toEqual('3'); + + const simplifiedChinese = formatting.formatBytesParts(1.5 * 10 ** 9, 'zh-CN'); + expect(simplifiedChinese.unit).toEqual('GB'); + expect(simplifiedChinese.value).toEqual('1.5'); + + const farsi = formatting.formatBytesParts(133.5 * 10 ** 6, 'fa'); + expect(farsi.unit).toEqual('MB'); + expect(farsi.value).toEqual('۱۳۳٫۵'); + }); + } +}); + +describe('formatBytes', () => { + if (process?.versions?.node) { + it("doesn't run on Node", () => { + expect(() => formatting.formatBytes(0, 'en')).toThrow(); + }); + } else { + it('Formats data amounts', () => { + expect(formatting.formatBytes(2.1, 'zh-TW')).toEqual('2 byte'); + expect(formatting.formatBytes(7.8 * 10 ** 3, 'ar')).toEqual('8 كيلوبايت'); + expect(formatting.formatBytes(1.5 * 10 ** 6, 'tr')).toEqual('1,5 MB'); + expect(formatting.formatBytes(10 * 10 ** 9, 'jp')).toEqual('10 GB'); + expect(formatting.formatBytes(2.35 * 10 ** 12, 'pr')).toEqual('2.35 TB'); + }); + + it('Omits trailing zero decimal digits', () => { + expect(formatting.formatBytes(10 ** 12, 'en')).toEqual('1 TB'); + }); + } +}); + +function makeDisplayDataAmount(value: number, unit: 'MB' | 'GB') { + return {unit, value}; +} + +describe('displayDataAmountToBytes', () => { + it('correctly converts DisplayDataAmounts to byte values', () => { + expect(formatting.displayDataAmountToBytes(makeDisplayDataAmount(1, 'MB'))).toEqual(10 ** 6); + expect(formatting.displayDataAmountToBytes(makeDisplayDataAmount(20, 'GB'))).toEqual( + 2 * 10 ** 10 + ); + expect(formatting.displayDataAmountToBytes(makeDisplayDataAmount(0, 'MB'))).toEqual(0); + }); + it('handles null input', () => { + expect(formatting.displayDataAmountToBytes(null)).toBeNull(); + }); +}); + +describe('bytesToDisplayDataAmount', () => { + it('correctly converts byte values to DisplayDataAmounts', () => { + expect(formatting.bytesToDisplayDataAmount(10 ** 6)).toEqual(makeDisplayDataAmount(1, 'MB')); + expect(formatting.bytesToDisplayDataAmount(3 * 10 ** 9)).toEqual( + makeDisplayDataAmount(3, 'GB') + ); + expect(formatting.bytesToDisplayDataAmount(7 * 10 ** 5)).toEqual( + makeDisplayDataAmount(0, 'MB') + ); + }); + it('handles null and undefined input', () => { + expect(formatting.bytesToDisplayDataAmount(null)).toBeNull(); + expect(formatting.bytesToDisplayDataAmount(undefined)).toBeNull(); + }); +}); diff --git a/src/server_manager/web_app/data_formatting.ts b/src/server_manager/web_app/data_formatting.ts new file mode 100644 index 000000000..e8e49d595 --- /dev/null +++ b/src/server_manager/web_app/data_formatting.ts @@ -0,0 +1,139 @@ +/* + Copyright 2020 The Outline Authors + + 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. +*/ + +// Utility functions for internationalizing numbers and units + +const TERABYTE = 10 ** 12; +const GIGABYTE = 10 ** 9; +const MEGABYTE = 10 ** 6; +const KILOBYTE = 10 ** 3; + +const inWebApp = typeof window !== 'undefined' && typeof window.document !== 'undefined'; +interface FormatParams { + value: number; + unit: 'terabyte' | 'gigabyte' | 'megabyte' | 'kilobyte' | 'byte'; + decimalPlaces: number; +} + +function getDataFormattingParams(numBytes: number): FormatParams { + if (numBytes >= TERABYTE) { + return {value: numBytes / TERABYTE, unit: 'terabyte', decimalPlaces: 2}; + } else if (numBytes >= GIGABYTE) { + return {value: numBytes / GIGABYTE, unit: 'gigabyte', decimalPlaces: 2}; + } else if (numBytes >= MEGABYTE) { + return {value: numBytes / MEGABYTE, unit: 'megabyte', decimalPlaces: 1}; + } else if (numBytes >= KILOBYTE) { + return {value: numBytes / KILOBYTE, unit: 'kilobyte', decimalPlaces: 0}; + } + return {value: numBytes, unit: 'byte', decimalPlaces: 0}; +} + +function makeDataAmountFormatter(language: string, params: FormatParams) { + // We need to cast through `unknown` since `tsc` mistakenly omits the 'unit' field in + // `NumberFormatOptions`. + const options = { + style: 'unit', + unit: params.unit, + unitDisplay: 'short', + maximumFractionDigits: params.decimalPlaces, + } as unknown as Intl.NumberFormatOptions; + return new Intl.NumberFormat(language, options); +} + +interface DataAmountParts { + value: string; + unit: string; +} + +/** + * Returns a localized amount of bytes as a separate value and unit. This is useful for styling + * the unit and the value differently, or if you need them in separate nodes in the layout. + * + * @param {number} numBytes An amount of data to format. + * @param {string} language The ISO language code for the lanugage to translate to, eg 'en'. + */ +export function formatBytesParts(numBytes: number, language: string): DataAmountParts { + if (!inWebApp) { + throw new Error("formatBytesParts only works in web app code. Node usage isn't supported."); + } + const params = getDataFormattingParams(numBytes); + const parts = makeDataAmountFormatter(language, params).formatToParts(params.value); + // Cast away the type since `tsc` mistakenly omits the possibility for a 'unit' part + const isUnit = (part: Intl.NumberFormatPart) => (part as {type: string}).type === 'unit'; + const unitText = parts.find(isUnit).value; + return { + value: parts + .filter((part: Intl.NumberFormatPart) => !isUnit(part)) + .map((part: Intl.NumberFormatPart) => part.value) + .join('') + .trim(), + // Special case for "byte", since we'd rather be consistent with "KB", etc. "byte" is + // presumably used due to the example in the Unicode standard, + // http://unicode.org/reports/tr35/tr35-general.html#Example_Units + unit: unitText === 'byte' ? 'B' : unitText, + }; +} + +/** + * Returns a string representation of a number of bytes, translated into the given language + * + * @param {Number} numBytes An amount of data to format. + * @param {string} language The ISO language code for the lanugage to translate to, eg 'en'. + * @returns {string} The formatted data amount. + */ +export function formatBytes(numBytes: number, language: string): string { + if (!inWebApp) { + throw new Error("formatBytes only works in web app code. Node usage isn't supported."); + } + const params = getDataFormattingParams(numBytes); + return makeDataAmountFormatter(language, params).format(params.value); +} + +// TODO(JonathanDCohen222) Differentiate between this type, which is an input data limit, and +// a more general DisplayDataAmount with a string-typed unit and value which respects i18n. +export interface DisplayDataAmount { + unit: 'MB' | 'GB'; + value: number; +} + +/** + * @param dataAmount + * @returns The number of bytes represented by dataAmount + */ +export function displayDataAmountToBytes(dataAmount: DisplayDataAmount): number { + if (!dataAmount) { + return null; + } + if (dataAmount.unit === 'GB') { + return dataAmount.value * 10 ** 9; + } else if (dataAmount.unit === 'MB') { + return dataAmount.value * 10 ** 6; + } +} + +/** + * @param bytes + * @returns A DisplayDataAmount representing the number of bytes + */ +export function bytesToDisplayDataAmount(bytes: number): DisplayDataAmount { + if (bytes === null || bytes === undefined) { + return null; + } + if (bytes >= 10 ** 9) { + return {value: Math.floor(bytes / 10 ** 9), unit: 'GB'}; + } + return {value: Math.floor(bytes / 10 ** 6), unit: 'MB'}; +} diff --git a/src/server_manager/web_app/digitalocean_account.ts b/src/server_manager/web_app/digitalocean_account.ts new file mode 100644 index 000000000..95bc402d4 --- /dev/null +++ b/src/server_manager/web_app/digitalocean_account.ts @@ -0,0 +1,185 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import {DigitalOceanSession, DropletInfo, RestApiSession} from '../cloud/digitalocean_api'; +import * as crypto from '../infrastructure/crypto'; +import * as do_install_script from '../install_scripts/do_install_script'; +import * as digitalocean from '../model/digitalocean'; +import * as server from '../model/server'; + +import {DigitalOceanServer} from './digitalocean_server'; +import {getShellExportCommands, ShadowboxSettings} from './server_install'; + +// Tag used to mark Shadowbox Droplets. +const SHADOWBOX_TAG = 'shadowbox'; +const MACHINE_SIZE = 's-1vcpu-1gb'; + +export class DigitalOceanAccount implements digitalocean.Account { + private readonly digitalOcean: DigitalOceanSession; + private servers: DigitalOceanServer[] = []; + + constructor( + private id: string, + private accessToken: string, + private shadowboxSettings: ShadowboxSettings, + private debugMode: boolean + ) { + this.digitalOcean = new RestApiSession(accessToken); + } + + getId(): string { + return this.id; + } + + async getName(): Promise { + return (await this.digitalOcean.getAccount())?.email; + } + + async getStatus(): Promise { + const [account, droplets] = await Promise.all([ + this.digitalOcean.getAccount(), + this.digitalOcean.getDroplets(), + ]); + const needsEmailVerification = !account.email_verified; + // If the account is locked for no discernible reason, and there are no droplets, + // assume the billing info is missing. + const needsBillingInfo = + account.status === 'locked' && !needsEmailVerification && droplets.length == 0; + const hasReachedLimit = droplets.length >= account.droplet_limit; + let warning: string; + if (account.status !== 'active') { + warning = `${account.status_message} (status=${account.status})`; + } + return { + needsBillingInfo, + needsEmailVerification, + dropletLimit: account.droplet_limit, + hasReachedLimit, + warning, + }; + } + + // Return a list of regions indicating whether they are available and support + // our target machine size. + async listLocations(): Promise> { + const regions = await this.digitalOcean.getRegionInfo(); + return regions.map((info) => ({ + cloudLocation: new digitalocean.Region(info.slug), + available: info.available && info.sizes.indexOf(MACHINE_SIZE) !== -1, + })); + } + + // Returns true if there is no more room for additional Droplets. + async hasReachedLimit(): Promise { + const account = this.digitalOcean.getAccount(); + const droplets = await this.digitalOcean.getDroplets(); + return droplets.length >= (await account).droplet_limit; + } + + // Creates a server and returning it when it becomes active. + async createServer(region: digitalocean.Region, name: string): Promise { + console.time('activeServer'); + console.time('servingServer'); + const keyPair = await crypto.generateKeyPair(); + const installCommand = getInstallScript( + this.digitalOcean.accessToken, + name, + this.shadowboxSettings + ); + + // You can find the API slugs at https://slugs.do-api.dev/. + const dropletSpec = { + installCommand, + size: MACHINE_SIZE, + image: 'docker-20-04', + tags: [SHADOWBOX_TAG], + }; + if (this.debugMode) { + // Strip carriage returns, which produce weird blank lines when pasted into a terminal. + console.debug( + `private key for SSH access to new droplet:\n${keyPair.private.replace(/\r/g, '')}\n\n` + + 'Use "ssh -i keyfile root@[ip_address]" to connect to the machine' + ); + } + const response = await this.digitalOcean.createDroplet( + name, + region.id, + keyPair.public, + dropletSpec + ); + const server = this.createDigitalOceanServer(this.digitalOcean, response.droplet); + server.onceDropletActive + .then(async () => { + console.timeEnd('activeServer'); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _ of server.monitorInstallProgress()) { + /* do nothing */ + } + console.timeEnd('servingServer'); + }) + .catch((e) => console.log("Couldn't time installation", e)); + return server; + } + + listServers(fetchFromHost = true): Promise { + if (!fetchFromHost) { + return Promise.resolve(this.servers); // Return the in-memory servers. + } + return this.digitalOcean.getDropletsByTag(SHADOWBOX_TAG).then((droplets) => { + this.servers = []; + return droplets.map((droplet) => { + return this.createDigitalOceanServer(this.digitalOcean, droplet); + }); + }); + } + + getAccessToken(): string { + return this.accessToken; + } + + // Creates a DigitalOceanServer object and adds it to the in-memory server list. + private createDigitalOceanServer(digitalOcean: DigitalOceanSession, dropletInfo: DropletInfo) { + const server = new DigitalOceanServer( + `${this.id}:${dropletInfo.id}`, + digitalOcean, + dropletInfo + ); + this.servers.push(server); + return server; + } +} + +function sanitizeDigitalOceanToken(input: string): string { + const sanitizedInput = input.trim(); + const pattern = /^[A-Za-z0-9_/-]+$/; + if (!pattern.test(sanitizedInput)) { + throw new Error('Invalid DigitalOcean Token'); + } + return sanitizedInput; +} + +// cloudFunctions needs to define cloud::public_ip and cloud::add_tag. +function getInstallScript( + accessToken: string, + name: string, + shadowboxSettings: ShadowboxSettings +): string { + const sanitizedAccessToken = sanitizeDigitalOceanToken(accessToken); + return ( + '#!/bin/bash -eu\n' + + `export DO_ACCESS_TOKEN='${sanitizedAccessToken}'\n` + + getShellExportCommands(shadowboxSettings, name) + + do_install_script.SCRIPT + ); +} diff --git a/src/server_manager/web_app/digitalocean_server.ts b/src/server_manager/web_app/digitalocean_server.ts new file mode 100644 index 000000000..84606c31a --- /dev/null +++ b/src/server_manager/web_app/digitalocean_server.ts @@ -0,0 +1,317 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import {DigitalOceanSession, DropletInfo} from '../cloud/digitalocean_api'; +import {hexToString} from '../infrastructure/hex_encoding'; +import {sleep} from '../infrastructure/sleep'; +import {ValueStream} from '../infrastructure/value_stream'; +import {Region} from '../model/digitalocean'; +import * as server from '../model/server'; +import {makePathApiClient} from './fetcher'; + +import {ShadowboxServer} from './shadowbox_server'; + +// Prefix used in key-value tags. +const KEY_VALUE_TAG = 'kv'; +// The tag that appears at the beginning of installation. +const INSTALL_STARTED_TAG = 'install-started'; +// The tag key for the manager API certificate fingerprint. +const CERTIFICATE_FINGERPRINT_TAG = 'certsha256'; +// The tag key for the manager API URL. +const API_URL_TAG = 'apiurl'; +// The tag which appears if there is an error during installation. +const INSTALL_ERROR_TAG = 'install-error'; + +// These are superseded by the API_URL_TAG +// The tag key for the manager API port. +const DEPRECATED_API_PORT_TAG = 'apiport'; +// The tag key for the manager API url prefix. +const DEPRECATED_API_PREFIX_TAG = 'apiprefix'; + +// Possible install states for DigitaloceanServer. +enum InstallState { + // Unknown state - server may still be installing. + UNKNOWN = 0, + // Droplet status is "active" + DROPLET_CREATED, + // Userspace is running (detected by the presence of tags) + DROPLET_RUNNING, + // The server has generated its management service certificate. + CERTIFICATE_CREATED, + // Server is running and has the API URL and certificate fingerprint set. + COMPLETED, + // Server installation failed. + FAILED, + // Server installation was canceled by the user. + CANCELED, +} + +function getCompletionFraction(state: InstallState): number { + // Values are based on observed installation timing. + // Installation typically takes 90 seconds in total. + switch (state) { + case InstallState.UNKNOWN: + return 0.1; + case InstallState.DROPLET_CREATED: + return 0.5; + case InstallState.DROPLET_RUNNING: + return 0.55; + case InstallState.CERTIFICATE_CREATED: + return 0.6; + case InstallState.COMPLETED: + return 1.0; + default: + return 0; + } +} + +function isFinal(state: InstallState): boolean { + return ( + state === InstallState.COMPLETED || + state === InstallState.FAILED || + state === InstallState.CANCELED + ); +} + +export class DigitalOceanServer extends ShadowboxServer implements server.ManagedServer { + private onDropletActive: () => void; + readonly onceDropletActive = new Promise((fulfill) => { + this.onDropletActive = fulfill; + }); + private installState = new ValueStream(InstallState.UNKNOWN); + private readonly startTimestamp = Date.now(); + + constructor( + id: string, + private digitalOcean: DigitalOceanSession, + private dropletInfo: DropletInfo + ) { + // Consider passing a RestEndpoint object to the parent constructor, + // to better encapsulate the management api address logic. + super(id); + console.info('DigitalOceanServer created'); + // Go to the correct initial state based on the initial dropletInfo. + this.updateInstallState(); + // Start polling for state updates. + this.pollInstallState(); + } + + async *monitorInstallProgress(): AsyncGenerator { + for await (const state of this.installState.watch()) { + yield getCompletionFraction(state); + } + + if (this.installState.get() === InstallState.FAILED) { + throw new server.ServerInstallFailedError(); + } else if (this.installState.get() === InstallState.CANCELED) { + throw new server.ServerInstallCanceledError(); + } + } + + // Synchronous function for updating the installState based on the latest + // dropletInfo. + private updateInstallState(): void { + const TIMEOUT_MS = 5 * 60 * 1000; + + const tagMap = this.getTagMap(); + if (tagMap.get(INSTALL_ERROR_TAG)) { + console.error(`error tag: ${tagMap.get(INSTALL_ERROR_TAG)}`); + this.setInstallState(InstallState.FAILED); + } else if (Date.now() - this.startTimestamp >= TIMEOUT_MS) { + console.error('hit timeout while waiting for installation'); + this.setInstallState(InstallState.FAILED); + } else if (this.setApiUrlAndCertificate()) { + // API Url and Certificate have been set, so we have successfully + // installed the server and can now make API calls. + console.info('digitalocean_server: Successfully found API and cert tags'); + this.setInstallState(InstallState.COMPLETED); + } else if (tagMap.get(CERTIFICATE_FINGERPRINT_TAG)) { + this.setInstallState(InstallState.CERTIFICATE_CREATED); + } else if (tagMap.get(INSTALL_STARTED_TAG)) { + this.setInstallState(InstallState.DROPLET_RUNNING); + } else if (this.dropletInfo?.status === 'active') { + this.setInstallState(InstallState.DROPLET_CREATED); + } + } + + // Maintains this.installState. Will keep polling until installation has + // succeeded, failed, or been canceled. + private async pollInstallState(): Promise { + // Periodically refresh the droplet info then try to update the install + // state. If the final install state has been reached, don't make an + // unnecessary request to fetch droplet info. + while (!this.installState.isClosed()) { + try { + await this.refreshDropletInfo(); + } catch (error) { + console.log('Failed to get droplet info', error); + this.setInstallState(InstallState.FAILED); + return; + } + this.updateInstallState(); + // Return immediately if installation is terminated + // to prevent race conditions and avoid unnecessary delay. + if (this.installState.isClosed()) { + return; + } + // TODO: If there is an error refreshing the droplet, we should just + // try again, as there may be an intermittent network issue. + await sleep(3000); + } + } + + private setInstallState(installState: InstallState) { + this.installState.set(installState); + if (isFinal(installState)) { + this.installState.close(); + } + } + + // Returns true on success, else false. + private setApiUrlAndCertificate(): boolean { + try { + // Attempt to get certificate fingerprint and management api address, + // these methods throw exceptions if the fields are unavailable. + const certificateFingerprint = this.getCertificateFingerprint(); + const apiAddress = this.getManagementApiAddress(); + this.setManagementApi(makePathApiClient(apiAddress, certificateFingerprint)); + return true; + } catch (e) { + // Install state not yet ready. + return false; + } + } + + // Refreshes the state from DigitalOcean API. + private async refreshDropletInfo(): Promise { + const newDropletInfo = await this.digitalOcean.getDroplet(this.dropletInfo.id); + const oldDropletInfo = this.dropletInfo; + this.dropletInfo = newDropletInfo; + if (newDropletInfo.status !== oldDropletInfo.status) { + if (newDropletInfo.status === 'active') { + this.onDropletActive(); + } + } + } + + // Gets the key-value map stored in the DigitalOcean tags. + private getTagMap(): Map { + const ret = new Map(); + const tagPrefix = KEY_VALUE_TAG + ':'; + for (const tag of this.dropletInfo.tags) { + if (!startsWithCaseInsensitive(tag, tagPrefix)) { + continue; + } + const keyValuePair = tag.slice(tagPrefix.length); + const [key, hexValue] = keyValuePair.split(':', 2); + try { + ret.set(key.toLowerCase(), hexToString(hexValue)); + } catch (e) { + console.error('error decoding hex string'); + } + } + return ret; + } + + // Returns the public ipv4 address of this server. + private ipv4Address() { + for (const network of this.dropletInfo.networks.v4) { + if (network.type === 'public') { + return network.ip_address; + } + } + return undefined; + } + + // Gets the address for the user management api, throws an error if unavailable. + private getManagementApiAddress(): string { + const tagMap = this.getTagMap(); + let apiAddress = tagMap.get(API_URL_TAG); + // Check the old tags for backward-compatibility. + // TODO(fortuna): Delete this before we release v1.0 + if (!apiAddress) { + const portNumber = tagMap.get(DEPRECATED_API_PORT_TAG); + if (!portNumber) { + throw new Error('Could not get API port number'); + } + if (!this.ipv4Address()) { + throw new Error('API hostname not set'); + } + apiAddress = `https://${this.ipv4Address()}:${portNumber}/`; + const apiPrefix = tagMap.get(DEPRECATED_API_PREFIX_TAG); + if (apiPrefix) { + apiAddress += apiPrefix + '/'; + } + } + if (!apiAddress.endsWith('/')) { + apiAddress += '/'; + } + return apiAddress; + } + + // Gets the certificate fingerprint in binary, throws an error if + // unavailable. + private getCertificateFingerprint(): string { + const fingerprint = this.getTagMap().get(CERTIFICATE_FINGERPRINT_TAG); + if (fingerprint) { + return fingerprint; + } else { + throw new Error('certificate fingerprint unavailable'); + } + } + + getHost(): DigitalOceanHost { + // Construct a new DigitalOceanHost object, to be sure it has the latest + // session and droplet info. + return new DigitalOceanHost(this.digitalOcean, this.dropletInfo, this.onDelete.bind(this)); + } + + // Callback to be invoked once server is deleted. + private onDelete() { + if (!this.installState.isClosed()) { + this.setInstallState(InstallState.CANCELED); + } + } +} + +class DigitalOceanHost implements server.ManagedServerHost { + constructor( + private digitalOcean: DigitalOceanSession, + private dropletInfo: DropletInfo, + private deleteCallback: Function + ) {} + + getMonthlyOutboundTransferLimit(): server.DataAmount { + // Details on the bandwidth limits can be found at + // https://www.digitalocean.com/community/tutorials/digitalocean-bandwidth-billing-faq + return {terabytes: this.dropletInfo.size.transfer}; + } + + getMonthlyCost(): server.MonetaryCost { + return {usd: this.dropletInfo.size.price_monthly}; + } + + getCloudLocation(): Region { + return new Region(this.dropletInfo.region.slug); + } + + delete(): Promise { + this.deleteCallback(); + return this.digitalOcean.deleteDroplet(this.dropletInfo.id); + } +} + +function startsWithCaseInsensitive(text: string, prefix: string) { + return text.slice(0, prefix.length).toLowerCase() === prefix.toLowerCase(); +} diff --git a/src/server_manager/web_app/fetcher.spec.ts b/src/server_manager/web_app/fetcher.spec.ts new file mode 100644 index 000000000..48489718d --- /dev/null +++ b/src/server_manager/web_app/fetcher.spec.ts @@ -0,0 +1,28 @@ +// Copyright 2022 The Outline Authors +// +// 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. + +import {makePathApiClient} from './fetcher'; + +describe('makePathApiClient', () => { + const api = makePathApiClient('https://api.github.com/repos/Jigsaw-Code/'); + + if (process?.versions?.node) { + // This test relies on fetch(), which doesn't exist in Node (yet). + return; + } + it('GET', async () => { + const response = await api.request<{name: string}>('outline-server'); + expect(response.name).toEqual('outline-server'); + }); +}); diff --git a/src/server_manager/web_app/fetcher.ts b/src/server_manager/web_app/fetcher.ts new file mode 100644 index 000000000..0dfb0915c --- /dev/null +++ b/src/server_manager/web_app/fetcher.ts @@ -0,0 +1,43 @@ +// Copyright 2022 The Outline Authors +// +// 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. + +import {Fetcher, PathApiClient} from '../infrastructure/path_api'; + +async function fetchWrapper(request: HttpRequest): Promise { + const response = await fetch(request.url, request); + return { + status: response.status, + body: await response.text(), + }; +} + +/** + * @param fingerprint A SHA-256 hash of the expected leaf certificate, in binary encoding. + * @returns An HTTP client that enforces `fingerprint`, if set. + */ +function makeFetcher(fingerprint?: string): Fetcher { + if (fingerprint) { + return (request) => fetchWithPin(request, fingerprint); + } + return fetchWrapper; +} + +/** + * @param base A valid URL + * @param fingerprint A SHA-256 hash of the expected leaf certificate, in binary encoding. + * @returns A fully initialized API client. + */ +export function makePathApiClient(base: string, fingerprint?: string): PathApiClient { + return new PathApiClient(base, makeFetcher(fingerprint)); +} diff --git a/src/server_manager/web_app/gallery_app/index.html b/src/server_manager/web_app/gallery_app/index.html new file mode 100644 index 000000000..37eafb0d5 --- /dev/null +++ b/src/server_manager/web_app/gallery_app/index.html @@ -0,0 +1,24 @@ + + + + + Outline Manager Components Gallery + + + + + diff --git a/src/server_manager/web_app/gallery_app/main.ts b/src/server_manager/web_app/gallery_app/main.ts new file mode 100644 index 000000000..617251b56 --- /dev/null +++ b/src/server_manager/web_app/gallery_app/main.ts @@ -0,0 +1,333 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import '../ui_components/outline-about-dialog'; +import '../ui_components/outline-do-oauth-step'; +import '../ui_components/outline-gcp-oauth-step'; +import '../ui_components/outline-gcp-create-server-app'; +import '../ui_components/outline-server-view'; +import '../ui_components/outline-feedback-dialog'; +import '../ui_components/outline-share-dialog'; +import '../ui_components/outline-sort-span'; +import '../ui_components/outline-survey-dialog'; +import '../ui_components/outline-per-key-data-limit-dialog'; +import '@polymer/paper-checkbox/paper-checkbox'; + +import {PaperCheckboxElement} from '@polymer/paper-checkbox/paper-checkbox'; +import IntlMessageFormat from 'intl-messageformat'; +import {css, customElement, html, LitElement, property} from 'lit-element'; + +import * as gcp from '../../model/gcp'; +import {FakeManagedServer, FakeGcpAccount} from '../testing/models'; +import {OutlinePerKeyDataLimitDialog} from '../ui_components/outline-per-key-data-limit-dialog'; +import {COMMON_STYLES} from '../ui_components/cloud-install-styles'; +import {DisplayCloudId} from '../ui_components/cloud-assets'; + +const FAKE_SERVER = new FakeManagedServer('fake-id', true); + +async function makeLocalize(language: string) { + let messages: {[key: string]: string}; + try { + messages = await (await fetch(`./messages/${language}.json`)).json(); + } catch (e) { + window.alert(`Could not load messages for language "${language}"`); + } + return (msgId: string, ...args: string[]): string => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const params = {} as {[key: string]: any}; + for (let i = 0; i < args.length; i += 2) { + params[args[i]] = args[i + 1]; + } + if (!messages || !messages[msgId]) { + // Fallback that shows message id and params. + return `${msgId}(${JSON.stringify(params, null, ' ')})`; + } + // Ideally we would pre-parse and cache the IntlMessageFormat objects, + // but it's ok here because it's a test app. + const formatter = new IntlMessageFormat(messages[msgId], language); + return formatter.format(params) as string; + }; +} + +function fakeLocalize(id: string) { + return id; +} + +const GCP_LOCATIONS: gcp.ZoneOption[] = [ + { + cloudLocation: new gcp.Zone('us-central1-fake'), + available: true, + }, + { + cloudLocation: new gcp.Zone('europe-west3-fake'), + available: true, + }, + { + cloudLocation: new gcp.Zone('europe-west3-fake2'), + available: true, + }, + { + cloudLocation: new gcp.Zone('southamerica-east1-b'), + available: false, + }, + { + cloudLocation: new gcp.Zone('fake-location-z'), + available: true, + }, +]; + +const GCP_BILLING_ACCOUNTS: gcp.BillingAccount[] = [ + {id: '1234-123456', name: 'My Billing Account'}, +]; + +@customElement('outline-test-app') +export class TestApp extends LitElement { + @property({type: String}) dir = 'ltr'; + @property({type: Function}) localize: (...args: string[]) => string = fakeLocalize; + @property({type: String}) language = 'zz'; // Replaced asynchronously in the constructor. + @property({type: Boolean}) savePerKeyDataLimitSuccessful = true; + @property({type: Number}) keyDataLimit: number | undefined; + @property({type: String}) gcpRefreshToken = ''; + @property({type: Boolean}) gcpAccountHasBillingAccounts = false; + + static get styles() { + return [ + COMMON_STYLES, + css` + :host { + background: white; + display: block; + height: 100%; + overflow-y: auto; + padding: 10px; + width: 100%; + } + .widget { + display: block; + padding: 20px; + } + .backdrop { + background: var(--background-color); + } + `, + ]; + } + + constructor() { + super(); + console.log('Created'); + this.setLanguage('en'); + } + + async setLanguage(newLanguage: string) { + if (newLanguage === this.language) { + return; + } + this.localize = await makeLocalize(newLanguage); + this.language = newLanguage; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private select(querySelector: string): any { + return this.shadowRoot.querySelector(querySelector); + } + + private setKeyDataLimit(bytes: number) { + if ((this.select('#perKeyDataLimitSuccessCheckbox') as PaperCheckboxElement).checked) { + this.keyDataLimit = bytes; + console.log(`Per Key Data Limit set to ${bytes} bytes!`); + return true; + } + console.error('Per Key Data Limit failed to be set!'); + return false; + } + + private removeKeyDataLimit() { + if ((this.select('#perKeyDataLimitSuccessCheckbox') as PaperCheckboxElement).checked) { + this.keyDataLimit = undefined; + console.log('Per Key Data Limit Removed!'); + return true; + } + console.error('Per Key Data Limit failed to be removed!'); + return false; + } + + render() { + return html` +

Outline Manager Components Gallery

+ ${this.pageControls} + +
+

outline-gcp-create-server-app

+ + (this.gcpAccountHasBillingAccounts = !this.gcpAccountHasBillingAccounts)} + >Fake billing accounts + +
+ +
+

outline-server-view

+
+ +
+
+ +
+

outline-per-key-data-limit-dialog

+ + { + this.savePerKeyDataLimitSuccessful = !this.savePerKeyDataLimitSuccessful; + }} + id="perKeyDataLimitSuccessCheckbox" + >Save Successful + +
+ +
+

outline-about-dialog

+ + +
+ +
+

outline-do-oauth-step

+ +
+ +
+

outline-gcp-oauth-step

+ +
+ +
+

outline-feedback-dialog

+ + +
+ +
+

outline-share-dialog

+ + +
+ +
+

outline-sort-icon

+ { + const el = this.select('outline-sort-span'); + el.direction *= -1; + }} + >Column Header +
+ +
+

outline-survey-dialog

+ + +
+ `; + } + + get pageControls() { + return html`

+ + +

+

+

`; + } +} diff --git a/src/server_manager/web_app/gcp_account.ts b/src/server_manager/web_app/gcp_account.ts new file mode 100644 index 000000000..6cc9ecc10 --- /dev/null +++ b/src/server_manager/web_app/gcp_account.ts @@ -0,0 +1,330 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import * as gcp_api from '../cloud/gcp_api'; +import {sleep} from '../infrastructure/sleep'; +import {SCRIPT} from '../install_scripts/gcp_install_script'; +import * as gcp from '../model/gcp'; +import {BillingAccount, Project} from '../model/gcp'; +import * as server from '../model/server'; + +import {GcpServer} from './gcp_server'; +import * as server_install from './server_install'; + +/** Returns a unique, RFC1035-style name as required by GCE. */ +function makeGcpInstanceName(): string { + function pad2(val: number) { + return val.toString().padStart(2, '0'); + } + + const now = new Date(); + const year = now.getUTCFullYear().toString(); + const month = pad2(now.getUTCMonth() + 1); // January is month 0. + const day = pad2(now.getUTCDate()); + const hour = pad2(now.getUTCHours()); + const minute = pad2(now.getUTCMinutes()); + const second = pad2(now.getUTCSeconds()); + return `outline-${year}${month}${day}-${hour}${minute}${second}`; +} + +// Regions where the first f1-micro instance is free. +// See https://cloud.google.com/free/docs/gcp-free-tier/#compute +const FREE_TIER_REGIONS = new Set(['us-west1', 'us-central1', 'us-east1']); + +export function isInFreeTier(zone: gcp.Zone): boolean { + return FREE_TIER_REGIONS.has(zone.regionId); +} + +/** + * The Google Cloud Platform account model. + */ +export class GcpAccount implements gcp.Account { + private static readonly OUTLINE_PROJECT_NAME = 'Outline servers'; + private static readonly OUTLINE_FIREWALL_NAME = 'outline'; + private static readonly OUTLINE_FIREWALL_TAG = 'outline'; + private static readonly MACHINE_SIZE = 'e2-micro'; + private static readonly REQUIRED_GCP_SERVICES = ['compute.googleapis.com']; + + private readonly apiClient: gcp_api.RestApiClient; + + constructor( + private id: string, + private refreshToken: string, + private shadowboxSettings: server_install.ShadowboxSettings + ) { + this.apiClient = new gcp_api.RestApiClient(refreshToken); + } + + getId(): string { + return this.id; + } + + /** @see {@link Account#getName}. */ + async getName(): Promise { + const userInfo = await this.apiClient.getUserInfo(); + return userInfo?.email; + } + + /** Returns the refresh token. */ + getRefreshToken(): string { + return this.refreshToken; + } + + /** @see {@link Account#listServers}. */ + async listServers(projectId: string): Promise { + const result: GcpServer[] = []; + const filter = 'labels.outline=true'; + const listAllInstancesResponse = await this.apiClient.listAllInstances(projectId, filter); + const instanceMap = listAllInstancesResponse?.items ?? {}; + Object.values(instanceMap).forEach(({instances}) => { + instances?.forEach((instance) => { + const {zoneId} = gcp_api.parseZoneUrl(instance.zone); + const locator = {projectId, zoneId, instanceId: instance.id}; + const id = `${this.id}:${instance.id}`; + result.push(new GcpServer(id, locator, instance.name, Promise.resolve(), this.apiClient)); + }); + }); + return result; + } + + /** @see {@link Account#listLocations}. */ + async listLocations(projectId: string): Promise { + const listZonesResponse = await this.apiClient.listZones(projectId); + const zones = listZonesResponse.items ?? []; + return zones.map((zoneInfo) => ({ + cloudLocation: new gcp.Zone(zoneInfo.name), + available: zoneInfo.status === 'UP', + })); + } + + /** @see {@link Account#listProjects}. */ + async listProjects(): Promise { + const filter = 'labels.outline=true AND lifecycleState=ACTIVE'; + const response = await this.apiClient.listProjects(filter); + if (response.projects?.length > 0) { + return response.projects.map((project) => { + return { + id: project.projectId, + name: project.name, + }; + }); + } + return []; + } + + /** @see {@link Account#createProject}. */ + async createProject(projectId: string, billingAccountId: string): Promise { + // Create GCP project + const createProjectData = { + projectId, + name: GcpAccount.OUTLINE_PROJECT_NAME, + labels: { + outline: 'true', + }, + }; + const createProjectResponse = await this.apiClient.createProject(createProjectData); + let createProjectOperation = null; + while (!createProjectOperation?.done) { + await sleep(2 * 1000); + createProjectOperation = await this.apiClient.resourceManagerOperationGet( + createProjectResponse.name + ); + } + if (createProjectOperation.error) { + // TODO: Throw error. The project wasn't created so we should have nothing to delete. + } + + await this.configureProject(projectId, billingAccountId); + + return { + id: projectId, + name: GcpAccount.OUTLINE_PROJECT_NAME, + }; + } + + async isProjectHealthy(projectId: string): Promise { + const projectBillingInfo = await this.apiClient.getProjectBillingInfo(projectId); + if (!projectBillingInfo.billingEnabled) { + return false; + } + + const listEnabledServicesResponse = await this.apiClient.listEnabledServices(projectId); + for (const requiredService of GcpAccount.REQUIRED_GCP_SERVICES) { + const found = listEnabledServicesResponse.services.find( + (service) => service.config.name === requiredService + ); + if (!found) { + return false; + } + } + + return true; + } + + async repairProject(projectId: string, billingAccountId: string): Promise { + return await this.configureProject(projectId, billingAccountId); + } + + /** @see {@link Account#listBillingAccounts}. */ + async listOpenBillingAccounts(): Promise { + const response = await this.apiClient.listBillingAccounts(); + if (response.billingAccounts?.length > 0) { + return response.billingAccounts + .filter((billingAccount) => billingAccount.open) + .map((billingAccount) => ({ + id: billingAccount.name.substring(billingAccount.name.lastIndexOf('/') + 1), + name: billingAccount.displayName, + })); + } + return []; + } + + private async createFirewallIfNeeded(projectId: string): Promise { + // Configure Outline firewall + const getFirewallResponse = await this.apiClient.listFirewalls( + projectId, + GcpAccount.OUTLINE_FIREWALL_NAME + ); + if (!getFirewallResponse?.items || getFirewallResponse?.items?.length === 0) { + const createFirewallData = { + name: GcpAccount.OUTLINE_FIREWALL_NAME, + direction: 'INGRESS', + priority: 1000, + targetTags: [GcpAccount.OUTLINE_FIREWALL_TAG], + allowed: [ + { + IPProtocol: 'all', + }, + ], + sourceRanges: ['0.0.0.0/0'], + }; + const createFirewallOperation = await this.apiClient.createFirewall( + projectId, + createFirewallData + ); + const errors = createFirewallOperation.error?.errors; + if (errors) { + throw new server.ServerInstallFailedError(`Firewall creation failed: ${errors}`); + } + } + } + + /** @see {@link Account#createServer}. */ + async createServer( + projectId: string, + name: string, + zone: gcp.Zone + ): Promise { + // TODO: Move this to project setup. + await this.createFirewallIfNeeded(projectId); + + // Create VM instance + const gcpInstanceName = makeGcpInstanceName(); + const createInstanceData = { + name: gcpInstanceName, + description: name, // Show a human-readable name in the GCP console + machineType: `zones/${zone.id}/machineTypes/${GcpAccount.MACHINE_SIZE}`, + disks: [ + { + boot: true, + initializeParams: { + sourceImage: 'projects/ubuntu-os-cloud/global/images/family/ubuntu-2004-lts', + }, + }, + ], + networkInterfaces: [ + { + network: 'global/networks/default', + // Empty accessConfigs necessary to allocate ephemeral IP + accessConfigs: [{}], + }, + ], + labels: { + outline: 'true', + }, + tags: { + // This must match the firewall target tag. + items: [GcpAccount.OUTLINE_FIREWALL_TAG], + }, + metadata: { + items: [ + { + key: 'enable-guest-attributes', + value: 'TRUE', + }, + { + key: 'user-data', + value: this.getInstallScript(name), + }, + ], + }, + }; + const zoneLocator = {projectId, zoneId: zone.id}; + const createInstanceOperation = await this.apiClient.createInstance( + zoneLocator, + createInstanceData + ); + const errors = createInstanceOperation.error?.errors; + if (errors) { + throw new server.ServerInstallFailedError(`Instance creation failed: ${errors}`); + } + + const instanceId = createInstanceOperation.targetId; + const instanceLocator = {instanceId, ...zoneLocator}; + const instanceCreation = this.apiClient.computeEngineOperationZoneWait( + zoneLocator, + createInstanceOperation.name + ); + + const id = `${this.id}:${instanceId}`; + return new GcpServer(id, instanceLocator, gcpInstanceName, instanceCreation, this.apiClient); + } + + private async configureProject(projectId: string, billingAccountId: string): Promise { + // Link billing account + const updateProjectBillingInfoData = { + name: `projects/${projectId}/billingInfo`, + projectId, + billingAccountName: `billingAccounts/${billingAccountId}`, + }; + await this.apiClient.updateProjectBillingInfo(projectId, updateProjectBillingInfoData); + + // Enable APIs + const enableServicesData = { + serviceIds: GcpAccount.REQUIRED_GCP_SERVICES, + }; + const enableServicesResponse = await this.apiClient.enableServices( + projectId, + enableServicesData + ); + let enableServicesOperation = null; + while (!enableServicesOperation?.done) { + await sleep(2 * 1000); + enableServicesOperation = await this.apiClient.serviceUsageOperationGet( + enableServicesResponse.name + ); + } + if (enableServicesResponse.error) { + // TODO: Throw error. + } + } + + private getInstallScript(serverName: string): string { + return ( + '#!/bin/bash -eu\n' + + server_install.getShellExportCommands(this.shadowboxSettings, serverName) + + SCRIPT + ); + } +} diff --git a/src/server_manager/web_app/gcp_server.ts b/src/server_manager/web_app/gcp_server.ts new file mode 100644 index 000000000..9a565e38b --- /dev/null +++ b/src/server_manager/web_app/gcp_server.ts @@ -0,0 +1,293 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import * as gcp_api from '../cloud/gcp_api'; +import {sleep} from '../infrastructure/sleep'; +import {ValueStream} from '../infrastructure/value_stream'; +import {Zone} from '../model/gcp'; +import * as server from '../model/server'; +import {DataAmount, ManagedServerHost, MonetaryCost} from '../model/server'; +import {makePathApiClient} from './fetcher'; + +import {ShadowboxServer} from './shadowbox_server'; + +enum InstallState { + // Unknown state - server request may still be pending. + UNKNOWN = 0, + // The instance has been created. + INSTANCE_CREATED, + // The static IP has been allocated. + IP_ALLOCATED, + // The system has booted (detected by the creation of guest tags) + INSTANCE_RUNNING, + // The server has generated its management service certificate. + CERTIFICATE_CREATED, + // Server is running and has the API URL and certificate fingerprint set. + COMPLETED, + // Server installation failed. + FAILED, + // Server installation was canceled by the user. + CANCELED, +} + +function getCompletionFraction(state: InstallState): number { + // Values are based on observed installation timing. + // Installation typically takes ~2.6 minutes in total. + switch (state) { + case InstallState.UNKNOWN: + return 0.01; + case InstallState.INSTANCE_CREATED: + return 0.12; + case InstallState.IP_ALLOCATED: + return 0.14; + case InstallState.INSTANCE_RUNNING: + return 0.4; + case InstallState.CERTIFICATE_CREATED: + return 0.7; + case InstallState.COMPLETED: + return 1.0; + default: + return 0; + } +} + +function isFinal(state: InstallState): boolean { + return ( + state === InstallState.COMPLETED || + state === InstallState.FAILED || + state === InstallState.CANCELED + ); +} + +export class GcpServer extends ShadowboxServer implements server.ManagedServer { + private static readonly GUEST_ATTRIBUTES_POLLING_INTERVAL_MS = 5 * 1000; + + private readonly instanceReadiness: Promise; + private readonly gcpHost: GcpHost; + private readonly installState = new ValueStream(InstallState.UNKNOWN); + + constructor( + id: string, + private locator: gcp_api.InstanceLocator, + private gcpInstanceName: string, // See makeGcpInstanceName() in gcp_account.ts. + instanceCreation: Promise, + private apiClient: gcp_api.RestApiClient + ) { + super(id); + // Optimization: start the check for a static IP immediately. + const hasStaticIp: Promise = this.hasStaticIp(); + this.instanceReadiness = instanceCreation + .then(async () => { + if (this.installState.isClosed()) { + return; + } + this.setInstallState(InstallState.INSTANCE_CREATED); + if (!(await hasStaticIp)) { + await this.promoteEphemeralIp(); + } + if (this.installState.isClosed()) { + return; + } + this.setInstallState(InstallState.IP_ALLOCATED); + this.pollInstallState(); // Start asynchronous polling. + }) + .catch((e) => { + this.setInstallState(InstallState.FAILED); + throw e; + }); + this.gcpHost = new GcpHost( + locator, + gcpInstanceName, + this.instanceReadiness, + apiClient, + this.onDelete.bind(this) + ); + } + + private getRegionLocator(): gcp_api.RegionLocator { + return { + regionId: new Zone(this.locator.zoneId).regionId, + projectId: this.locator.projectId, + }; + } + + private async hasStaticIp(): Promise { + try { + // By convention, the static IP for an Outline instance uses the instance's name. + await this.apiClient.getStaticIp(this.getRegionLocator(), this.gcpInstanceName); + return true; + } catch (e) { + if (is404(e)) { + // The IP address has not yet been reserved. + return false; + } + throw new server.ServerInstallFailedError(`Static IP check failed: ${e}`); + } + } + + private async promoteEphemeralIp(): Promise { + const instance = await this.apiClient.getInstance(this.locator); + // Promote ephemeral IP to static IP + const ipAddress = instance.networkInterfaces[0].accessConfigs[0].natIP; + const createStaticIpData = { + name: instance.name, + description: instance.description, + address: ipAddress, + }; + const createStaticIpOperation = await this.apiClient.createStaticIp( + this.getRegionLocator(), + createStaticIpData + ); + const operationErrors = createStaticIpOperation.error?.errors; + if (operationErrors) { + throw new server.ServerInstallFailedError(`Firewall creation failed: ${operationErrors}`); + } + } + + getHost(): ManagedServerHost { + return this.gcpHost; + } + + async *monitorInstallProgress(): AsyncGenerator { + for await (const state of this.installState.watch()) { + yield getCompletionFraction(state); + } + + if (this.installState.get() === InstallState.FAILED) { + throw new server.ServerInstallFailedError(); + } else if (this.installState.get() === InstallState.CANCELED) { + throw new server.ServerInstallCanceledError(); + } + yield getCompletionFraction(this.installState.get()); + } + + private async pollInstallState(): Promise { + while (!this.installState.isClosed()) { + const outlineGuestAttributes = await this.getOutlineGuestAttributes(); + if (outlineGuestAttributes.has('apiUrl') && outlineGuestAttributes.has('certSha256')) { + const certSha256 = outlineGuestAttributes.get('certSha256'); + const apiUrl = outlineGuestAttributes.get('apiUrl'); + this.setManagementApi(makePathApiClient(apiUrl, atob(certSha256))); + this.setInstallState(InstallState.COMPLETED); + break; + } else if (outlineGuestAttributes.has('install-error')) { + this.setInstallState(InstallState.FAILED); + break; + } else if (outlineGuestAttributes.has('certSha256')) { + this.setInstallState(InstallState.CERTIFICATE_CREATED); + } else if (outlineGuestAttributes.has('install-started')) { + this.setInstallState(InstallState.INSTANCE_RUNNING); + } + + await sleep(GcpServer.GUEST_ATTRIBUTES_POLLING_INTERVAL_MS); + } + } + + private async getOutlineGuestAttributes(): Promise> { + const result = new Map(); + const guestAttributes = await this.apiClient.getGuestAttributes(this.locator, 'outline/'); + const attributes = guestAttributes?.queryValue?.items ?? []; + attributes.forEach((entry) => { + result.set(entry.key, entry.value); + }); + return result; + } + + private setInstallState(newState: InstallState): void { + console.debug(InstallState[newState]); + this.installState.set(newState); + if (isFinal(newState)) { + this.installState.close(); + } + } + + private onDelete(): void { + if (!this.installState.isClosed()) { + this.setInstallState(InstallState.CANCELED); + } + } +} + +class GcpHost implements server.ManagedServerHost { + constructor( + private readonly locator: gcp_api.InstanceLocator, + private readonly gcpInstanceName: string, + private readonly instanceReadiness: Promise, + private readonly apiClient: gcp_api.RestApiClient, + private readonly deleteCallback: () => void + ) {} + + async delete(): Promise { + this.deleteCallback(); + // The GCP API documentation doesn't specify whether instances can be deleted + // before creation has finished, and the static IP allocation is entirely + // asynchronous, so we must wait for instance setup to complete before starting + // deletion. Also, if creation failed, then deletion is trivially successful. + try { + await this.instanceReadiness; + } catch (e) { + console.warn(`Attempting deletion of server that failed setup: ${e}`); + } + const regionLocator = { + regionId: this.getCloudLocation().regionId, + projectId: this.locator.projectId, + }; + // By convention, the static IP for an Outline instance uses the instance's name. + await this.waitForDelete( + this.apiClient.deleteStaticIp(regionLocator, this.gcpInstanceName), + 'Deleted server did not have a static IP' + ); + await this.waitForDelete( + this.apiClient.deleteInstance(this.locator), + 'No instance for deleted server' + ); + } + + private async waitForDelete( + deletion: Promise, + msg404: string + ): Promise { + try { + await deletion; + // We assume that deletion will eventually succeed once the operation has + // been queued successfully, so there's no need to wait for it. + } catch (e) { + if (is404(e)) { + console.warn(msg404); + return; + } + throw e; + } + } + + getHostId(): string { + return this.locator.instanceId; + } + + getMonthlyCost(): MonetaryCost { + return undefined; + } + + getMonthlyOutboundTransferLimit(): DataAmount { + return undefined; + } + + getCloudLocation(): Zone { + return new Zone(this.locator.zoneId); + } +} + +function is404(error: Error): boolean { + return error instanceof gcp_api.HttpError && error.getStatusCode() === 404; +} diff --git a/src/server_manager/web_app/karma.conf.js b/src/server_manager/web_app/karma.conf.js new file mode 100644 index 000000000..98160ee95 --- /dev/null +++ b/src/server_manager/web_app/karma.conf.js @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +// Copyright 2020 The Outline Authors +// +// 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. + +const webpack = require('webpack'); +const {makeConfig} = require('../base.webpack.js'); +process.env.CHROMIUM_BIN = require('puppeteer').executablePath(); + +const baseConfig = makeConfig({ + defaultMode: 'development', +}); + +const test_patterns = [ + '**/*.spec.ts', + // We need to test data_formatting in a browser context + './data_formatting.spec.ts', +]; + +let preprocessors = {}; +for (const pattern of test_patterns) { + preprocessors[pattern] = ['webpack']; +} + +module.exports = function (config) { + config.set({ + frameworks: ['jasmine'], + files: test_patterns, + preprocessors, + reporters: ['progress'], + colors: true, + logLevel: config.LOG_INFO, + browsers: ['ChromiumHeadless'], + singleRun: true, + concurrency: Infinity, + webpack: { + module: baseConfig.module, + resolve: baseConfig.resolve, + plugins: [ + ...baseConfig.plugins, + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + mode: baseConfig.mode, + }, + }); +}; diff --git a/src/server_manager/web_app/location_formatting.spec.ts b/src/server_manager/web_app/location_formatting.spec.ts new file mode 100644 index 000000000..70e72d194 --- /dev/null +++ b/src/server_manager/web_app/location_formatting.spec.ts @@ -0,0 +1,156 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import * as location from '../model/location'; +import {filterOptions, getShortName, localizeCountry} from './location_formatting'; + +describe('getShortName', () => { + it('basic case', () => { + expect( + getShortName({id: 'fake-id', location: location.SYDNEY}, (msgId) => { + expect(msgId).toEqual('geo-sydney'); + return 'foo'; + }) + ).toEqual('foo'); + }); + + it('city-state is converted to lowercase', () => { + expect( + getShortName({id: 'fake-id', location: location.SINGAPORE}, (msgId) => { + expect(msgId).toEqual('geo-sg'); + return 'foo'; + }) + ).toEqual('foo'); + }); + + it('returns the ID when geoId is null', () => { + expect( + getShortName({id: 'fake-id', location: null}, (_msgId) => { + fail(); + return null; + }) + ).toEqual('fake-id'); + }); + + it('returns empty string when the location is null', () => { + expect( + getShortName(null, (_msgId) => { + fail(); + return null; + }) + ).toEqual(''); + }); +}); + +describe('localizeCountry', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!(Intl as any).DisplayNames) { + console.log('country localization requires modern Intl features'); + return; + } + + it('basic case', () => { + expect(localizeCountry(location.NEW_YORK_CITY, 'en')).toEqual('United States'); + }); + + it('other language', () => { + expect(localizeCountry(location.NEW_YORK_CITY, 'es')).toEqual('Estados Unidos'); + }); + + it('city-state is localized', () => { + expect(localizeCountry(location.SINGAPORE, 'en')).toEqual('Singapore'); + }); + + it('null is empty', () => { + expect(localizeCountry(null, 'en')).toEqual(''); + }); +}); + +describe('filterOptions', () => { + it('empty', () => { + expect(filterOptions([])).toEqual([]); + }); + + it('one available', () => { + const option = { + cloudLocation: {id: 'zone-id', location: location.SAO_PAULO}, + available: true, + }; + expect(filterOptions([option])).toEqual([option]); + }); + + it('one not available', () => { + const option = { + cloudLocation: {id: 'zone-id', location: location.SALT_LAKE_CITY}, + available: false, + }; + expect(filterOptions([option])).toEqual([option]); + }); + + it('one unrecognized', () => { + const option: location.CloudLocationOption = { + cloudLocation: {id: 'zone-id', location: null}, + available: true, + }; + expect(filterOptions([option])).toEqual([option]); + }); + + it('one unrecognized and unavailable', () => { + const option: location.CloudLocationOption = { + cloudLocation: {id: 'zone-id', location: null}, + available: false, + }; + expect(filterOptions([option])).toEqual([]); + }); + + it('one of each', () => { + const available = { + cloudLocation: {id: 'available', location: location.SAN_FRANCISCO}, + available: true, + }; + const unavailable = { + cloudLocation: {id: 'unavailable', location: location.SEOUL}, + available: false, + }; + const unrecognized: location.CloudLocationOption = { + cloudLocation: {id: 'unrecognized', location: null}, + available: true, + }; + const unrecognizedAndUnavailable: location.CloudLocationOption = { + cloudLocation: {id: 'unrecognized-and-unavailable', location: null}, + available: false, + }; + + const filtered = filterOptions([ + unrecognized, + unavailable, + unrecognizedAndUnavailable, + available, + ]); + expect(filtered).toEqual([unavailable, available, unrecognized]); + }); + + it('available preferred', () => { + const available = { + cloudLocation: {id: 'available', location: location.TOKYO}, + available: true, + }; + const unavailable = { + cloudLocation: {id: 'unavailable', location: location.TOKYO}, + available: false, + }; + const filtered = filterOptions([unavailable, available]); + expect(filtered).toEqual([available]); + }); +}); diff --git a/src/server_manager/web_app/location_formatting.ts b/src/server_manager/web_app/location_formatting.ts new file mode 100644 index 000000000..2249cf4e6 --- /dev/null +++ b/src/server_manager/web_app/location_formatting.ts @@ -0,0 +1,72 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import {CloudLocation, CloudLocationOption, GeoLocation} from '../model/location'; + +/** + * Returns the localized place name, or the data center ID if the location is + * unknown. + */ +export function getShortName( + cloudLocation: CloudLocation, + localize: (id: string) => string +): string { + if (!cloudLocation) { + return ''; + } + if (!cloudLocation.location) { + return cloudLocation.id; + } + return localize(`geo-${cloudLocation.location.id.toLowerCase()}`); +} + +/** + * Returns the localized country name, or "" if the country is unknown or + * unnecessary. + */ +export function localizeCountry(geoLocation: GeoLocation, language: string): string { + if (!geoLocation) { + return ''; + } + // TODO: Remove typecast after https://github.com/microsoft/TypeScript/pull/44022 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const displayName = new (Intl as any).DisplayNames([language], {type: 'region'}); + return displayName.of(geoLocation.countryCode); +} + +/** + * Given an array of cloud location options, this function returns an array + * containing one representative option for each GeoLocation. Available + * options are preferred within each location. Available options with unknown + * GeoLocation (e.g. newly added zones) are placed at the end of the array. + */ +export function filterOptions(options: readonly T[]): T[] { + // Contains one available datacenter ID for each GeoLocation, or null if + // there are datacenters for that GeoLocation but none are available. + const map = new Map(); + const unmappedOptions: T[] = []; + + options.forEach((option) => { + const geoLocation = option.cloudLocation.location; + if (geoLocation) { + if (option.available || !map.has(geoLocation.id)) { + map.set(geoLocation.id, option); + } + } else if (option.available) { + unmappedOptions.push(option); + } + }); + + return [...map.values(), ...unmappedOptions]; +} diff --git a/src/server_manager/web_app/main.ts b/src/server_manager/web_app/main.ts new file mode 100644 index 000000000..998647cfe --- /dev/null +++ b/src/server_manager/web_app/main.ts @@ -0,0 +1,140 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import './ui_components/app-root'; + +import * as i18n from '../infrastructure/i18n'; + +import {App} from './app'; +import {CloudAccounts} from './cloud_accounts'; +import {ManualServerRepository} from './manual_server'; +import {AppRoot} from './ui_components/app-root'; +import {LanguageDef} from './ui_components/outline-language-picker'; + +const SUPPORTED_LANGUAGES: {[key: string]: LanguageDef} = { + af: {id: 'af', name: 'Afrikaans', dir: 'ltr'}, + am: {id: 'am', name: 'አማርኛ', dir: 'ltr'}, + ar: {id: 'ar', name: 'العربية', dir: 'rtl'}, + az: {id: 'az', name: 'azərbaycan', dir: 'ltr'}, + bg: {id: 'bg', name: 'български', dir: 'ltr'}, + bn: {id: 'bn', name: 'বাংলা', dir: 'ltr'}, + bs: {id: 'bs', name: 'bosanski', dir: 'ltr'}, + ca: {id: 'ca', name: 'català', dir: 'ltr'}, + cs: {id: 'cs', name: 'Čeština', dir: 'ltr'}, + da: {id: 'da', name: 'Dansk', dir: 'ltr'}, + de: {id: 'de', name: 'Deutsch', dir: 'ltr'}, + el: {id: 'el', name: 'Ελληνικά', dir: 'ltr'}, + en: {id: 'en', name: 'English', dir: 'ltr'}, + 'en-GB': {id: 'en-GB', name: 'English (United Kingdom)', dir: 'ltr'}, + es: {id: 'es', name: 'Español', dir: 'ltr'}, + 'es-419': {id: 'es-419', name: 'Español (Latinoamérica)', dir: 'ltr'}, + et: {id: 'et', name: 'eesti', dir: 'ltr'}, + fa: {id: 'fa', name: 'فارسی', dir: 'rtl'}, + fi: {id: 'fi', name: 'Suomi', dir: 'ltr'}, + fil: {id: 'fil', name: 'Filipino', dir: 'ltr'}, + fr: {id: 'fr', name: 'Français', dir: 'ltr'}, + he: {id: 'he', name: 'עברית', dir: 'rtl'}, + hi: {id: 'hi', name: 'हिन्दी', dir: 'ltr'}, + hr: {id: 'hr', name: 'Hrvatski', dir: 'ltr'}, + hu: {id: 'hu', name: 'magyar', dir: 'ltr'}, + hy: {id: 'hy', name: 'հայերեն', dir: 'ltr'}, + id: {id: 'id', name: 'Indonesia', dir: 'ltr'}, + is: {id: 'is', name: 'íslenska', dir: 'ltr'}, + it: {id: 'it', name: 'Italiano', dir: 'ltr'}, + ja: {id: 'ja', name: '日本語', dir: 'ltr'}, + ka: {id: 'ka', name: 'ქართული', dir: 'ltr'}, + kk: {id: 'kk', name: 'қазақ тілі', dir: 'ltr'}, + km: {id: 'km', name: 'ខ្មែរ', dir: 'ltr'}, + ko: {id: 'ko', name: '한국어', dir: 'ltr'}, + lo: {id: 'lo', name: 'ລາວ', dir: 'ltr'}, + lt: {id: 'lt', name: 'lietuvių', dir: 'ltr'}, + lv: {id: 'lv', name: 'latviešu', dir: 'ltr'}, + mk: {id: 'mk', name: 'македонски', dir: 'ltr'}, + mn: {id: 'mn', name: 'монгол', dir: 'ltr'}, + mr: {id: 'mr', name: 'मराठी', dir: 'ltr'}, + ms: {id: 'ms', name: 'Melayu', dir: 'ltr'}, + my: {id: 'my', name: 'မြန်မာ', dir: 'ltr'}, + ne: {id: 'ne', name: 'नेपाली', dir: 'ltr'}, + nl: {id: 'nl', name: 'Nederlands', dir: 'ltr'}, + no: {id: 'no', name: 'norsk', dir: 'ltr'}, + pl: {id: 'pl', name: 'polski', dir: 'ltr'}, + 'pt-BR': {id: 'pt-BR', name: 'Português (Brasil)', dir: 'ltr'}, + 'pt-PT': {id: 'pt-PT', name: 'Português (Portugal)', dir: 'ltr'}, + ro: {id: 'ro', name: 'română', dir: 'ltr'}, + ru: {id: 'ru', name: 'Русский', dir: 'ltr'}, + si: {id: 'si', name: 'සිංහල', dir: 'ltr'}, + sk: {id: 'sk', name: 'Slovenčina', dir: 'ltr'}, + sl: {id: 'sl', name: 'slovenščina', dir: 'ltr'}, + sq: {id: 'sq', name: 'shqip', dir: 'ltr'}, + sr: {id: 'sr', name: 'српски', dir: 'ltr'}, + 'sr-Latn': {id: 'sr-Latn', name: 'srpski (latinica)', dir: 'ltr'}, + sv: {id: 'sv', name: 'Svenska', dir: 'ltr'}, + sw: {id: 'sw', name: 'Kiswahili', dir: 'ltr'}, + ta: {id: 'ta', name: 'தமிழ்', dir: 'ltr'}, + th: {id: 'th', name: 'ไทย', dir: 'ltr'}, + tr: {id: 'tr', name: 'Türkçe', dir: 'ltr'}, + uk: {id: 'uk', name: 'Українська', dir: 'ltr'}, + ur: {id: 'ur', name: 'اردو', dir: 'rtl'}, + vi: {id: 'vi', name: 'Tiếng Việt', dir: 'ltr'}, + 'zh-CN': {id: 'zh-CN', name: '简体中文', dir: 'ltr'}, + 'zh-TW': {id: 'zh-TW', name: '繁體中文', dir: 'ltr'}, +}; + +function getLanguageToUse(): i18n.LanguageCode { + const supportedLanguages = i18n.languageList(Object.keys(SUPPORTED_LANGUAGES)); + const preferredLanguages = i18n.getBrowserLanguages(); + const overrideLanguage = window.localStorage.getItem('overrideLanguage'); + if (overrideLanguage) { + preferredLanguages.unshift(new i18n.LanguageCode(overrideLanguage)); + } + const defaultLanguage = new i18n.LanguageCode('en'); + return new i18n.LanguageMatcher(supportedLanguages, defaultLanguage).getBestSupportedLanguage( + preferredLanguages + ); +} + +function sortLanguageDefsByName(languageDefs: LanguageDef[]) { + return languageDefs.sort((a, b) => { + return a.name > b.name ? 1 : -1; + }); +} + +document.addEventListener('WebComponentsReady', () => { + // Parse URL query params. + const params = new URL(document.URL).searchParams; + const debugMode = params.get('outlineDebugMode') === 'true'; + const version = params.get('version'); + + const shadowboxImageId = params.get('image'); + const shadowboxSettings = { + imageId: shadowboxImageId, + metricsUrl: params.get('metricsUrl'), + sentryApiUrl: params.get('sentryDsn'), + watchtowerRefreshSeconds: shadowboxImageId ? 30 : undefined, + }; + + const cloudAccounts = new CloudAccounts(shadowboxSettings, debugMode); + + // Create and start the app. + const language = getLanguageToUse(); + const languageDirection = SUPPORTED_LANGUAGES[language.string()].dir; + document.documentElement.setAttribute('dir', languageDirection); + const appRoot = document.getElementById('appRoot') as AppRoot; + appRoot.language = language.string(); + + const filteredLanguageDefs = Object.values(SUPPORTED_LANGUAGES); + appRoot.supportedLanguages = sortLanguageDefsByName(filteredLanguageDefs); + appRoot.setLanguage(language.string(), languageDirection); + new App(appRoot, version, new ManualServerRepository('manualServers'), cloudAccounts).start(); +}); diff --git a/src/server_manager/web_app/management_urls.spec.ts b/src/server_manager/web_app/management_urls.spec.ts new file mode 100644 index 000000000..9d94a6e25 --- /dev/null +++ b/src/server_manager/web_app/management_urls.spec.ts @@ -0,0 +1,67 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import {parseManualServerConfig} from './management_urls'; + +describe('parseManualServerConfig', () => { + it('basic case', () => { + const result = parseManualServerConfig( + '{"apiUrl":"http://abc.com/xyz", "certSha256":"1234567"}' + ); + expect(result.apiUrl).toEqual('http://abc.com/xyz'); + expect(result.certSha256).toEqual('1234567'); + }); + + it('ignores missing outer braces', () => { + const result = parseManualServerConfig('"apiUrl":"http://abc.com/xyz", "certSha256":"1234567"'); + expect(result.apiUrl).toEqual('http://abc.com/xyz'); + expect(result.certSha256).toEqual('1234567'); + }); + + it('ignores missing quotes on key names', () => { + const result = parseManualServerConfig('apiUrl:"http://abc.com/xyz", "certSha256":"1234567"'); + expect(result.apiUrl).toEqual('http://abc.com/xyz'); + expect(result.certSha256).toEqual('1234567'); + }); + + it('ignores missing quotes on values', () => { + const result = parseManualServerConfig('"apiUrl":http://abc.com/xyz, "certSha256":"1234567"'); + expect(result.apiUrl).toEqual('http://abc.com/xyz'); + expect(result.certSha256).toEqual('1234567'); + }); + + it('ignores content outside of braces', () => { + const result = parseManualServerConfig( + 'working... {"apiUrl":http://abc.com/xyz, "certSha256":"1234567"} ALL DONE' + ); + expect(result.apiUrl).toEqual('http://abc.com/xyz'); + expect(result.certSha256).toEqual('1234567'); + }); + + it('strips whitespace', () => { + const result = parseManualServerConfig( + '{"apiUrl":http://abc.com/xyz, "certSha256":"123 4567"}' + ); + expect(result.apiUrl).toEqual('http://abc.com/xyz'); + expect(result.certSha256).toEqual('1234567'); + }); + + it('strips newlines', () => { + const result = parseManualServerConfig( + '{"apiUrl":http://abc.com/xyz, "certSha256":"123\n4567"}' + ); + expect(result.apiUrl).toEqual('http://abc.com/xyz'); + expect(result.certSha256).toEqual('1234567'); + }); +}); diff --git a/src/server_manager/web_app/management_urls.ts b/src/server_manager/web_app/management_urls.ts new file mode 100644 index 000000000..0744b70a0 --- /dev/null +++ b/src/server_manager/web_app/management_urls.ts @@ -0,0 +1,43 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as jsonic from 'jsonic'; + +import {ManualServerConfig} from '../model/server'; + +// Parses a server management URL (generated by the server install script) leniently. +// TODO: ignore case of key names +// TODO: check value types +export function parseManualServerConfig(userInput: string): ManualServerConfig { + // Remove anything before and after the first and last braces, if found. + const indexOfFirstBrace = userInput.indexOf('{'); + if (indexOfFirstBrace >= 0) { + userInput = userInput.substring(indexOfFirstBrace); + } + const indexOfLastBrace = userInput.lastIndexOf('}'); + if (indexOfLastBrace >= 0) { + userInput = userInput.substring(0, indexOfLastBrace + 1); + } + + // Strip whitespace. + userInput = userInput.replace(/\s+/g, ''); + + const config = jsonic(userInput) as ManualServerConfig; + + if (!config.apiUrl) { + throw new Error('no apiUrl field'); + } + + return config; +} diff --git a/src/server_manager/web_app/manual_server.ts b/src/server_manager/web_app/manual_server.ts new file mode 100644 index 000000000..f7674227b --- /dev/null +++ b/src/server_manager/web_app/manual_server.ts @@ -0,0 +1,104 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import {hexToString} from '../infrastructure/hex_encoding'; +import * as server from '../model/server'; +import {makePathApiClient} from './fetcher'; + +import {ShadowboxServer} from './shadowbox_server'; + +class ManualServer extends ShadowboxServer implements server.ManualServer { + constructor( + id: string, + private manualServerConfig: server.ManualServerConfig, + private forgetCallback: Function + ) { + super(id); + const fingerprint = hexToString(manualServerConfig.certSha256 ?? ''); + this.setManagementApi(makePathApiClient(manualServerConfig.apiUrl, fingerprint)); + } + + getCertificateFingerprint() { + return this.manualServerConfig.certSha256; + } + + forget(): void { + this.forgetCallback(); + } +} + +export class ManualServerRepository implements server.ManualServerRepository { + private servers: server.ManualServer[] = []; + + constructor(private storageKey: string) { + this.loadServers(); + } + + addServer(config: server.ManualServerConfig): Promise { + const existingServer = this.findServer(config); + if (existingServer) { + console.debug('server already added'); + return Promise.resolve(existingServer); + } + const server = this.createServer(config); + this.servers.push(server); + this.storeServers(); + return Promise.resolve(server); + } + + listServers(): Promise { + return Promise.resolve(this.servers); + } + + findServer(config: server.ManualServerConfig): server.ManualServer | undefined { + return this.servers.find((server) => server.getManagementApiUrl() === config.apiUrl); + } + + private loadServers() { + this.servers = []; + const serversJson = localStorage.getItem(this.storageKey); + if (serversJson) { + try { + const serverConfigs = JSON.parse(serversJson); + this.servers = serverConfigs.map((config: server.ManualServerConfig) => { + return this.createServer(config); + }); + } catch (e) { + console.error('Error creating manual servers from localStorage'); + } + } + } + + private storeServers() { + const serverConfigs: server.ManualServerConfig[] = this.servers.map((server) => { + return {apiUrl: server.getManagementApiUrl(), certSha256: server.getCertificateFingerprint()}; + }); + localStorage.setItem(this.storageKey, JSON.stringify(serverConfigs)); + } + + private createServer(config: server.ManualServerConfig) { + const server = new ManualServer(`manual:${config.apiUrl}`, config, () => { + this.forgetServer(server); + }); + return server; + } + + private forgetServer(serverToForget: server.ManualServer): void { + const apiUrl = serverToForget.getManagementApiUrl(); + this.servers = this.servers.filter((server) => { + return apiUrl !== server.getManagementApiUrl(); + }); + this.storeServers(); + } +} diff --git a/src/server_manager/web_app/outline-gcp-create-server-app.ts b/src/server_manager/web_app/outline-gcp-create-server-app.ts new file mode 100644 index 000000000..c551e23db --- /dev/null +++ b/src/server_manager/web_app/outline-gcp-create-server-app.ts @@ -0,0 +1,425 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import '@polymer/paper-dropdown-menu/paper-dropdown-menu'; +import '@polymer/paper-listbox/paper-listbox'; +import '@polymer/paper-input/paper-input'; +import '@polymer/paper-item/paper-item'; + +import './ui_components/outline-step-view'; +import './ui_components/outline-region-picker-step'; + +import {css, customElement, html, internalProperty, LitElement, property} from 'lit-element'; +import {unsafeHTML} from 'lit-html/directives/unsafe-html'; + +import {AppRoot} from './ui_components/app-root'; +import {BillingAccount, Project, Zone, Account} from '../model/gcp'; +import {GcpAccount, isInFreeTier} from './gcp_account'; +import {COMMON_STYLES} from './ui_components/cloud-install-styles'; +import {OutlineRegionPicker} from './ui_components/outline-region-picker-step'; +import {filterOptions, getShortName} from './location_formatting'; +import {CloudLocation} from '../model/location'; + +@customElement('outline-gcp-create-server-app') +export class GcpCreateServerApp extends LitElement { + @property({type: Function}) localize: (msgId: string, ...params: string[]) => string; + @property({type: String}) language: string; + @internalProperty() private currentPage = ''; + @internalProperty() private selectedProjectId = ''; + @internalProperty() private selectedBillingAccountId = ''; + @internalProperty() private isProjectBeingCreated = false; + + private account: GcpAccount; + private project: Project; + private billingAccounts: BillingAccount[] = []; + private regionPicker: OutlineRegionPicker; + private billingAccountsRefreshLoop: number = null; + + static get styles() { + return [ + COMMON_STYLES, + css` + :host { + --paper-input-container-input-color: var(--medium-gray); + } + .container { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + align-items: center; + padding: 156px 0; + font-size: 14px; + } + .card { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: space-between; + margin: 24px 0; + background: var(--background-contrast-color); + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.14), 0 2px 2px 0 rgba(0, 0, 0, 0.12), + 0 1px 3px 0 rgba(0, 0, 0, 0.2); + border-radius: 2px; + } + .section { + padding: 24px 12px; + color: var(--light-gray); + background: var(--background-contrast-color); + border-radius: 2px; + } + .section:not(:first-child) { + margin-top: 8px; + } + .section-header { + padding: 0 6px 0; + display: flex; + } + .section-content { + padding: 0 48px; + } + .instructions { + font-size: 16px; + line-height: 26px; + margin-left: 16px; + flex: 2; + } + .stepcircle { + height: 26px; + width: 26px; + font-size: 14px; + border-radius: 50%; + float: left; + vertical-align: middle; + color: #000; + background-color: #fff; + margin: auto; + text-align: center; + line-height: 26px; + } + @media (min-width: 1025px) { + paper-card { + /* Set min with for the paper-card to grow responsively. */ + min-width: 600px; + } + } + .card p { + color: var(--light-gray); + width: 100%; + text-align: center; + } + #projectName { + width: 250px; + } + #billingAccount { + width: 250px; + } + paper-button { + background: var(--primary-green); + color: var(--light-gray); + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 2px; + } + paper-button[disabled] { + color: var(--medium-gray); + background: transparent; + } + `, + ]; + } + + render() { + switch (this.currentPage) { + case 'billingAccountSetup': + return this.renderBillingAccountSetup(); + case 'projectSetup': + return this.renderProjectSetup(); + case 'regionPicker': + return this.renderRegionPicker(); + default: + } + } + + private renderBillingAccountSetup() { + const openLink = ''; + const closeLink = ''; + return html` + ${this.localize('gcp-billing-title')} + + ${unsafeHTML( + this.localize('gcp-billing-description', 'openLink', openLink, 'closeLink', closeLink) + )} + + + + ${this.localize('gcp-billing-action')} + + + +
+ +

+ ${unsafeHTML( + this.localize('gcp-billing-body', 'openLink', openLink, 'closeLink', closeLink) + )} +

+
+ +
+
`; + } + + private renderProjectSetup() { + return html` + Create your Google Cloud Platform project. + This will create a new project on your GCP account to hold your Outline servers. + + ${this.isProjectBeingCreated + ? // TODO: Support canceling server creation. + html`IN PROGRESS...` + : html` + CREATE PROJECT + `} + +
+
+ 1 +
Name your new Google Cloud Project
+
+
+ + +
+
+ +
+
+ 2 +
Choose your preferred billing method for this project
+
+
+ + + ${this.billingAccounts.map((billingAccount) => { + return html`${billingAccount.name}`; + })} + + +
+
+ ${this.isProjectBeingCreated + ? html`` + : ''} +
`; + } + + private renderRegionPicker() { + return html` + `; + } + + async start(account: Account): Promise { + this.init(); + this.account = account as GcpAccount; + + try { + this.billingAccounts = await this.account.listOpenBillingAccounts(); + const projects = await this.account.listProjects(); + // TODO: We don't support multiple projects atm, but we will want to allow + // the user to choose the appropriate one. + this.project = projects?.[0]; + } catch (e) { + // TODO: Surface this error to the user. + console.warn('Error fetching GCP account info', e); + } + if (await this.isProjectHealthy()) { + this.showRegionPicker(); + } else if (!(this.billingAccounts?.length > 0)) { + this.showBillingAccountSetup(); + // Check every five seconds to see if an account has been added. + this.billingAccountsRefreshLoop = window.setInterval(() => { + try { + this.refreshBillingAccounts(); + } catch (e) { + console.warn('Billing account refresh error', e); + } + }, 5000); + } else { + this.showProjectSetup(this.project); + } + } + + private async isProjectHealthy(): Promise { + return this.project ? await this.account.isProjectHealthy(this.project.id) : false; + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.stopRefreshingBillingAccounts(); + } + + private init() { + this.currentPage = ''; + this.selectedProjectId = ''; + this.selectedBillingAccountId = ''; + this.stopRefreshingBillingAccounts(); + } + + private showBillingAccountSetup(): void { + this.currentPage = 'billingAccountSetup'; + } + + private async refreshBillingAccounts(): Promise { + this.billingAccounts = await this.account.listOpenBillingAccounts(); + + if (this.billingAccounts?.length > 0) { + this.stopRefreshingBillingAccounts(); + if (await this.isProjectHealthy()) { + this.showRegionPicker(); + } else { + this.showProjectSetup(this.project); + } + bringToFront(); + } + } + + public stopRefreshingBillingAccounts(): void { + window.clearInterval(this.billingAccountsRefreshLoop); + this.billingAccountsRefreshLoop = null; + } + + private showError(message: string) { + const appRoot: AppRoot = document.getElementById('appRoot') as AppRoot; + appRoot.showError(message); + } + + private async handleBillingVerificationNextTap(): Promise { + try { + await this.refreshBillingAccounts(); + } catch (e) { + this.showError(this.localize('gcp-billing-error')); + } + if (this.billingAccounts?.length > 0) { + await this.showProjectSetup(); + } else { + this.showError(this.localize('gcp-billing-error-zero')); + } + } + + private async showProjectSetup(existingProject?: Project): Promise { + this.project = existingProject ?? null; + this.selectedProjectId = this.project?.id ?? this.makeProjectName(); + this.selectedBillingAccountId = this.billingAccounts[0].id; + this.currentPage = 'projectSetup'; + } + + private isProjectSetupNextEnabled(projectId: string, billingAccountId: string): boolean { + // TODO: Proper validation + return projectId !== '' && billingAccountId !== ''; + } + + private async handleProjectSetupNextTap(): Promise { + this.isProjectBeingCreated = true; + try { + if (!this.project) { + this.project = await this.account.createProject( + this.selectedProjectId, + this.selectedBillingAccountId + ); + } else { + await this.account.repairProject(this.project.id, this.selectedBillingAccountId); + } + this.showRegionPicker(); + } catch (e) { + this.showError(this.localize('gcp-project-setup-error')); + console.warn('Project setup failed:', e); + } + this.isProjectBeingCreated = false; + } + + private async showRegionPicker(): Promise { + const isProjectHealthy = await this.account.isProjectHealthy(this.project.id); + if (!isProjectHealthy) { + return this.showProjectSetup(); + } + + this.currentPage = 'regionPicker'; + const zoneOptions = await this.account.listLocations(this.project.id); + // Note: This relies on a side effect of the previous call to `await`. + // `this.regionPicker` is null after `this.currentPage`, and is only populated + // asynchronously. + this.regionPicker = this.shadowRoot.querySelector('#regionPicker') as OutlineRegionPicker; + this.regionPicker.options = filterOptions(zoneOptions).map((option) => ({ + markedBestValue: isInFreeTier(option.cloudLocation), + ...option, + })); + } + + private onProjectIdChanged(event: CustomEvent) { + this.selectedProjectId = event.detail.value; + } + private onBillingAccountSelected(event: CustomEvent) { + this.selectedBillingAccountId = event.detail.value; + } + + private async onRegionSelected(event: CustomEvent) { + event.stopPropagation(); + + this.regionPicker.isServerBeingCreated = true; + const zone = event.detail.selectedLocation as Zone; + const name = this.makeLocalizedServerName(zone); + const server = await this.account.createServer(this.project.id, name, zone); + const params = {bubbles: true, composed: true, detail: {server}}; + const serverCreatedEvent = new CustomEvent('GcpServerCreated', params); + this.dispatchEvent(serverCreatedEvent); + } + + private makeProjectName(): string { + return `outline-${Math.random().toString(20).substring(3)}`; + } + + private makeLocalizedServerName(cloudLocation: CloudLocation): string { + const placeName = getShortName(cloudLocation, this.localize); + return this.localize('server-name', 'serverLocation', placeName); + } +} diff --git a/src/server_manager/web_app/server_install.spec.ts b/src/server_manager/web_app/server_install.spec.ts new file mode 100644 index 000000000..5592fe66b --- /dev/null +++ b/src/server_manager/web_app/server_install.spec.ts @@ -0,0 +1,56 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import {getShellExportCommands, ShadowboxSettings} from './server_install'; + +describe('getShellExportCommands', () => { + it('fully populated', () => { + const settings: ShadowboxSettings = { + imageId: 'foo', + metricsUrl: 'https://metrics.example/', + sentryApiUrl: 'https://sentry.example/', + watchtowerRefreshSeconds: 999, + }; + const serverName = 'Outline Server Foo'; + expect(getShellExportCommands(settings, serverName)).toEqual( + "export SB_IMAGE='foo'\n" + + "export WATCHTOWER_REFRESH_SECONDS='999'\n" + + "export SENTRY_API_URL='https://sentry.example/'\n" + + "export SB_METRICS_URL='https://metrics.example/'\n" + + 'export SB_DEFAULT_SERVER_NAME="$(printf \'Outline Server Foo\')"\n' + ); + }); + + it('minimal', () => { + const settings: ShadowboxSettings = { + imageId: null, + metricsUrl: '', + }; + const serverName = ''; + expect(getShellExportCommands(settings, serverName)).toEqual( + 'export SB_DEFAULT_SERVER_NAME="$(printf \'\')"\n' + ); + }); + + it('server name escaping', () => { + const settings: ShadowboxSettings = { + imageId: '', + metricsUrl: null, + }; + const serverName = 'Outline Server فرانكفورت'; + expect(getShellExportCommands(settings, serverName)).toEqual( + 'export SB_DEFAULT_SERVER_NAME="$(printf \'Outline Server \\u0641\\u0631\\u0627\\u0646\\u0643\\u0641\\u0648\\u0631\\u062a\')"\n' + ); + }); +}); diff --git a/src/server_manager/web_app/server_install.ts b/src/server_manager/web_app/server_install.ts new file mode 100644 index 000000000..815ae888a --- /dev/null +++ b/src/server_manager/web_app/server_install.ts @@ -0,0 +1,45 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +/** Represents the settings needed to launch a dockerized shadowbox. */ +export interface ShadowboxSettings { + imageId: string; + metricsUrl: string; + sentryApiUrl?: string; + watchtowerRefreshSeconds?: number; +} + +export function getShellExportCommands(settings: ShadowboxSettings, serverName: string): string { + const variables: {[name: string]: string | number} = { + SB_IMAGE: settings.imageId, + WATCHTOWER_REFRESH_SECONDS: settings.watchtowerRefreshSeconds, + SENTRY_API_URL: settings.sentryApiUrl, + SB_METRICS_URL: settings.metricsUrl, + }; + const lines: string[] = []; + for (const name in variables) { + if (variables[name]) { + lines.push(`export ${name}='${variables[name]}'`); + } + } + lines.push(`export SB_DEFAULT_SERVER_NAME="$(printf '${bashEscape(serverName)}')"`); + return lines.join('\n') + '\n'; +} + +function bashEscape(s: string): string { + // Replace each non-ASCII character with a unicode escape sequence that + // is understood by bash. This avoids an apparent bug in DigitalOcean's + // handling of unicode characters in the user_data value. + return s.replace(/\P{ASCII}/gu, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')); +} diff --git a/src/server_manager/web_app/shadowbox_server.ts b/src/server_manager/web_app/shadowbox_server.ts new file mode 100644 index 000000000..7e4901df6 --- /dev/null +++ b/src/server_manager/web_app/shadowbox_server.ts @@ -0,0 +1,236 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as semver from 'semver'; + +import * as server from '../model/server'; +import {PathApiClient} from '../infrastructure/path_api'; + +interface AccessKeyJson { + id: string; + name: string; + accessUrl: string; +} + +interface ServerConfigJson { + name: string; + metricsEnabled: boolean; + serverId: string; + createdTimestampMs: number; + portForNewAccessKeys: number; + hostnameForAccessKeys: string; + version: string; + // This is the server default data limit. We use this instead of defaultDataLimit for API + // backwards compatibility. + accessKeyDataLimit?: server.DataLimit; +} + +// Byte transfer stats for the past 30 days, including both inbound and outbound. +// TODO: this is copied at src/shadowbox/model/metrics.ts. Both copies should +// be kept in sync, until we can find a way to share code between the web_app +// and shadowbox. +interface DataUsageByAccessKeyJson { + // The accessKeyId should be of type AccessKeyId, however that results in the tsc + // error TS1023: An index signature parameter type must be 'string' or 'number'. + // See https://github.com/Microsoft/TypeScript/issues/2491 + // TODO: this still says "UserId", changing to "AccessKeyId" will require + // a change on the shadowbox server. + bytesTransferredByUserId: {[accessKeyId: string]: number}; +} + +// Converts the access key JSON from the API to its model. +function makeAccessKeyModel(apiAccessKey: AccessKeyJson): server.AccessKey { + return apiAccessKey as server.AccessKey; +} + +export class ShadowboxServer implements server.Server { + private api: PathApiClient; + private serverConfig: ServerConfigJson; + + constructor(private readonly id: string) {} + + getId(): string { + return this.id; + } + + getAccessKey(accessKeyId: server.AccessKeyId): Promise { + return this.api.request('access-keys/' + accessKeyId).then((response) => { + return makeAccessKeyModel(response); + }); + } + + listAccessKeys(): Promise { + console.info('Listing access keys'); + return this.api.request<{accessKeys: AccessKeyJson[]}>('access-keys').then((response) => { + return response.accessKeys.map(makeAccessKeyModel); + }); + } + + async addAccessKey(): Promise { + console.info('Adding access key'); + return makeAccessKeyModel(await this.api.request('access-keys', 'POST')); + } + + renameAccessKey(accessKeyId: server.AccessKeyId, name: string): Promise { + console.info('Renaming access key'); + return this.api.requestForm('access-keys/' + accessKeyId + '/name', 'PUT', {name}); + } + + removeAccessKey(accessKeyId: server.AccessKeyId): Promise { + console.info('Removing access key'); + return this.api.request('access-keys/' + accessKeyId, 'DELETE'); + } + + async setDefaultDataLimit(limit: server.DataLimit): Promise { + console.info(`Setting server default data limit: ${JSON.stringify(limit)}`); + await this.api.requestJson(this.getDefaultDataLimitPath(), 'PUT', {limit}); + this.serverConfig.accessKeyDataLimit = limit; + } + + async removeDefaultDataLimit(): Promise { + console.info(`Removing server default data limit`); + await this.api.request(this.getDefaultDataLimitPath(), 'DELETE'); + delete this.serverConfig.accessKeyDataLimit; + } + + getDefaultDataLimit(): server.DataLimit | undefined { + return this.serverConfig.accessKeyDataLimit; + } + + private getDefaultDataLimitPath(): string { + const version = this.getVersion(); + if (semver.gte(version, '1.4.0')) { + // Data limits became a permanent feature in shadowbox v1.4.0. + return 'server/access-key-data-limit'; + } + return 'experimental/access-key-data-limit'; + } + + async setAccessKeyDataLimit(keyId: server.AccessKeyId, limit: server.DataLimit): Promise { + console.info(`Setting data limit of ${limit.bytes} bytes for access key ${keyId}`); + await this.api.requestJson(`access-keys/${keyId}/data-limit`, 'PUT', {limit}); + } + + async removeAccessKeyDataLimit(keyId: server.AccessKeyId): Promise { + console.info(`Removing data limit from access key ${keyId}`); + await this.api.request(`access-keys/${keyId}/data-limit`, 'DELETE'); + } + + async getDataUsage(): Promise { + const jsonResponse = await this.api.request('metrics/transfer'); + const usageMap = new Map(); + for (const [accessKeyId, bytes] of Object.entries(jsonResponse.bytesTransferredByUserId)) { + usageMap.set(accessKeyId, bytes ?? 0); + } + return usageMap; + } + + getName(): string { + return this.serverConfig?.name; + } + + async setName(name: string): Promise { + console.info('Setting server name'); + await this.api.requestJson('name', 'PUT', {name}); + this.serverConfig.name = name; + } + + getVersion(): string { + return this.serverConfig.version; + } + + getMetricsEnabled(): boolean { + return this.serverConfig.metricsEnabled; + } + + async setMetricsEnabled(metricsEnabled: boolean): Promise { + const action = metricsEnabled ? 'Enabling' : 'Disabling'; + console.info(`${action} metrics`); + await this.api.requestJson('metrics/enabled', 'PUT', {metricsEnabled}); + this.serverConfig.metricsEnabled = metricsEnabled; + } + + getMetricsId(): string { + return this.serverConfig.serverId; + } + + isHealthy(timeoutMs = 30000): Promise { + return new Promise((fulfill, _reject) => { + // Query the API and expect a successful response to validate that the + // service is up and running. + this.getServerConfig().then( + (serverConfig) => { + this.serverConfig = serverConfig; + fulfill(true); + }, + (_e) => { + fulfill(false); + } + ); + // Return not healthy if API doesn't complete within timeoutMs. + setTimeout(() => { + fulfill(false); + }, timeoutMs); + }); + } + + getCreatedDate(): Date { + return new Date(this.serverConfig.createdTimestampMs); + } + + async setHostnameForAccessKeys(hostname: string): Promise { + console.info(`setHostname ${hostname}`); + this.serverConfig.hostnameForAccessKeys = hostname; + await this.api.requestJson('server/hostname-for-access-keys', 'PUT', {hostname}); + this.serverConfig.hostnameForAccessKeys = hostname; + } + + getHostnameForAccessKeys(): string { + try { + return this.serverConfig?.hostnameForAccessKeys ?? new URL(this.api.base).hostname; + } catch (e) { + return ''; + } + } + + getPortForNewAccessKeys(): number | undefined { + try { + if (typeof this.serverConfig.portForNewAccessKeys !== 'number') { + return undefined; + } + return this.serverConfig.portForNewAccessKeys; + } catch (e) { + return undefined; + } + } + + async setPortForNewAccessKeys(newPort: number): Promise { + console.info(`setPortForNewAccessKeys: ${newPort}`); + await this.api.requestJson('server/port-for-new-access-keys', 'PUT', {port: newPort}); + this.serverConfig.portForNewAccessKeys = newPort; + } + + private async getServerConfig(): Promise { + console.info('Retrieving server configuration'); + return await this.api.request('server'); + } + + protected setManagementApi(api: PathApiClient): void { + this.api = api; + } + + getManagementApiUrl(): string { + return this.api.base; + } +} diff --git a/src/server_manager/web_app/start.action.sh b/src/server_manager/web_app/start.action.sh new file mode 100755 index 000000000..c800a85f6 --- /dev/null +++ b/src/server_manager/web_app/start.action.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# +# Copyright 2018 The Outline Authors +# +# 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. + +set -eu + +rm -rf "${BUILD_DIR}/server_manager/web_app" + +run_action server_manager/web_app/build_install_script + +webpack-dev-server --config=src/server_manager/browser.webpack.js --open diff --git a/src/server_manager/web_app/start_gallery.action.sh b/src/server_manager/web_app/start_gallery.action.sh new file mode 100755 index 000000000..fba8924af --- /dev/null +++ b/src/server_manager/web_app/start_gallery.action.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Copyright 2018 The Outline Authors +# +# 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. + +set -eu + +webpack-dev-server --config=src/server_manager/gallery.webpack.js --open \ No newline at end of file diff --git a/src/server_manager/web_app/survey.spec.ts b/src/server_manager/web_app/survey.spec.ts new file mode 100644 index 000000000..36ec77796 --- /dev/null +++ b/src/server_manager/web_app/survey.spec.ts @@ -0,0 +1,101 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import {InMemoryStorage} from '../infrastructure/memory_storage'; + +import {OutlineSurveys} from './survey'; + +describe('Surveys', () => { + it('presents data limits surveys with the correct arguments', async (done) => { + const view = new FakeSurveyDialog(); + const storage = new InMemoryStorage(); + const surveys = new OutlineSurveys(view, storage, 0); + + await surveys.presentDataLimitsEnabledSurvey(); + expect(view.title).toEqual('survey-data-limits-title'); + expect(view.surveyLink).toEqual( + 'https://docs.google.com/forms/d/e/1FAIpQLSeXQ5WUHXQHlF1Ul_ViX52GjTUPlrRB_7rhwbol3dKJfM4Kiw/viewform' + ); + + await surveys.presentDataLimitsDisabledSurvey(); + expect(view.title).toEqual('survey-data-limits-title'); + expect(view.surveyLink).toEqual( + 'https://docs.google.com/forms/d/e/1FAIpQLSc2ZNx0C1a-alFlXLxhJ8jWk-WgcxqKilFoQ5ToI8HBOK9qRA/viewform' + ); + done(); + }); + + it('presents data limits surveys after the default prompt impression delay', async (done) => { + const TEST_PROMPT_IMPRESSION_DELAY_MS = 750; + const view = new FakeSurveyDialog(); + const storage = new InMemoryStorage(); + const surveys = new OutlineSurveys(view, storage, TEST_PROMPT_IMPRESSION_DELAY_MS); + + let start = Date.now(); + await surveys.presentDataLimitsEnabledSurvey(); + let delay = Date.now() - start; + expect(delay).toBeGreaterThanOrEqual(TEST_PROMPT_IMPRESSION_DELAY_MS); + + start = Date.now(); + await surveys.presentDataLimitsDisabledSurvey(); + delay = Date.now() - start; + expect(delay).toBeGreaterThanOrEqual(TEST_PROMPT_IMPRESSION_DELAY_MS); + done(); + }); + + it('presents data limits surveys once', async (done) => { + const view = new FakeSurveyDialog(); + const storage = new InMemoryStorage(); + const surveys = new OutlineSurveys(view, storage, 0); + + await surveys.presentDataLimitsEnabledSurvey(); + expect(storage.getItem('dataLimitsEnabledSurvey')).toEqual('true'); + await surveys.presentDataLimitsDisabledSurvey(); + expect(storage.getItem('dataLimitsDisabledSurvey')).toEqual('true'); + + spyOn(view, 'open'); + await surveys.presentDataLimitsEnabledSurvey(); + expect(view.open).not.toHaveBeenCalled(); + await surveys.presentDataLimitsDisabledSurvey(); + expect(view.open).not.toHaveBeenCalled(); + done(); + }); + + it('does not present data limits surveys after availability date', async (done) => { + const view = new FakeSurveyDialog(); + const storage = new InMemoryStorage(); + const yesterday = new Date(new Date().setDate(new Date().getDate() - 1)); + const surveys = new OutlineSurveys(view, storage, 0, yesterday); + spyOn(view, 'open'); + + await surveys.presentDataLimitsEnabledSurvey(); + expect(view.open).not.toHaveBeenCalled(); + await surveys.presentDataLimitsDisabledSurvey(); + expect(view.open).not.toHaveBeenCalled(); + done(); + }); +}); + +class FakeSurveyDialog implements polymer.Base { + title: string; + surveyLink: string; + is: 'fake-survey-dialog'; + localize(messageId: string) { + return messageId; + } + open(title: string, surveyLink: string) { + this.title = title; + this.surveyLink = surveyLink; + } +} diff --git a/src/server_manager/web_app/survey.ts b/src/server_manager/web_app/survey.ts new file mode 100644 index 000000000..3afbf736f --- /dev/null +++ b/src/server_manager/web_app/survey.ts @@ -0,0 +1,66 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import {sleep} from '../infrastructure/sleep'; +import {Surveys} from '../model/survey'; + +export const DEFAULT_PROMPT_IMPRESSION_DELAY_MS = 3000; + +export class OutlineSurveys implements Surveys { + constructor( + private view: polymer.Base, + private storage: Storage = localStorage, + private promptImpressionDelayMs: number = DEFAULT_PROMPT_IMPRESSION_DELAY_MS, + private dataLimitsAvailabilityDate?: Date + ) {} + + async presentDataLimitsEnabledSurvey() { + if (this.isSurveyExpired(this.dataLimitsAvailabilityDate)) { + return; + } + await this.presentSurvey( + 'dataLimitsEnabledSurvey', + this.view.localize('survey-data-limits-title'), + 'https://docs.google.com/forms/d/e/1FAIpQLSeXQ5WUHXQHlF1Ul_ViX52GjTUPlrRB_7rhwbol3dKJfM4Kiw/viewform' + ); + } + + async presentDataLimitsDisabledSurvey() { + if (this.isSurveyExpired(this.dataLimitsAvailabilityDate)) { + return; + } + await this.presentSurvey( + 'dataLimitsDisabledSurvey', + this.view.localize('survey-data-limits-title'), + 'https://docs.google.com/forms/d/e/1FAIpQLSc2ZNx0C1a-alFlXLxhJ8jWk-WgcxqKilFoQ5ToI8HBOK9qRA/viewform' + ); + } + + // Displays a survey dialog for`surveyId` with title `surveyTitle` and a link to `surveyLink` + // after `promptImpressionDelayMs` has elapsed. Rate-limits the survey to once per user. + private async presentSurvey(surveyId: string, surveyTitle: string, surveyLink: string) { + if (this.storage.getItem(surveyId)) { + return; + } + await sleep(this.promptImpressionDelayMs); + this.view.open(surveyTitle, surveyLink); + this.storage.setItem(surveyId, 'true'); + } + + // Returns whether `surveyAvailabilityDate` is in the past. + private isSurveyExpired(surveyAvailabilityDate: Date | undefined) { + const now = new Date(); + return surveyAvailabilityDate && now > surveyAvailabilityDate; + } +} diff --git a/src/server_manager/web_app/test.action.sh b/src/server_manager/web_app/test.action.sh new file mode 100755 index 000000000..9e5da6a5a --- /dev/null +++ b/src/server_manager/web_app/test.action.sh @@ -0,0 +1,18 @@ +#!/bin/bash -eu +# +# Copyright 2020 The Outline Authors +# +# 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. + +npm run action server_manager/web_app/build_install_script +karma start "${ROOT_DIR}/src/server_manager/web_app/karma.conf.js" diff --git a/src/server_manager/web_app/testing/models.ts b/src/server_manager/web_app/testing/models.ts new file mode 100644 index 000000000..d6f3c27c5 --- /dev/null +++ b/src/server_manager/web_app/testing/models.ts @@ -0,0 +1,282 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import * as accounts from '../../model/accounts'; +import * as digitalocean from '../../model/digitalocean'; +import * as gcp from '../../model/gcp'; +import * as server from '../../model/server'; + +export class FakeDigitalOceanAccount implements digitalocean.Account { + private servers: server.ManagedServer[] = []; + + constructor(private accessToken = 'fake-access-token') {} + + getId(): string { + return 'account-id'; + } + async getName(): Promise { + return 'fake-digitalocean-account-name'; + } + async getStatus(): Promise { + return { + needsBillingInfo: false, + needsEmailVerification: false, + dropletLimit: 3, + hasReachedLimit: false, + }; + } + listServers() { + return Promise.resolve(this.servers); + } + async hasReachedLimit(): Promise { + return false; + } + listLocations() { + return Promise.resolve([ + { + cloudLocation: new digitalocean.Region('AMS999'), + available: true, + }, + { + cloudLocation: new digitalocean.Region('FRA999'), + available: false, + }, + ]); + } + createServer(region: digitalocean.Region) { + const newServer = new FakeManagedServer(region.id, false); + this.servers.push(newServer); + return Promise.resolve(newServer); + } + getAccessToken(): string { + return this.accessToken; + } +} + +export class FakeGcpAccount implements gcp.Account { + constructor( + private refreshToken = 'fake-access-token', + private billingAccounts: gcp.BillingAccount[] = [], + private locations: gcp.ZoneOption[] = [] + ) {} + + getId() { + return 'id'; + } + async getName(): Promise { + return 'fake-gcp-account-name'; + } + getRefreshToken(): string { + return this.refreshToken; + } + createServer(_projectId: string, _name: string, _zone: gcp.Zone): Promise { + return undefined; + } + async listLocations(_projectId: string): Promise> { + return this.locations; + } + async listServers(_projectId: string): Promise { + return []; + } + async createProject(_id: string, _billingAccountId: string): Promise { + return { + id: 'project-id', + name: 'project-name', + }; + } + async isProjectHealthy(_projectId: string): Promise { + return true; + } + async listOpenBillingAccounts(): Promise { + return this.billingAccounts; + } + async listProjects(): Promise { + return []; + } +} + +export class FakeServer implements server.Server { + private name = 'serverName'; + private metricsId: string; + private metricsEnabled = false; + apiUrl: string; + constructor(protected id: string) { + this.metricsId = Math.random().toString(); + } + getId() { + return this.id; + } + getName() { + return this.name; + } + setName(name: string) { + this.name = name; + return Promise.resolve(); + } + getVersion() { + return '1.2.3'; + } + getAccessKey(accessKeyId: server.AccessKeyId) { + return Promise.reject(new Error(`Access key "${accessKeyId}" not found`)); + } + listAccessKeys() { + return Promise.resolve([]); + } + getMetricsEnabled() { + return this.metricsEnabled; + } + setMetricsEnabled(metricsEnabled: boolean) { + this.metricsEnabled = metricsEnabled; + return Promise.resolve(); + } + getMetricsId() { + return this.metricsId; + } + isHealthy() { + return Promise.resolve(true); + } + getCreatedDate() { + return new Date(); + } + getDataUsage() { + return Promise.resolve(new Map()); + } + addAccessKey() { + return Promise.reject(new Error('FakeServer.addAccessKey not implemented')); + } + renameAccessKey(_accessKeyId: server.AccessKeyId, _name: string) { + return Promise.reject(new Error('FakeServer.renameAccessKey not implemented')); + } + removeAccessKey(_accessKeyId: server.AccessKeyId) { + return Promise.reject(new Error('FakeServer.removeAccessKey not implemented')); + } + setHostnameForAccessKeys(_hostname: string) { + return Promise.reject(new Error('FakeServer.setHostname not implemented')); + } + getHostnameForAccessKeys() { + return 'fake-server'; + } + getManagementApiUrl() { + return this.apiUrl || Math.random().toString(); + } + getPortForNewAccessKeys(): number | undefined { + return undefined; + } + setPortForNewAccessKeys(): Promise { + return Promise.reject(new Error('FakeServer.setPortForNewAccessKeys not implemented')); + } + setAccessKeyDataLimit(_accessKeyId: string, _limit: server.DataLimit): Promise { + return Promise.reject(new Error('FakeServer.setAccessKeyDataLimit not implemented')); + } + removeAccessKeyDataLimit(_accessKeyId: string): Promise { + return Promise.reject(new Error('FakeServer.removeAccessKeyDataLimit not implemented')); + } + setDefaultDataLimit(_limit: server.DataLimit): Promise { + return Promise.reject(new Error('FakeServer.setDefaultDataLimit not implemented')); + } + removeDefaultDataLimit(): Promise { + return Promise.resolve(); + } + getDefaultDataLimit(): server.DataLimit | undefined { + return undefined; + } +} + +export class FakeManualServer extends FakeServer implements server.ManualServer { + constructor(public manualServerConfig: server.ManualServerConfig) { + super(manualServerConfig.apiUrl); + } + getManagementApiUrl() { + return this.manualServerConfig.apiUrl; + } + forget() { + return Promise.reject(new Error('FakeManualServer.forget not implemented')); + } + getCertificateFingerprint() { + return this.manualServerConfig.certSha256; + } +} + +export class FakeManualServerRepository implements server.ManualServerRepository { + private servers: server.ManualServer[] = []; + + addServer(config: server.ManualServerConfig) { + const newServer = new FakeManualServer(config); + this.servers.push(newServer); + return Promise.resolve(newServer); + } + + findServer(config: server.ManualServerConfig) { + return this.servers.find((server) => server.getManagementApiUrl() === config.apiUrl); + } + + listServers() { + return Promise.resolve(this.servers); + } +} + +export class FakeManagedServer extends FakeServer implements server.ManagedServer { + constructor(id: string, private isInstalled = true) { + super(id); + } + async *monitorInstallProgress() { + yield 0.5; + if (!this.isInstalled) { + // Leave the progress bar at 0.5 and never return. + await new Promise(() => {}); + } + } + getHost() { + return { + getMonthlyOutboundTransferLimit: () => ({terabytes: 1}), + getMonthlyCost: () => ({usd: 5}), + getCloudLocation: () => new digitalocean.Region('AMS999'), + delete: () => Promise.resolve(), + getHostId: () => 'fake-host-id', + }; + } +} + +export class FakeCloudAccounts implements accounts.CloudAccounts { + constructor( + private digitalOceanAccount: digitalocean.Account = null, + private gcpAccount: gcp.Account = null + ) {} + + connectDigitalOceanAccount(accessToken: string): digitalocean.Account { + this.digitalOceanAccount = new FakeDigitalOceanAccount(accessToken); + return this.digitalOceanAccount; + } + + connectGcpAccount(refreshToken: string): gcp.Account { + this.gcpAccount = new FakeGcpAccount(refreshToken); + return this.gcpAccount; + } + + disconnectDigitalOceanAccount(): void { + this.digitalOceanAccount = null; + } + + disconnectGcpAccount(): void { + this.gcpAccount = null; + } + + getDigitalOceanAccount(): digitalocean.Account { + return this.digitalOceanAccount; + } + + getGcpAccount(): gcp.Account { + return this.gcpAccount; + } +} diff --git a/src/server_manager/web_app/ui_components/app-root.ts b/src/server_manager/web_app/ui_components/app-root.ts new file mode 100644 index 000000000..57699dd56 --- /dev/null +++ b/src/server_manager/web_app/ui_components/app-root.ts @@ -0,0 +1,1162 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; +import '@polymer/app-layout/app-drawer/app-drawer'; +import '@polymer/app-layout/app-drawer-layout/app-drawer-layout'; +import '@polymer/app-layout/app-toolbar/app-toolbar'; +import '@polymer/iron-icon/iron-icon'; +import '@polymer/iron-icons/iron-icons'; +import '@polymer/iron-pages/iron-pages'; +import '@polymer/paper-icon-button/paper-icon-button'; +import '@polymer/paper-toast/paper-toast'; +import '@polymer/paper-dialog/paper-dialog'; +import '@polymer/paper-dialog-scrollable/paper-dialog-scrollable'; +import '@polymer/paper-listbox/paper-listbox'; +import '@polymer/paper-menu-button/paper-menu-button'; +import './cloud-install-styles'; +import './outline-about-dialog'; +import './outline-do-oauth-step'; +import './outline-gcp-oauth-step'; +import '../outline-gcp-create-server-app'; +import './outline-feedback-dialog'; +import './outline-survey-dialog'; +import './outline-intro-step'; +import './outline-per-key-data-limit-dialog'; +import './outline-language-picker'; +import './outline-manual-server-entry'; +import './outline-modal-dialog'; +import './outline-region-picker-step'; +import './outline-server-list'; +import './outline-tos-view'; + +import {AppLocalizeBehavior} from '@polymer/app-localize-behavior/app-localize-behavior'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; +import {PolymerElement} from '@polymer/polymer/polymer-element'; +import {DisplayCloudId} from './cloud-assets'; + +import type {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin'; +import type {AppDrawerElement} from '@polymer/app-layout/app-drawer/app-drawer'; +import type {AppDrawerLayoutElement} from '@polymer/app-layout/app-drawer-layout/app-drawer-layout'; +import type {PaperDialogElement} from '@polymer/paper-dialog/paper-dialog'; +import type {PaperToastElement} from '@polymer/paper-toast/paper-toast'; +import type {PolymerElementProperties} from '@polymer/polymer/interfaces'; +import type {OutlineRegionPicker} from './outline-region-picker-step'; +import type {OutlineDoOauthStep} from './outline-do-oauth-step'; +import type {GcpConnectAccountApp} from './outline-gcp-oauth-step'; +import type {GcpCreateServerApp} from '../outline-gcp-create-server-app'; +import type {OutlineServerList, ServerViewListEntry} from './outline-server-list'; +import type {OutlineManualServerEntry} from './outline-manual-server-entry'; +import type {OutlinePerKeyDataLimitDialog} from './outline-per-key-data-limit-dialog'; +import type {OutlineFeedbackDialog} from './outline-feedback-dialog'; +import type {OutlineAboutDialog} from './outline-about-dialog'; +import type {OutlineShareDialog} from './outline-share-dialog'; +import type {OutlineMetricsOptionDialog} from './outline-metrics-option-dialog'; +import type {OutlineModalDialog} from './outline-modal-dialog'; +import type {ServerView} from './outline-server-view'; +import type {LanguageDef} from './outline-language-picker'; + +const TOS_ACK_LOCAL_STORAGE_KEY = 'tos-ack'; + +/** A cloud account to be displayed */ +type AccountListEntry = { + id: string; + name: string; +}; + +/** An access key to be displayed */ +export type ServerListEntry = { + id: string; + accountId: string; + name: string; + isSynced: boolean; +}; + +// mixinBehaviors() returns `any`, but the documentation indicates that +// this is the actual return type. +const polymerElementWithLocalize = mixinBehaviors( + AppLocalizeBehavior, + PolymerElement +) as new () => PolymerElement & LegacyElementMixin & AppLocalizeBehavior; + +export class AppRoot extends polymerElementWithLocalize { + static get template() { + return html` + + + + + +
+ + + + + +
+
+ + +
+ ${this.expandedServersTemplate()} +
+ + +
+ + [[localize('servers-add')]] + +
+ + + + + [[localize('manager-resources')]] + + + [[localize('nav-data-collection')]] + [[localize('nav-feedback')]] + [[localize('nav-help')]] + [[localize('nav-about')]] + + +
+ + +
+ + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ [[localize('close')]] +
+
+ + + + + [[localize('error-licenses')]] + + +
+ [[localize('close')]] +
+
+ +`; + } + + static expandedServersTemplate() { + return html` + +
+
+ [[localize('servers-digitalocean')]] + + +
+

[[localize('digitalocean-disconnect-account')]]

+ +
+ [[localize('disconnect')]] +
+
+
+
+
+ +
+
+ +
+
+ [[localize('servers-gcp')]] + + +
+

[[localize('gcp-disconnect-account')]]

+ +
+ [[localize('disconnect')]] +
+
+
+
+
+ +
+
+ +
+
+ [[localize('servers-manual')]] +
+
+ +
+
+ `; + } + + static minimizedServersTemplate() { + return html` + + + + + + + `; + } + + static get is() { + return 'app-root'; + } + + static get properties(): PolymerElementProperties { + return { + // Properties language and useKeyIfMissing are used by Polymer.AppLocalizeBehavior. + language: {type: String}, + supportedLanguages: {type: Array}, + useKeyIfMissing: {type: Boolean}, + serverList: {type: Array}, + selectedServerId: {type: String}, + digitalOceanAccount: Object, + gcpAccount: Object, + outlineVersion: String, + userAcceptedTos: { + type: Boolean, + // Get notified when the user clicks the OK button in the ToS view + observer: '_userAcceptedTosChanged', + }, + hasAcceptedTos: { + type: Boolean, + computed: '_computeHasAcceptedTermsOfService(userAcceptedTos)', + }, + currentPage: { + type: String, + observer: '_currentPageChanged', + }, + shouldShowSideBar: {type: Boolean}, + showManagerResourcesLink: {type: Boolean}, + }; + } + + selectedServerId = ''; + language = ''; + supportedLanguages: LanguageDef[] = []; + useKeyIfMissing = true; + serverList: ServerListEntry[] = []; + digitalOceanAccount: AccountListEntry = null; + gcpAccount: AccountListEntry = null; + outlineVersion = ''; + currentPage = 'intro'; + shouldShowSideBar = false; + showManagerResourcesLink = false; + + constructor() { + super(); + + this.addEventListener('RegionSelected', this.handleRegionSelected); + this.addEventListener( + 'SetUpGenericCloudProviderRequested', + this.handleSetUpGenericCloudProviderRequested + ); + this.addEventListener('SetUpAwsRequested', this.handleSetUpAwsRequested); + this.addEventListener('SetUpGcpRequested', this.handleSetUpGcpRequested); + this.addEventListener('ManualServerEntryCancelled', this.handleManualCancelled); + } + + /** + * Loads a new translation file and returns a Promise which resolves when the file is loaded or + * rejects when there was an error loading translations. + * + * @param language The language code to load translations for, eg 'en' + */ + loadLanguageResources(language: string) { + const localizeResourcesResponder = new Promise((resolve, reject) => { + // loadResources uses events and continuation instead of Promises. In order to make this + // function easier to use, we wrap the language-changing logic in event handlers which + // resolve or reject the Promise. Note that they need to clean up whichever event handler + // didn't fire so we don't leak it, which could cause future language changes to not work + // properly by triggering old event listeners. + const successHandler = () => { + this.removeEventListener('app-localize-resources-error', failureHandler); + resolve(); + }; + const failureHandler = () => { + this.removeEventListener('app-localize-resources-loaded', successHandler); + reject(new Error(`Failed to load resources for language ${language}`)); + }; + this.addEventListener('app-localize-resources-loaded', successHandler, {once: true}); + this.addEventListener('app-localize-resources-error', failureHandler, {once: true}); + }); + + const messagesUrl = `./messages/${language}.json`; + this.loadResources(messagesUrl, language, /* merge= */ false); + return localizeResourcesResponder; + } + + /** + * Sets the language and direction for the application + * @param language The ISO language code for the new language, e.g. 'en'. + */ + async setLanguage(language: string, direction: 'rtl' | 'ltr') { + await this.loadLanguageResources(language); + + const alignDir = direction === 'ltr' ? 'left' : 'right'; + (this.$.appDrawer as AppDrawerElement).align = alignDir; + (this.$.sideBar as AppDrawerElement).align = alignDir; + this.language = language; + + this.showManagerResourcesLink = this.hasTranslation('manager-resources'); + } + + hasTranslation(key: string) { + let message; + + try { + message = this.localize(key); + } catch (e) { + // failed to find translation + message = ''; + } + + return message !== key && message !== ''; + } + + showIntro() { + this.maybeCloseDrawer(); + this.selectedServerId = ''; + this.currentPage = 'intro'; + } + + getDigitalOceanOauthFlow(onCancel: Function): OutlineDoOauthStep { + const oauthFlow = this.$.digitalOceanOauth as OutlineDoOauthStep; + oauthFlow.onCancel = onCancel; + return oauthFlow; + } + + showDigitalOceanOauthFlow() { + this.currentPage = 'digitalOceanOauth'; + } + + getAndShowDigitalOceanOauthFlow(onCancel: Function) { + this.currentPage = 'digitalOceanOauth'; + const oauthFlow = this.getDigitalOceanOauthFlow(onCancel); + oauthFlow.showConnectAccount(); + return oauthFlow; + } + + getAndShowGcpOauthFlow(onCancel: Function) { + this.currentPage = 'gcpOauth'; + const oauthFlow = this.$.gcpOauth as GcpConnectAccountApp; + oauthFlow.onCancel = onCancel; + return oauthFlow; + } + + getAndShowGcpCreateServerApp(): GcpCreateServerApp { + this.currentPage = 'gcpCreateServer'; + return this.$.gcpCreateServer as GcpCreateServerApp; + } + + getAndShowRegionPicker(): OutlineRegionPicker { + this.currentPage = 'regionPicker'; + const regionPicker = this.$.regionPicker as OutlineRegionPicker; + regionPicker.reset(); + return regionPicker; + } + + getManualServerEntry() { + return this.$.manualEntry as OutlineManualServerEntry; + } + + showServerView() { + this.currentPage = 'serverView'; + } + + _currentPageChanged() { + if (this.currentPage !== 'gcpCreateServer') { + // The refresh loop will be restarted by App, which calls + // GcpCreateServerApp.start() whenever it switches back to this page. + (this.$.gcpCreateServer as GcpCreateServerApp).stopRefreshingBillingAccounts(); + } + } + + /** Gets the ServerView for the server given by its id */ + getServerView(displayServerId: string): Promise { + const serverList = this.shadowRoot.querySelector('#serverView'); + return serverList.getServerView(displayServerId); + } + + handleRegionSelected(e: CustomEvent) { + this.fire('SetUpDigitalOceanServerRequested', {region: e.detail.selectedLocation}); + } + + handleSetUpGenericCloudProviderRequested() { + this.handleManualServerSelected('generic'); + } + + handleSetUpAwsRequested() { + this.handleManualServerSelected('aws'); + } + + handleSetUpGcpRequested() { + this.handleManualServerSelected('gcp'); + } + + handleManualServerSelected(cloudProvider: 'generic' | 'aws' | 'gcp') { + const manualEntry = this.$.manualEntry as OutlineManualServerEntry; + manualEntry.clear(); + manualEntry.cloudProvider = cloudProvider; + this.currentPage = 'manualEntry'; + } + + handleManualCancelled() { + this.currentPage = 'intro'; + } + + showError(errorMsg: string) { + this.showToast(errorMsg, Infinity); + } + + showNotification(message: string, durationMs = 3000) { + this.showToast(message, durationMs); + } + + /** + * Show a toast with a message + * @param duration in seconds + */ + showToast(message: string, duration: number) { + const toast = this.$.toast as PaperToastElement; + toast.close(); + // Defer in order to trigger the toast animation, otherwise the + // update happens in place. + setTimeout(() => { + toast.show({ + text: message, + duration, + noOverlap: true, + }); + }, 0); + } + + closeError() { + (this.$.toast as PaperToastElement).close(); + } + + /** + * @param cb a function which accepts a single boolean which is true + * iff + * the user chose to retry the failing operation. + */ + showConnectivityDialog(cb: (retry: boolean) => void) { + const dialogTitle = this.localize('error-connectivity-title'); + const dialogText = this.localize('error-connectivity'); + this.showModalDialog(dialogTitle, dialogText, [ + this.localize('digitalocean-disconnect'), + this.localize('retry'), + ]).then((clickedButtonIndex) => { + cb(clickedButtonIndex === 1); // pass true if user clicked retry + }); + } + + getConfirmation(title: string, text: string, confirmButtonText: string, continueFunc: Function) { + this.showModalDialog(title, text, [this.localize('cancel'), confirmButtonText]).then( + (clickedButtonIndex) => { + if (clickedButtonIndex === 1) { + // user clicked to confirm. + continueFunc(); + } + } + ); + } + + showManualServerError(errorTitle: string, errorText: string) { + this.showModalDialog(errorTitle, errorText, [ + this.localize('cancel'), + this.localize('retry'), + ]).then((clickedButtonIndex) => { + const manualEntry = this.$.manualEntry as OutlineManualServerEntry; + if (clickedButtonIndex === 1) { + manualEntry.retryTapped(); + } + }); + } + + _hasManualServers(serverList: ServerListEntry[]) { + return serverList.filter((server) => !server.accountId).length > 0; + } + + _userAcceptedTosChanged(userAcceptedTos: boolean) { + if (userAcceptedTos) { + window.localStorage[TOS_ACK_LOCAL_STORAGE_KEY] = Date.now(); + } + } + + _computeHasAcceptedTermsOfService(userAcceptedTos: boolean) { + return userAcceptedTos || !!window.localStorage[TOS_ACK_LOCAL_STORAGE_KEY]; + } + + _toggleAppDrawer() { + const drawerLayout = this.$.drawerLayout as AppDrawerLayoutElement; + const drawerNarrow = drawerLayout.narrow; + const forceNarrow = drawerLayout.forceNarrow; + if (drawerNarrow) { + if (forceNarrow) { + // The window width is below the responsive threshold. Do not force narrow mode. + drawerLayout.forceNarrow = false; + } + (this.$.appDrawer as AppDrawerElement).toggle(); + } else { + // Forcing narrow mode when the window width is above the responsive threshold effectively + // collapses the drawer. Conversely, reverting force narrow expands the drawer. + drawerLayout.forceNarrow = !forceNarrow; + } + } + + maybeCloseDrawer() { + const drawerLayout = this.$.drawerLayout as AppDrawerLayoutElement; + if (drawerLayout.narrow || drawerLayout.forceNarrow) { + (this.$.appDrawer as AppDrawerElement).close(); + } + } + + submitFeedbackTapped() { + (this.$.feedbackDialog as OutlineFeedbackDialog).open(); + this.maybeCloseDrawer(); + } + + aboutTapped() { + (this.$.aboutDialog as OutlineAboutDialog).open(); + this.maybeCloseDrawer(); + } + + _digitalOceanSignOutTapped() { + this.fire('DigitalOceanSignOutRequested'); + } + + _gcpSignOutTapped() { + this.fire('GcpSignOutRequested'); + } + + openManualInstallFeedback(prepopulatedMessage: string) { + (this.$.feedbackDialog as OutlineFeedbackDialog).open(prepopulatedMessage, true); + } + + openShareDialog(accessKey: string, s3Url: string) { + (this.$.shareDialog as OutlineShareDialog).open(accessKey, s3Url); + } + + openPerKeyDataLimitDialog( + keyName: string, + activeDataLimitBytes: number, + onDataLimitSet: (dataLimitBytes: number) => Promise, + onDataLimitRemoved: () => Promise + ) { + // attach listeners here + (this.$.perKeyDataLimitDialog as OutlinePerKeyDataLimitDialog).open( + keyName, + activeDataLimitBytes, + onDataLimitSet, + onDataLimitRemoved + ); + } + + openGetConnectedDialog(inviteUrl: string) { + const dialog = this.$.getConnectedDialog as PaperDialogElement; + if (dialog.children.length > 1) { + return; // The iframe is already loading. + } + // Reset the iframe's state, by replacing it with a newly constructed + // iframe. Unfortunately the location.reload API does not work in our case due to + // this Chrome error: + // "Blocked a frame with origin "outline://web_app" from accessing a cross-origin frame." + const iframe = document.createElement('iframe'); + iframe.onload = () => dialog.open(); + iframe.src = inviteUrl; + dialog.insertBefore(iframe, dialog.children[0]); + } + + closeGetConnectedDialog() { + const dialog = this.$.getConnectedDialog as PaperDialogElement; + dialog.close(); + const oldIframe = dialog.children[0]; + dialog.removeChild(oldIframe); + } + + showMetricsDialogForNewServer() { + (this.$.metricsDialog as OutlineMetricsOptionDialog).showMetricsOptInDialog(); + } + + /** @return A Promise which fulfills with the index of the button clicked. */ + showModalDialog(title: string, text: string, buttons: string[]): Promise { + return (this.$.modalDialog as OutlineModalDialog).open(title, text, buttons); + } + + closeModalDialog() { + return (this.$.modalDialog as OutlineModalDialog).close(); + } + + showLicensesTapped() { + const xhr = new XMLHttpRequest(); + xhr.onload = () => { + (this.$.licensesText as HTMLElement).innerText = xhr.responseText; + (this.$.licenses as PaperDialogElement).open(); + }; + xhr.onerror = () => { + console.error('could not load license.txt'); + }; + xhr.open('GET', '/ui_components/licenses/licenses.txt', true); + xhr.send(); + } + + _computeShouldShowSideBar() { + const drawerNarrow = (this.$.drawerLayout as AppDrawerLayoutElement).narrow; + const drawerOpened = (this.$.appDrawer as AppDrawerElement).opened; + if (drawerOpened && drawerNarrow) { + this.shouldShowSideBar = false; + } else { + this.shouldShowSideBar = drawerNarrow; + } + } + + _accountServerFilter(account: AccountListEntry) { + return (server: ServerListEntry) => account && server.accountId === account.id; + } + + _isServerManual(server: ServerListEntry) { + return !server.accountId; + } + + _sortServersByName(a: ServerListEntry, b: ServerListEntry) { + const aName = a.name.toUpperCase(); + const bName = b.name.toUpperCase(); + if (aName < bName) { + return -1; + } else if (aName > bName) { + return 1; + } + return 0; + } + + _computeServerClasses(selectedServerId: string, server: ServerListEntry) { + const serverClasses = []; + if (this._isServerSelected(selectedServerId, server)) { + serverClasses.push('selected'); + } + if (!server.isSynced) { + serverClasses.push('syncing'); + } + return serverClasses.join(' '); + } + + _computeServerImage(selectedServerId: string, server: ServerListEntry) { + if (this._isServerSelected(selectedServerId, server)) { + return 'server-icon-selected.png'; + } + return 'server-icon.png'; + } + + _getCloudId(accountId: string): DisplayCloudId { + // TODO: Replace separate account fields with a map. + if (this.gcpAccount && accountId === this.gcpAccount.id) { + return DisplayCloudId.GCP; + } else if (this.digitalOceanAccount && accountId === this.digitalOceanAccount.id) { + return DisplayCloudId.DO; + } + return null; + } + + _serverViewList(serverList: ServerListEntry[]): ServerViewListEntry[] { + return serverList.map((entry) => ({ + id: entry.id, + name: entry.name, + cloudId: this._getCloudId(entry.accountId), + })); + } + + _isServerSelected(selectedServerId: string, server: ServerListEntry) { + return !!selectedServerId && selectedServerId === server.id; + } + + _showServer(event: Event & {model: {server: ServerListEntry}}) { + const server = event.model.server; + this.fire('ShowServerRequested', {displayServerId: server.id}); + this.maybeCloseDrawer(); + } +} +customElements.define(AppRoot.is, AppRoot); diff --git a/src/server_manager/web_app/ui_components/cloud-assets.ts b/src/server_manager/web_app/ui_components/cloud-assets.ts new file mode 100644 index 000000000..e2eff6ae8 --- /dev/null +++ b/src/server_manager/web_app/ui_components/cloud-assets.ts @@ -0,0 +1,42 @@ +/* + Copyright 2021 The Outline Authors + + 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. +*/ + +export enum DisplayCloudId { + DO = 'do', + GCP = 'gcp', +} + +export function getCloudIcon(id: DisplayCloudId): string { + switch (id) { + case DisplayCloudId.DO: + return 'images/do_white_logo.svg'; + case DisplayCloudId.GCP: + return 'images/gcp-logo.svg'; + default: + return null; + } +} + +export function getCloudName(id: DisplayCloudId): string { + switch (id) { + case DisplayCloudId.DO: + return 'DigitalOcean'; + case DisplayCloudId.GCP: + return 'Google Cloud Platform'; + default: + return null; + } +} diff --git a/src/server_manager/web_app/ui_components/cloud-install-styles.ts b/src/server_manager/web_app/ui_components/cloud-install-styles.ts new file mode 100644 index 000000000..02143331a --- /dev/null +++ b/src/server_manager/web_app/ui_components/cloud-install-styles.ts @@ -0,0 +1,248 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ + +import {html} from '@polymer/polymer/lib/utils/html-tag'; +import {unsafeCSS} from 'lit-element'; + +// Polymer style module to share styles between steps +// https://polymer-library.polymer-project.org/3.0/docs/devguide/style-shadow-dom#share-styles-between-elements +const styleElement = document.createElement('dom-module'); +styleElement.appendChild(html` `); +styleElement.register('cloud-install-styles'); + +// Shared styles for LitElement components +const commonStyleCss = styleElement.querySelector('template').content.textContent; +export const COMMON_STYLES = unsafeCSS(commonStyleCss); diff --git a/src/server_manager/web_app/ui_components/licenses/README.md b/src/server_manager/web_app/ui_components/licenses/README.md new file mode 100644 index 000000000..cd789c0dd --- /dev/null +++ b/src/server_manager/web_app/ui_components/licenses/README.md @@ -0,0 +1,26 @@ +# HOWTO re-generate `license.txt` + +## Steps + +- `cd` to the root of your clone of this repo +- Ensure `node_modules` is up to date and only include dependencies of the Electron app by running `npm ci && npm run action server_manager/web_app/build` +- Copy `LICENSE` and the entire `node_modules` folder to `./src/server_manager/LICENSE` and `./src/server_manager/node_modules` +- `cd src/server_manager` +- `npx generate-license-file --input package.json --output web_app/ui_components/licenses/licenses.txt` +- `cd web_app/ui_components/licenses` +- `cat db-ip_license.txt >> licenses.txt` + +Done! (remember to delete the `./src/server_manager/node_modules` folder) + +> Note that the third step of copying `LICENSE` and `node_modules` is required because: +> +> - `generate-license-file` iterates all dependencies under `node_modules` folder in the current directory +> - `generate-license-file` will try to find a `LICENSE` file in the current directory, if not found, it will use `README.md` instead which is not what we want + +## Check + +To quickly look for non-compliant licenses: + +```bash +yarn licenses list --prod|grep -Ev \(@\|VendorUrl:\|VendorName:\|URL:\) +``` diff --git a/src/server_manager/web_app/ui_components/licenses/db-ip_license.txt b/src/server_manager/web_app/ui_components/licenses/db-ip_license.txt new file mode 100644 index 000000000..650f42ded --- /dev/null +++ b/src/server_manager/web_app/ui_components/licenses/db-ip_license.txt @@ -0,0 +1,5 @@ +------ + +IP Geolocation by DB-IP (https://db-ip.com) + +This database is licensed under a Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/src/server_manager/web_app/ui_components/licenses/licenses.txt b/src/server_manager/web_app/ui_components/licenses/licenses.txt new file mode 100644 index 000000000..ceead765d --- /dev/null +++ b/src/server_manager/web_app/ui_components/licenses/licenses.txt @@ -0,0 +1,11196 @@ +The following NPM packages may be included in this product: + + - @formatjs/intl-unified-numberformat@3.3.7 + - @formatjs/intl-utils@2.3.0 + +These packages each contain the following license and notice below: + +MIT License + +Copyright (c) 2019 FormatJS + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - @polymer/app-layout@3.1.0 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/app-layout.svg)](https://www.npmjs.com/package/@polymer/app-layout) +[![Build status](https://travis-ci.org/PolymerElements/app-layout.svg?branch=master)](https://travis-ci.org/PolymerElements/app-layout) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/app-layout) + +## App Layout + + + +A collection of elements, along with guidelines and templates that can be used to structure your app’s layout. + +## What is inside + +### Elements + +- [app-box](https://github.com/PolymerElements/app-layout/tree/master/app-box) - A container element that can have scroll effects - visual effects based on scroll position. + +- [app-drawer](https://github.com/PolymerElements/app-layout/tree/master/app-drawer) - A navigation drawer that can slide in from the left or right. + +- [app-drawer-layout](https://github.com/PolymerElements/app-layout/tree/master/app-drawer-layout) - A wrapper element that positions an app-drawer and other content. + +- [app-grid](https://github.com/PolymerElements/app-layout/tree/master/app-grid) - A helper class useful for creating responsive, fluid grid layouts using custom properties. + +- [app-header](https://github.com/PolymerElements/app-layout/tree/master/app-header) - A container element for app-toolbars at the top of the screen that can have scroll effects - visual effects based on scroll position. + +- [app-header-layout](https://github.com/PolymerElements/app-layout/tree/master/app-header-layout) - A wrapper element that positions an app-header and other content. + +- [app-toolbar](https://github.com/PolymerElements/app-layout/tree/master/app-toolbar) - A horizontal toolbar containing items that can be used for label, navigation, search and actions. + +### Templates + +The templates are a means to define, illustrate and share best practices in App Layout. Pick a template and customize it: + +- **Getting started** +([Demo](https://polymerelements.github.io/app-layout/templates/getting-started) - [Source](/templates/getting-started)) + +- **Landing page** +([Demo](https://polymerelements.github.io/app-layout/templates/landing-page) - [Source](/templates/landing-page)) + +- **Publishing: Zuperkülblog** +([Demo](https://polymerelements.github.io/app-layout/templates/publishing) - [Source](/templates/publishing)) + +- **Shop: Shrine** +([Demo](https://polymerelements.github.io/app-layout/templates/shrine) - [Source](/templates/shrine)) + +- **Blog: Pesto** +([Demo](https://polymerelements.github.io/app-layout/templates/pesto) - [Source](/templates/pesto)) + +- **Scroll effects: Test drive** +([Demo](https://polymerelements.github.io/app-layout/templates/test-drive) - [Source](/templates/test-drive)) + +### Patterns + +Sample code for various UI patterns: + +- **Transform navigation:** +As more screen space is available, side navigation can transform into tabs. +([Demo](https://www.webcomponents.org/element/PolymerElements/app-layout/demo/patterns/transform-navigation/index.html) - [Source](/patterns/transform-navigation/x-app.html)) + +- **Expand Card:** +Content cards may expand to take up more horizontal space. +([Demo](https://www.webcomponents.org/element/PolymerElements/app-layout/demo/patterns/expand-card/index.html) - [Source](/patterns/expand-card/index.html)) + +- **Material Design Responsive Toolbar:** +Toolbar changes its height and padding to adapt mobile screen size. +([Demo](https://www.webcomponents.org/element/PolymerElements/app-layout/demo/patterns/md-responsive-toolbar/index.html) - [Source](/patterns/md-responsive-toolbar/index.html)) + +## Users + +Here are some web apps built with App Layout: + +- [Youtube Web](https://www.youtube.com/new) +- [Google I/O 2016](https://events.google.com/io2016/) +- [Polymer project site](https://www.polymer-project.org/summit) +- [Polymer summit](https://www.polymer-project.org/summit) +- [Shop](https://shop.polymer-project.org) +- [News](https://news.polymer-project.org) +- [webcomponents.org](https://www.webcomponents.org/) +- [Chrome Status](https://www.chromestatus.com/) +- [Project Fi](https://fi.google.com/about/) +- [NASA Open Source Software](https://code.nasa.gov/) + +See: [Documentation](https://www.webcomponents.org/element/@polymer/app-layout), + [Demo](https://www.webcomponents.org/element/@polymer/app-layout/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/app-layout +``` + +### In an html file +```html + + + + + + + +
My app
+
+
+ + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/app-layout/app-layout.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + +
My app
+
+
+ + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/app-layout +cd app-layout +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/app-localize-behavior@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/app-localize-behavior.svg)](https://www.npmjs.com/package/@polymer/app-localize-behavior) +[![Build status](https://travis-ci.org/PolymerElements/app-localize-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/app-localize-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/app-localize-behavior) + +## <app-localize-behavior> +`app-localize-behavior` is a behavior that wraps the [format.js](http://formatjs.io/) library to +help you internationalize your application. Note that if you're on a browser that +does not natively support the [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) +object, you must load the polyfill yourself. An example polyfill can +be found [here](https://github.com/andyearnshaw/Intl.js/). + +See: [Documentation](https://www.webcomponents.org/element/@polymer/app-localize-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/app-localize-behavior/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/app-localize-behavior +``` + +### In an html file using the localized element +```html + + + + + + + + + + + + + + +``` + +### Localizing a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {AppLocalizeBehavior} from '@polymer/app-localize-behavior/app-localize-behavior.js'; + +class SampleElement extends mixinBehaviors([AppLocalizeBehavior], PolymerElement) { + static get template() { + return html` +
{{localize('hello', 'name', 'Batman')}}
+ `; + } + + static get properties() { + return { + language: { value: 'en' }, + } + } + + function attached() { + this.loadResources(this.resolveUrl('locales.json')); + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/app-localize-behavior +cd app-localize-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/font-roboto@3.0.2 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/font-roboto.svg)](https://www.npmjs.com/package/@polymer/paper-input) +[![Build status](https://travis-ci.org/PolymerElements/font-roboto.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-input) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/font-roboto) + +## font-roboto +`font-roboto` loads the Roboto family of fonts from Google Fonts. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/font-roboto). + +## Usage + +### Installation +``` +npm install --save @polymer/font-roboto +``` + +### In an html file +```html + + + + + + +

This text is in Roboto.

+ + +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/font-roboto/roboto.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + +

This text is in Roboto.

+ `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/font-roboto +cd font-roboto +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-a11y-announcer@3.2.0 + +This package contains the following license and notice below: + + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-a11y-announcer.svg)](https://www.npmjs.com/package/@polymer/iron-a11y-announcer) +[![Build status](https://travis-ci.org/PolymerElements/iron-a11y-announcer.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-a11y-announcer) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-a11y-announcer) + +## <iron-a11y-announcer> + +`iron-a11y-announcer` is a singleton element that is intended to add a11y +to features that require on-demand announcement from screen readers. In +order to make use of the announcer, it is best to request its availability +in the announcing element. +Note: announcements are only audible if you have a screen reader enabled. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-a11y-announcer), + [Demo](https://www.webcomponents.org/element/@polymer/iron-a11y-announcer/demo/demo/index.html) + +## Usage + +### Installation +``` +npm install --save @polymer/iron-a11y-announcer +``` + +### In an html file +```html + + + + + +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + `; + } + function attached() { + IronA11yAnnouncer.requestAvailability(); + } + + // After the `iron-a11y-announcer` has been made available, elements can + // make announces by firing bubbling `iron-announce` events. + // Note: announcements are only audible if you have a screen reader enabled. + function announce() { + IronA11yAnnouncer.instance.fire('iron-announce', + {text: 'Hello there!'}, {bubbles: true}); + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-a11y-announcer +cd iron-a11y-announcer +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +## Known Issues +This element doesn't work on Firefox (it doesn't read anything in Voice Over), since +`aria-live` has been broken since the Quantum redesign (see the [MDN docs demo](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions)) +-- we tested it on Firefox 60, but it doesn't look like a regression, so +it's probably broken on older versions as well. + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-a11y-keys-behavior@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-a11y-keys-behavior.svg)](https://www.npmjs.com/package/@polymer/iron-a11y-keys-behavior) +[![Build status](https://travis-ci.org/PolymerElements/iron-a11y-keys-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-a11y-keys-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-a11y-keys-behavior) + +## <iron-a11y-keys-behavior> +`Polymer.IronA11yKeysBehavior` provides a normalized interface for processing +keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding). +The element takes care of browser differences with respect to Keyboard events +and uses an expressive syntax to filter key presses. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-a11y-keys-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/iron-a11y-keys-behavior/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-a11y-keys-behavior +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js'; + +class SampleElement extends extends mixinBehaviors([IronA11yKeysBehavior], PolymerElement) { + static get template() { + return html` +
[[pressed]]
+ `; + } + + static get properties() { + return { + pressed: {type: String, readOnly: true, value: ''}, + keyBindings: { + 'space': '_onKeydown', // same as 'space:keydown' + 'shift+tab': '_onKeydown', + 'enter:keypress': '_onKeypress', + 'esc:keyup': '_onKeyup' + } + } + } + + function _onKeydown: function(event) { + console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab" + console.log(event.detail.key); // KEY only, e.g. "tab" + console.log(event.detail.event); // EVENT, e.g. "keydown" + console.log(event.detail.keyboardEvent); // the original KeyboardEvent + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-a11y-keys-behavior +cd iron-a11y-keys-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-ajax@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-ajax.svg)](https://www.npmjs.com/package/@polymer/iron-ajax) +[![Build status](https://travis-ci.org/PolymerElements/iron-ajax.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-ajax) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-ajax) + +## <iron-ajax> + +The `iron-ajax` element declaratively exposes network request functionality to +Polymer's data-binding system. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-ajax), + [Demo](https://www.webcomponents.org/element/@polymer/iron-ajax/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-ajax +``` + +### In an html file +```html + + + + + + + + + +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/iron-ajax/iron-ajax.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + + `; + } +} +customElements.define('sample-element', SampleElement); +``` +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-ajax +cd iron-ajax +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-autogrow-textarea@3.0.3 + +This package contains the following license and notice below: + + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-autogrow-textarea.svg)](https://www.npmjs.com/package/@polymer/iron-autogrow-textarea) +[![Build status](https://travis-ci.org/PolymerElements/iron-autogrow-textarea.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-autogrow-textarea) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-autogrow-textarea) + + +## <iron-autogrow-textarea> + +`iron-autogrow-textarea` is an element containing a textarea that grows in height as more +lines of input are entered. Unless an explicit height or the `maxRows` property is set, it will +never scroll. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-autogrow-textarea), + [Demo](https://www.webcomponents.org/element/@polymer/iron-autogrow-textarea/demo/demo/index.html). + + + ## Usage + + ### Installation + ``` + npm install --save @polymer/iron-autogrow-textarea + ``` + + ### In an html file + ```html + + + + + + + + + ``` + ### In a Polymer 3 element + ```js + import {PolymerElement, html} from '@polymer/polymer'; + import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; + + class SampleElement extends PolymerElement { + static get template() { + return html` + + `; + } + } + customElements.define('sample-element', SampleElement); + ``` + + ## Contributing + If you want to send a PR to this element, here are + the instructions for running the tests and demo locally: + + ### Installation + ```sh + git clone https://github.com/PolymerElements/iron-autogrow-textarea + cd iron-autogrow-textarea + npm install + npm install -g polymer-cli + ``` + + ### Running the demo locally + ```sh + polymer serve --npm + open http://127.0.0.1:/demo/ + ``` + + ### Running the tests + ```sh + polymer test --npm + ``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-behaviors@3.0.1 + +This package contains the following license and notice below: + + + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-behaviors.svg)](https://www.npmjs.com/package/@polymer/iron-behaviors) +[![Build status](https://travis-ci.org/PolymerElements/iron-behaviors.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-behaviors) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-behaviors) + +## <iron-behaviors> +`` provides a set of behaviors for the iron elements. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-behaviors), + [Demo](https://www.webcomponents.org/element/@polymer/iron-behaviors/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-behaviors +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {IronButtonState} from '../iron-button-state.js'; +import {IronControlState} from '../iron-control-state.js'; + +class SampleElement extends mixinBehaviors([IronButtonState, IronControlState], PolymerElement) { + static get template() { + return html` + + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-behaviors +cd iron-behaviors +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-checked-element-behavior@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-checked-element-behavior.svg)](https://www.npmjs.com/package/@polymer/iron-checked-element-behavior) +[![Build status](https://travis-ci.org/PolymerElements/iron-checked-element-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-checked-element-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-checked-element-behavior) + +## IronCheckedElementBehavior + +Use `IronCheckedElementBehavior` to implement a custom element that has a +`checked` property, which can be used for validation if the element is also +`required`. Element instances implementing this behavior will also be +registered for use in an `iron-form` element. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-checked-element-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/iron-checked-element-behavior/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-checked-element-behavior +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import {IronCheckedElementBehavior} from '../iron-checked-element-behavior.js'; + +class SimpleCheckbox extends mixinBehaviors(IronCheckedElementBehavior, PolymerElement) { + static get template() { + return html` + + + + + {{label}} + `; + } + + static get properties() { + return {label: {type: String, value: 'not validated'}}; + } + + _checkValidity() { + this.validate(); + this.label = this.invalid ? 'is invalid' : 'is valid'; + } +} + +customElements.define('simple-checkbox', SimpleCheckbox); +``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-checked-element-behavior +cd iron-checked-element-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-collapse@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-collapse.svg)](https://www.npmjs.com/package/@polymer/iron-collapse) +[![Build status](https://travis-ci.org/PolymerElements/iron-collapse.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-collapse) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-collapse) + +## <iron-collapse> +`iron-collapse` creates a collapsible block of content. By default, the content +will be collapsed. Use `opened` or `toggle()` to show/hide the content. The +aria-expanded attribute should only be set on the button that controls the +collapsable area, not on the area itself. See +https://www.w3.org/WAI/GL/wiki/Using_aria-expanded_to_indicate_the_state_of_a_collapsible_element#Description + +`iron-collapse` adjusts the max-height/max-width of the collapsible element to show/hide +the content. So avoid putting padding/margin/border on the collapsible directly, +and instead put a div inside and style that. + +```html + + + +
+
Content goes here...
+
+
+``` + +### Styling + +The following custom properties and mixins are available for styling: + +| Custom property | Description | Default | +| --- | --- | --- | +| `--iron-collapse-transition-duration` | Animation transition duration | `300ms` | + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-collapse), + [Demo](https://www.webcomponents.org/element/@polymer/iron-collapse/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-collapse +``` + +### In an html file +```html + + + + + + +
Content goes here...
+
+ + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/iron-collapse/iron-collapse.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + +
Content goes here...
+
+ `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-collapse +cd iron-collapse +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-dropdown@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-dropdown.svg)](https://www.npmjs.com/package/@polymer/iron-dropdown) +[![Build status](https://travis-ci.org/PolymerElements/iron-dropdown.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-dropdown) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-dropdown) + +## <iron-dropdown> + +`` displays content inside a fixed-position container, +positioned relative to another element. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-dropdown), + [Demo](https://www.webcomponents.org/element/@polymer/iron-dropdown/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-dropdown +``` + +### In an HTML file + +```html + + + + + + +
+ + +
Hello!
+
+
+ + +``` + +### In a Polymer 3 element + +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/iron-dropdown/iron-dropdown.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + + +
Hello!
+
+ `; + } + + _openDropdown() { + this.$.dropdown.open(); + } +} +customElements.define('sample-element', SampleElement); +``` + +In the above example, the `
` assigned to the `dropdown-content` slot will +be hidden until the dropdown element has `opened` set to true, or when the +`open` method is called on the element. + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-dropdown +cd iron-dropdown +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-fit-behavior@3.1.0 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-fit-behavior.svg)](https://www.npmjs.com/package/@polymer/iron-fit-behavior) +[![Build status](https://travis-ci.org/PolymerElements/iron-fit-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-fit-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-fit-behavior) + +## IronFitBehavior + +`IronFitBehavior` positions and fits an element in the bounds of another +element and optionally centers it in the window or the other element. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-fit-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/iron-fit-behavior/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-fit-behavior +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior.js'; + +class SimpleFit extends mixinBehaviors(IronFitBehavior, PolymerElement) { + static get template() { + return html` + + verticalAlign: [[verticalAlign]], horizontalAlign: [[horizontalAlign]] + `; + } +} + +customElements.define('simple-fit', SimpleFit); +``` + +Then, in your HTML: + +```html + + + + +
+ The <simple-fit> below will be positioned within this div. + +
+``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-fit-behavior +cd iron-fit-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-flex-layout@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-flex-layout.svg)](https://www.npmjs.com/package/@polymer/iron-flex-layout) +[![Build status](https://travis-ci.org/PolymerElements/iron-flex-layout.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-flex-layout) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-flex-layout) + +## <iron-flex-layout> +The `` component provides simple ways to use +[CSS flexible box layout](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Flexible_boxes), +also known as flexbox. Note that this is an old element, that was written +before all modern browsers had non-prefixed flex styles. As such, nowadays you +don't really need to use this element anymore, and can use CSS flex styles +directly in your code. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-flex-layout), + [Demo](https://www.webcomponents.org/element/@polymer/iron-flex-layout/demo/demo/index.html). + +This component provides two different ways to use flexbox: + +1. [Layout classes](https://github.com/PolymerElements/iron-flex-layout/tree/master/iron-flex-layout-classes.html). +The layout class stylesheet provides a simple set of class-based flexbox rules, that +let you specify layout properties directly in markup. You must include this file +in every element that needs to use them. + +1. [Custom CSS mixins](https://github.com/PolymerElements/iron-flex-layout/blob/master/iron-flex-layout.html). +The mixin stylesheet includes custom CSS mixins that can be applied inside a CSS rule using the `@apply` function. + +## Usage + +### Installation +``` +npm install --save @polymer/iron-flex-layout +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/iron-flex-layout/iron-flex-layout-classes.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + +
+
horizontal layout center alignment
+
+ `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-flex-layout +cd iron-flex-layout +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-form-element-behavior@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-form-element-behavior.svg)](https://www.npmjs.com/package/@polymer/iron-form-element-behavior) +[![Build status](https://travis-ci.org/PolymerElements/iron-form-element-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-form-element-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-form-element-behavior) + +## IronFormElementBehavior +`IronFormElementBehavior` adds a `name`, `value` and `required` properties to +a custom element. This element is deprecated, and only exists for back compatibility +with Polymer 1.x (where `iron-form` was a type extension), and +it is not something you want to use. No contributions or fixes will be accepted. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-form-element-behavior). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-form-element-behavior +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {IronFormElementBehavior} from '@polymer/iron-form-element-behavior/iron-form-element-behavior.js'; + +class SampleElement extends mixinBehaviors([IronFormElementBehavior], PolymerElement) { + static get template() { + return html` + + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-icon@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-icon.svg)](https://www.npmjs.com/package/@polymer/iron-icon) +[![Build status](https://travis-ci.org/PolymerElements/iron-icon.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-icon) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-icon) + +## <iron-icon> + +The `iron-icon` element displays an icon. By default an icon renders as a 24px +square. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-icon), + [Demo](https://www.webcomponents.org/element/@polymer/iron-icon/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-icon +``` + +### In an HTML file + +```html + + + + + + + + + + + + +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import '@polymer/iron-icon/iron-icon.js'; + +class ExampleElement extends PolymerElement { + static get template() { + return html` + + `; + } +} + +customElements.define('example-element', ExampleElement); +``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-icon +cd iron-icon +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-icons@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-icons.svg)](https://www.npmjs.com/package/@polymer/iron-icons) +[![Build status](https://travis-ci.org/PolymerElements/iron-icons.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-icons) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-icons) + +## <iron-icons> + +`iron-icons` is a utility import that includes the definition for the +`iron-icon` element, `iron-iconset-svg` element, as well as an import for the +default icon set. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-icons), + [Demo](https://www.webcomponents.org/element/@polymer/iron-icons/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-icons +``` + +### In an HTML file + +```html + + + + + + + + +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import '@polymer/iron-icon/iron-icon.js'; +import '@polymer/iron-icons/iron-icons.js'; + +class ExampleElement extends PolymerElement { + static get template() { + return html` + + `; + } +} + +customElements.define('example-element', ExampleElement); +``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-icons +cd iron-icons +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-iconset-svg@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-iconset-svg.svg)](https://www.npmjs.com/package/@polymer/iron-iconset-svg) +[![Build status](https://travis-ci.org/PolymerElements/iron-iconset-svg.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-iconset-svg) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-iconset-svg) + +## <iron-iconset-svg> + +The `iron-iconset-svg` element allows users to define their own icon sets that +contain svg icons. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-iconset-svg), + [Demo](https://www.webcomponents.org/element/@polymer/iron-iconset-svg/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-iconset-svg +``` + +### In an HTML file + +```html + + + + + + + + + + + + + + + + + + + +``` + +### In a Polymer 3 element + +You can use an `` anywhere you could put a custom element, +such as in the shadow root of another component to expose icons to it. However, +if you're going to be creating many instances of the containing component, you +should move your `` out to a separate module. This prevents a +redundant `` from being added to the shadow root of each +instance of that component. See the demo (and specifically +`demo/svg-sample-icons.js`) for an example. + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import '@polymer/iron-iconset-svg/iron-iconset-svg.js'; +import '@polymer/iron-icon/iron-icon.js'; + +class ExampleElement extends PolymerElement { + static get template() { + return html` + + + + + + + + + + + + + `; + } +} + +customElements.define('example-element', ExampleElement); +``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-iconset-svg +cd iron-iconset-svg +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-input@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-input.svg)](https://www.npmjs.com/package/@polymer/iron-input) +[![Build status](https://travis-ci.org/PolymerElements/iron-input.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-input) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-input) + +## <iron-input> +`` adds two-way binding and custom validators using `Polymer.IronValidatorBehavior` +to ``. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-input), + [Demo](https://www.webcomponents.org/element/@polymer/iron-input/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-input +``` + +### In an html file +```html + + + + + + + + + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/iron-input/iron-input.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +### Two-way binding + +By default you can only get notified of changes to an `input`'s `value` due to user input: + +```html + +``` + +`iron-input` adds the `bind-value` property that mirrors the `value` property, and can be used +for two-way data binding. `bind-value` will notify if it is changed either by user input or by script. + +```html + + + +``` + +### Custom validators + +You can use custom validators that implement `Polymer.IronValidatorBehavior` with ``. + +```html + + + +``` + +### Stopping invalid input + +It may be desirable to only allow users to enter certain characters. You can use the +`prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature +is separate from validation, and `allowed-pattern` does not affect how the input is validated. + +```html + + + + +``` + + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-input +cd iron-input +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-media-query@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-media-query.svg)](https://www.npmjs.com/package/@polymer/iron-media-query) +[![Build status](https://travis-ci.org/PolymerElements/iron-media-query.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-media-query) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-media-query) + +## <iron-media-query> +`iron-media-query` can be used to data bind to a CSS media query. +The `query` property is a bare CSS media query. +The `query-matches` property is a boolean representing whether the page matches that media query. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-media-query), + [Demo](https://www.webcomponents.org/element/@polymer/iron-media-query/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-media-query +``` + +### In an html file +```html + + + + + + + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/iron-media-query/iron-media-query.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-media-query +cd iron-media-query +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-menu-behavior@3.0.2 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-menu-behavior.svg)](https://www.npmjs.com/package/@polymer/iron-menu-behavior) +[![Build status](https://travis-ci.org/PolymerElements/iron-menu-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-menu-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-menu-behavior) + +## `IronMenuBehavior`, `IronMenubarBehavior` + +`IronMenuBehavior` and `IronMenubarBehavior` implement accessible menu and +menubar behaviors. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-menu-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/iron-menu-behavior/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-menu-behavior +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import {IronMenuBehavior} from '@polymer/iron-menu-behavior/iron-menu-behavior.js'; + +class SimpleMenu extends mixinBehaviors(IronMenuBehavior, PolymerElement) { + static get template() { + return html` + + + + `; + } +} + +customElements.define('simple-menu', SimpleMenu); +``` + +Then, in your HTML: + +```html + + + + + +
Item 0
+
Item 1
+
Item 2 (disabled)
+
+``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-menu-behavior +cd iron-menu-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-meta@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-meta.svg)](https://www.npmjs.com/package/@polymer/iron-meta) +[![Build status](https://travis-ci.org/PolymerElements/iron-meta.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-meta) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-meta) + +## <iron-meta> + +`iron-meta` is a generic element you can use for sharing information across the +DOM tree. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-meta), + [Demo](https://www.webcomponents.org/element/@polymer/iron-meta/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-meta +``` + +### In an HTML file + +```html + + + + + + + + + +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import '@polymer/iron-meta/iron-meta.js'; + +class ExampleElement extends PolymerElement { + static get properties() { + return { + prop: String, + }; + } + + static get template() { + return html` + + info: [[prop]] + `; + } +} + +customElements.define('example-element', ExampleElement); +``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-meta +cd iron-meta +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-overlay-behavior@3.0.3 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-overlay-behavior.svg)](https://www.npmjs.com/package/@polymer/iron-overlay-behavior) +[![Build status](https://travis-ci.org/PolymerElements/iron-overlay-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-overlay-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-overlay-behavior) + +## IronOverlayBehavior + +Use `IronOverlayBehavior` to implement an element that can be hidden or shown, and displays +on top of other content. It includes an optional backdrop, and can be used to implement a variety +of UI controls including dialogs and drop downs. Multiple overlays may be displayed at once. + +See the [demo source code](https://github.com/PolymerElements/iron-overlay-behavior/blob/master/demo/simple-overlay.js) +for an example. + +### Closing and canceling + +An overlay may be hidden by closing or canceling. The difference between close and cancel is user +intent. Closing generally implies that the user acknowledged the content on the overlay. By default, +it will cancel whenever the user taps outside it or presses the escape key. This behavior is +configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click` properties. +`close()` should be called explicitly by the implementer when the user interacts with a control +in the overlay element. When the dialog is canceled, the overlay fires an 'iron-overlay-canceled' +event. Call `preventDefault` on this event to prevent the overlay from closing. + +### Positioning + +By default the element is sized and positioned to fit and centered inside the window. You can +position and size it manually using CSS. See `Polymer.IronFitBehavior`. + +### Backdrop + +Set the `with-backdrop` attribute to display a backdrop behind the overlay. The backdrop is +appended to `` and is of type ``. See its doc page for styling +options. + +In addition, `with-backdrop` will wrap the focus within the content in the light DOM. +Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_focusableNodes) +to achieve a different behavior. + +### Limitations + +The element is styled to appear on top of other content by setting its `z-index` property. You +must ensure no element has a stacking context with a higher `z-index` than its parent stacking +context. You should place this element as a child of `` whenever possible. + +## <iron-overlay-backdrop> + +`iron-overlay-backdrop` is a backdrop used by `Polymer.IronOverlayBehavior`. It should be a +singleton. + +### Styling + +The following custom properties and mixins are available for styling. + +| Custom property | Description | Default | +| --- | --- | --- | +| `--iron-overlay-backdrop-background-color` | Backdrop background color | #000 | +| `--iron-overlay-backdrop-opacity` | Backdrop opacity | 0.6 | +| `--iron-overlay-backdrop` | Mixin applied to `iron-overlay-backdrop`. | {} | +| `--iron-overlay-backdrop-opened` | Mixin applied to `iron-overlay-backdrop` when it is displayed | {} | + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-overlay-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/iron-overlay-behavior/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-overlay-behavior +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js'; + +class SampleElement extends mixinBehaviors(IronOverlayBehavior, PolymerElement) { + static get template() { + return html` + +

Overlay Content

+ `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-overlay-behavior +cd iron-overlay-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-pages@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-pages.svg)](https://www.npmjs.com/package/@polymer/iron-pages) +[![Build status](https://travis-ci.org/PolymerElements/iron-pages.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-pages) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-pages) + +## <iron-pages> + +`iron-pages` is used to select one of its children to show. One use is to cycle +through a list of children "pages". + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-pages), + [Demo](https://www.webcomponents.org/element/@polymer/iron-pages/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-pages +``` + +### In an HTML file + +```html + + + + + + +
Page 0
+
Page 1
+
Page 2
+
Page 3
+
+ + +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import '@polymer/iron-pages/iron-pages.js'; + +class ExampleElement extends PolymerElement { + static get template() { + return html` + +
Page 0
+
Page 1
+
Page 2
+
Page 3
+
+ `; + } +} + +customElements.define('example-element', ExampleElement); +``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-pages +cd iron-pages +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-range-behavior@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-range-behavior.svg)](https://www.npmjs.com/package/@polymer/iron-range-behavior) +[![Build status](https://travis-ci.org/PolymerElements/iron-range-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-range-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-range-behavior) + +## `IronRangeBehavior` + +`IronRangeBehavior` provides the behavior for something with a minimum to +maximum range. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-range-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/iron-range-behavior/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-range-behavior +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import {IronRangeBehavior} from '@polymer/iron-range-behavior/iron-range-behavior.js'; + +class SimpleRange extends mixinBehaviors(IronRangeBehavior, PolymerElement) { + static get template() { + return html` + + + [[ratio]]% +
+ `; + } +} + +customElements.define('simple-range', SimpleRange); +``` + +Then, in your HTML: + +```html + +``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-range-behavior +cd iron-range-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-resizable-behavior@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-resizable-behavior.svg)](https://www.npmjs.com/package/@polymer/iron-resizable-behavior) +[![Build status](https://travis-ci.org/PolymerElements/iron-resizable-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-resizable-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-resizable-behavior) + +## IronResizableBehavior + +`IronResizableBehavior` is a behavior that can be used in Polymer elements to +coordinate the flow of resize events between "resizers" (elements that control the +size or hidden state of their children) and "resizables" (elements that need to be +notified when they are resized or un-hidden by their parents in order to take +action on their new measurements). + +Elements that perform measurement should add the `IronResizableBehavior` behavior to +their element definition and listen for the `iron-resize` event on themselves. +This event will be fired when they become showing after having been hidden, +when they are resized explicitly by another resizable, or when the window has been +resized. + +Note, the `iron-resize` event is non-bubbling. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-resizable-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/iron-resizable-behavior/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-resizable-behavior +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {IronResizableBehavior} from '@polymer/iron-resizable-behavior/iron-resizable-behavior.js'; + +class SampleElement extends mixinBehaviors([IronResizableBehavior], PolymerElement) { + static get template() { + return html` + + width: [[width]] + height: [[height]] + `; + } + + static get properties() { + return { + width: Number, + height: Number, + } + } + + connectedCallback() { + super.connectedCallback(); + this.addEventListener('iron-resize', this.onIronResize.bind(this)); + } + + onIronResize() { + this.width = this.offsetWidth; + this.height = this.offsetHeight; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-resizable-behavior +cd iron-resizable-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-scroll-target-behavior@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-scroll-target-behavior.svg)](https://www.npmjs.com/package/@polymer/iron-scroll-target-behavior) +[![Build status](https://travis-ci.org/PolymerElements/iron-scroll-target-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-scroll-target-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-scroll-target-behavior) + +## IronScrollTargetBehavior + +`IronScrollTargetBehavior` allows an element to respond to scroll events from a +designated scroll target. + +Elements that consume this behavior can override the `_scrollHandler` +method to add logic on the scroll event. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-scroll-target-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/iron-scroll-target-behavior/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-scroll-target-behavior +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {IronScrollTargetBehavior} from '@polymer/iron-scroll-target-behavior/iron-scroll-target-behavior.js'; + +class SampleElement extends mixinBehaviors(IronScrollTargetBehavior, PolymerElement) { + static get template() { + return html` +

Scrollable content here

+ `; + } + + _scrollHandler() { + console.log('_scrollHandler', this._scrollTop, this._scrollLeft); + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-scroll-target-behavior +cd iron-scroll-target-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-selector@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-selector.svg)](https://www.npmjs.com/package/@polymer/iron-selector) +[![Build status](https://travis-ci.org/PolymerElements/iron-selector.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-selector) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-selector) + +## <iron-selector>, `IronSelectableBehavior`, `IronMultiSelectableBehavior` + +`iron-selector` is an element which can be used to manage a list of elements +that can be selected. Tapping on the item will make the item selected. The +`selected` indicates which item is being selected. The default is to use the +index of the item. `iron-selector`'s functionality is entirely defined by +`IronMultiSelectableBehavior`. + +`IronSelectableBehavior` gives an element the concept of a selected child +element. By default, the element will select one of its selectable children +when a ['tap' +event](https://www.polymer-project.org/3.0/docs/devguide/gesture-events#gesture-event-types) +(synthesized by Polymer, roughly 'click') is dispatched to it. + +`IronSelectableBehavior` lets you ... + + - decide which children should be considered selectable (`selectable`), + - retrieve the currently selected element (`selectedItem`) and all elements + in the selectable set (`items`), + - change the selection (`select`, `selectNext`, etc.), + - decide how selected elements are modified to indicate their selected state + (`selectedClass`, `selectedAttribute`), + +... among other things. + +`IronMultiSelectableBehavior` includes all the features of +`IronSelectableBehavior` as well as a `multi` property, which can be set to +`true` to indicate that the element can have multiple selected child elements. +It also includes the `selectedItems` and `selectedValues` properties for +working with arrays of selectable elements and their corresponding values +(`multi` is `true`) - similar to the single-item versions provided by +`IronSelectableBehavior`: `selectedItem` and `selected`. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-selector), + [Demo](https://www.webcomponents.org/element/@polymer/iron-selector/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/iron-selector +``` + +### In an HTML file + +```html + + + + + + +
Item 1
+
Item 2
+
Item 3
+
+ + +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import '@polymer/iron-selector/iron-selector.js'; + +class ExampleElement extends PolymerElement { + static get template() { + return html` + +
Item 1
+
Item 2
+
Item 3
+
+ `; + } +} + +customElements.define('example-element', ExampleElement); +``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/iron-selector +cd iron-selector +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/iron-validatable-behavior@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/iron-validatable-behavior.svg)](https://www.npmjs.com/package/@polymer/iron-validatable-behavior) +[![Build status](https://travis-ci.org/PolymerElements/iron-validatable-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-validatable-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/iron-validatable-behavior) + +## IronValidatableBehavior +Use `IronValidatableBehavior` to implement an element that validates user input. By using this behaviour, your custom element will get a public `validate()` method, which +will return the validity of the element, and a corresponding `invalid` attribute, +which can be used for styling. Can be used alongside an element implementing +the `IronValidatableBehavior` behaviour. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/iron-validatable-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/iron-validatable-behavior/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/iron-validatable-behavior +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {IronValidatableBehavior} from '@polymer/iron-validatable-behavior/iron-validatable-behavior.js'; + +class SampleElement extends mixinBehaviors([IronValidatableBehavior], PolymerElement) { + static get template() { + return html` + + + `; + + // Override this method if you want to implement custom validity + // for your element. This element is only valid if the value in the + // input is "cat". + function _getValidity() { + return this.$.input.value === 'cat'; + } + } +} +customElements.define('sample-element', SampleElement); +``` + +### In an html file using the element +```html + + + + + + + + + +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/iron-validatable-behavior +cd iron-validatable-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/neon-animation@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/neon-animation.svg)](https://www.npmjs.com/package/@polymer/neon-animation) +[![Build status](https://travis-ci.org/PolymerElements/neon-animation.svg?branch=master)](https://travis-ci.org/PolymerElements/neon-animation) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/neon-animation) + +## neon-animation + +`neon-animation` is a suite of elements and behaviors to implement pluggable animated transitions for Polymer Elements using [Web Animations](https://w3c.github.io/web-animations/). Please note that these elements do not include the web-animations polyfill. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/neon-animation), + [Demo](https://www.webcomponents.org/element/@polymer/neon-animation/demo/demo/index.html). + +_See [the guide](./guide.md) for detailed usage._ + +## Usage + +### Installation + +Element: +``` +npm install --save @polymer/neon-animation +``` + +Polyfill: +``` +npm install --save web-animations-js +``` + +### In an HTML file + +### `NeonAnimatableBehavior` +Elements that can be animated by `NeonAnimationRunnerBehavior` should implement the `NeonAnimatableBehavior` behavior, or `NeonAnimationRunnerBehavior` if they're also responsible for running an animation. + +#### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {NeonAnimatableBehavior} from '@polymer/neon-animation/neon-animatable-behavior.js'; + +class SampleElement extends mixinBehaviors([NeonAnimatableBehavior], PolymerElement) { + static get template() { + return html` + + + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +### `NeonAnimationRunnerBehavior` + +#### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {NeonAnimationRunnerBehavior} from '@polymer/neon-animation/neon-animation-runner-behavior.js'; +import '@polymer/neon-animation/animations/scale-down-animation.js'; + +class SampleElement extends mixinBehaviors([NeonAnimationRunnerBehavior], PolymerElement) { + static get template() { + return html` +
this entire element will be animated after one second
+ `; + } + + connectedCallback() { + super.connectedCallback(); + + // must be set here because properties is static and cannot reference "this" + this.animationConfig = { + // provided by neon-animation/animations/scale-down-animation.js + name: 'scale-down-animation', + node: this, + }; + + setTimeout(() => this.playAnimation(), 1000); + } +} +customElements.define('sample-element', SampleElement); +``` + +### `` +A simple element that implements NeonAnimatableBehavior. + +#### In an html file +```html + + + + + +
this entire element and its parent will be animated after one second
+
+ + + + +``` + +#### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {NeonAnimationRunnerBehavior} from '@polymer/neon-animation/neon-animation-runner-behavior.js'; +import '@polymer/neon-animation/neon-animatable.js'; +import '@polymer/neon-animation/animations/scale-down-animation.js'; + +class SampleElement extends mixinBehaviors([NeonAnimationRunnerBehavior], PolymerElement) { + static get template() { + return html` +
this div will not be animated
+ +
this div and its parent will be animated after one second
+
+ `; + } + + connectedCallback() { + super.connectedCallback(); + + // must be set here because properties is static and cannot reference "this" + this.animationConfig = { + // provided by neon-animation/animations/scale-down-animation.js + name: 'scale-down-animation', + node: this.$.animatable, + }; + + setTimeout(() => this.playAnimation(), 1000); + } +} +customElements.define('sample-element', SampleElement); +``` + +### `` +`neon-animated-pages` manages a set of pages and runs an animation when +switching between them. + +#### In an html file +```html + + + + + + + 1 + 2 + 3 + 4 + 5 + + + + + + +``` + +#### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/neon-animation/neon-animated-pages.js'; +import '@polymer/neon-animation/neon-animatable.js'; +import '@polymer/neon-animation/animations/slide-from-right-animation.js'; +import '@polymer/neon-animation/animations/slide-left-animation.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + 1 + 2 + 3 + 4 + 5 + + + + `; + } + + increase() { + this.$.pages.selected = this.$.pages.selected + 1 % 5; + } + + decrease() { + this.$.pages.selected = (this.$.pages.selected - 1 + 5) % 5; + } +} +customElements.define('sample-element', SampleElement); +``` + +#### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {NeonAnimationRunnerBehavior} from '@polymer/neon-animation/neon-animation-runner-behavior.js'; +import '@polymer/neon-animation/animations/neon-animatable.js'; +import '@polymer/neon-animation/animations/scale-down-animation.js'; + +class SampleElement extends mixinBehaviors([NeonAnimationRunnerBehavior], PolymerElement) { + static get template() { + return html` +
this div will not be animated
+ +
this div and its parent will be animated after one second
+
+ `; + } + + connectedCallback() { + super.connectedCallback(); + + // must be set here because properties is static and cannot reference "this" + this.animationConfig = { + // provided by neon-animation/animations/scale-down-animation.js + name: 'scale-down-animation', + node: this.$.animatable, + }; + + setTimeout(() => this.playAnimation(), 1000); + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/neon-animation +cd neon-animation +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-behaviors@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-behaviors.svg)](https://www.npmjs.com/package/@polymer/paper-behaviors) +[![Build status](https://travis-ci.org/PolymerElements/paper-behaviors.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-behaviors) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-behaviors) + +## <paper-behaviors> +`` is a set of behaviours to help implement Material Design elements: + +- `PaperCheckedElementBehavior` to implement a custom element +that has a `checked` property similar to `IronCheckedElementBehavior` +and is compatible with having a ripple effect. +- `PaperInkyFocusBehavior` implements a ripple when the element has keyboard focus. +- `PaperRippleBehavior` dynamically implements a ripple +when the element has focus via pointer or keyboard. This behavior is intended to be used in conjunction with and after +`IronButtonState` and `IronControlState`. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-behaviors), + [Demo](https://www.webcomponents.org/element/@polymer/paper-behaviors/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-behaviors +``` + +### Example of using one of the behaviours in a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {PaperButtonBehavior} from '@polymer/paper-behaviors/paper-button-behavior.js'; + +class SampleElement extends mixinBehaviors([PaperButtonBehavior], PolymerElement) { + static get template() { + return html` + +
I am a ripple-y button
+ `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-behaviors +cd paper-behaviors +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-button@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-button.svg)](https://www.npmjs.com/package/@polymer/paper-button) +[![Build status](https://travis-ci.org/PolymerElements/paper-button.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-button) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-button) + +## <paper-button> + +Material design: [Buttons](https://www.google.com/design/spec/components/buttons.html) + +`paper-button` is a button. When the user touches the button, a ripple effect emanates from the point of contact. It may be flat or raised. A raised button is styled with a shadow. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-button), + [Demo](https://www.webcomponents.org/element/@polymer/paper-button/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-button +``` + +### In an html file +```html + + + + + + link + raised + toggles + disabled + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-button/paper-button.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + link + raised + toggles + disabled + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-button +cd paper-button +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-checkbox@3.1.0 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-checkbox.svg)](https://www.npmjs.com/package/@polymer/paper-checkbox) +[![Build status](https://travis-ci.org/PolymerElements/paper-checkbox.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-checkbox) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-checkbox) + +## <paper-checkbox> + +`paper-checkbox` is a button that can be either checked or unchecked. User can +tap the checkbox to check or uncheck it. Usually you use checkboxes to allow +user to select multiple options from a set. If you have a single ON/OFF option, +avoid using a single checkbox and use `paper-toggle-button` instead. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-checkbox), + [Demo](https://www.webcomponents.org/element/@polymer/paper-checkbox/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/paper-checkbox +``` + +### In an HTML file + +```html + + + + + + Unchecked + Checked + Disabled + + +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import '@polymer/paper-checkbox/paper-checkbox.js'; + +class ExampleElement extends PolymerElement { + static get template() { + return html` + Unchecked + Checked + Disabled + `; + } +} + +customElements.define('example-element', ExampleElement); +``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/paper-checkbox +cd paper-checkbox +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-dialog-behavior@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-dialog-behavior.svg)](https://www.npmjs.com/package/@polymer/paper-dialog-behavior) +[![Build status](https://travis-ci.org/PolymerElements/paper-dialog-behavior.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-dialog-behavior) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-dialog-behavior) + +## PaperDialogBehavior + +Use `PaperDialogBehavior` and `paper-dialog-shared-styles.js` to implement a Material Design +dialog. + +For example, if `` implements this behavior: + +```html + +

Header

+
Dialog body
+
+ Cancel + Accept +
+
+``` + +`paper-dialog-shared-styles.js` provide styles for a header, content area, and an action area for buttons. +Use the `

` tag for the header and the `paper-dialog-buttons` or `buttons` class for the action area. You can use the +`paper-dialog-scrollable` element (in its own repository) if you need a scrolling content area. + +Use the `dialog-dismiss` and `dialog-confirm` attributes on interactive controls to close the +dialog. If the user dismisses the dialog with `dialog-confirm`, the `closingReason` will update +to include `confirmed: true`. + +### Accessibility + +This element has `role="dialog"` by default. Depending on the context, it may be more appropriate +to override this attribute with `role="alertdialog"`. + +If `modal` is set, the element will prevent the focus from exiting the element. +It will also ensure that focus remains in the dialog. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-dialog-behavior), + [Demo](https://www.webcomponents.org/element/@polymer/paper-dialog-behavior/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-dialog-behavior +``` + +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {PaperDialogBehavior} from '@polymer/paper-dialog-behavior/paper-dialog-behavior.js'; + +class SampleElement extends mixinBehaviors(PaperDialogBehavior, PolymerElement) { + static get template() { + return html` + + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-dialog-behavior +cd paper-dialog-behavior +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-dialog-scrollable@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-dialog-scrollable.svg)](https://www.npmjs.com/package/@polymer/paper-dialog-scrollable) +[![Build status](https://travis-ci.org/PolymerElements/paper-dialog-scrollable.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-dialog-scrollable) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-dialog-scrollable) + +## <paper-dialog-scrollable> +`paper-dialog-scrollable` implements a scrolling area used in a Material Design dialog. It shows +a divider at the top and/or bottom indicating more content, depending on scroll position. Use this +together with elements implementing `PaperDialogBehavior`. + +```html + +

Header

+ + Lorem ipsum... + +
+ OK +
+
+``` + +It shows a top divider after scrolling if it is not the first child in its parent container, +indicating there is more content above. It shows a bottom divider if it is scrollable and it is not +the last child in its parent container, indicating there is more content below. The bottom divider +is hidden if it is scrolled to the bottom. + +If `paper-dialog-scrollable` is not a direct child of the element implementing `PaperDialogBehavior`, +remember to set the `dialogElement`: + +```html + +

Header

+
+

Sub-header

+ + Lorem ipsum... + +
+
+ OK +
+
+ + +``` + +### Styling + +The following custom properties and mixins are available for styling: + +| Custom property | Description | Default | +| --- | --- | --- | +| `--paper-dialog-scrollable` | Mixin for the scrollable content | {} | + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-dialog-scrollable), + [Demo](https://www.webcomponents.org/element/@polymer/paper-dialog-scrollable/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-dialog-scrollable +``` + +### In an html file +```html + + + + + + + +

Heading

+ +

Scrolalble content...

+
+
+ + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-dialog/paper-dialog.js'; +import '@polymer/paper-dialog-scrollable/paper-dialog-scrollable.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + +

Heading

+ +

Scrolalble content...

+
+
+ `; + } + + _openDialog() { + this.$.dialog.open(); + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-dialog-scrollable +cd paper-dialog-scrollable +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-dialog@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-dialog.svg)](https://www.npmjs.com/package/@polymer/paper-dialog) +[![Build status](https://travis-ci.org/PolymerElements/paper-dialog.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-dialog) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-dialog) + +## <paper-dialog> +`` is a dialog with Material Design styling and optional animations when it is +opened or closed. It provides styles for a header, content area, and an action area for buttons. +You can use the `` element (in its own repository) if you need a scrolling +content area. To autofocus a specific child element after opening the dialog, give it the `autofocus` +attribute. See `PaperDialogBehavior` and `IronOverlayBehavior` for specifics. + +For example, the following code implements a dialog with a header, scrolling content area and +buttons. Focus will be given to the `dialog-confirm` button when the dialog is opened. + +```html + +

Header

+ + Lorem ipsum... + +
+ Cancel + Accept +
+
+``` + +### Styling + +See the docs for `PaperDialogBehavior` for the custom properties available for styling +this element. + +### Animations + +Set the `entry-animation` and/or `exit-animation` attributes to add an animation when the dialog +is opened or closed. See the documentation in +[PolymerElements/neon-animation](https://github.com/PolymerElements/neon-animation) for more info. + +For example: + +```html + + + +

Header

+
Dialog body
+
+``` + +### Accessibility + +See the docs for `PaperDialogBehavior` for accessibility features implemented by this +element. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-dialog), + [Demo](https://www.webcomponents.org/element/@polymer/paper-dialog/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-dialog +``` + +### In an html file +```html + + + + + + +

Content

+
+ + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-dialog/paper-dialog.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + +

Content

+
+ `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-dialog +cd paper-dialog +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-dropdown-menu@3.2.0 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-dropdown-menu.svg)](https://www.npmjs.com/package/@polymer/paper-dropdown-menu) +[![Build status](https://travis-ci.org/PolymerElements/paper-dropdown-menu.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-dropdown-menu) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-dropdown-menu) + +## <paper-dropdown-menu> +`paper-dropdown-menu` is similar to a native browser select element. +`paper-dropdown-menu` works with selectable content. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-dropdown-menu), + [Demo](https://www.webcomponents.org/element/@polymer/paper-dropdown-menu/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-dropdown-menu +``` + +### In an html file +```html + + + + + + + + allosaurus + brontosaurus + carcharodontosaurus + diplodocus + + + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-dropdown-menu/paper-dropdown-menu.js'; +import '@polymer/paper-item/paper-item.js'; +import '@polymer/paper-listbox/paper-listbox.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + + allosaurus + brontosaurus + carcharodontosaurus + diplodocus + + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-dropdown-menu +cd paper-dropdown-menu +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-icon-button@3.0.2 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-icon-button.svg)](https://www.npmjs.com/package/@polymer/paper-icon-button) +[![Build status](https://travis-ci.org/PolymerElements/paper-icon-button.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-icon-button) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-icon-button) + +## <paper-icon-button> +`paper-icon-button` is a button with an image placed at the center. When the user touches +the button, a ripple effect emanates from the center of the button. + +`paper-icon-button` does not include a default icon set. To use icons from the default +set, include `@polymer/iron-icons/iron-icons.js`, and use the `icon` attribute to specify which icon +from the icon set to use. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-icon-button), + [Demo](https://www.webcomponents.org/element/@polymer/paper-icon-button/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-icon-button +``` + +### In an html file +```html + + + + + + + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-icon-button/paper-icon-button.js'; +import '@polymer/iron-icons/iron-icons.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-icon-button +cd paper-icon-button +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-input@3.2.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-input.svg)](https://www.npmjs.com/package/@polymer/paper-input) +[![Build status](https://travis-ci.org/PolymerElements/paper-input.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-input) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-input) + +## <paper-input> +`` is a single-line text field with Material Design styling. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-input), + [Demo](https://www.webcomponents.org/element/@polymer/paper-input/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-input +``` + +### In an html file +```html + + + + + + + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-input/paper-input.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-input +cd paper-input +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-item@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-item.svg)](https://www.npmjs.com/package/@polymer/paper-item) +[![Build status](https://travis-ci.org/PolymerElements/paper-item.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-item) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-item) + +## <paper-item> +`` is an interactive list item. By default, it is a horizontal flexbox. +```html +Item +``` + +Use this element with `` to make Material Design styled two-line and three-line +items. + +```html + + +
Show your status
+
Your status is visible to everyone
+
+ +
+``` + +To use `paper-item` as a link, wrap it in an anchor tag. Since `paper-item` will +already receive focus, you may want to prevent the anchor tag from receiving +focus as well by setting its tabindex to -1. + +```html + + Polymer Project + +``` + +If you are concerned about performance and want to use `paper-item` in a `paper-listbox` +with many items, you can just use a native `button` with the `paper-item` class +applied (provided you have correctly included the shared styles): + +```html + + + + + + + +``` + +### Styling + +The following custom properties and mixins are available for styling: + +| Custom property | Description | Default | +| --- | --- | --- | +| `--paper-item-min-height` | Minimum height of the item | `48px` | +| `--paper-item` | Mixin applied to the item | `{}` | +| `--paper-item-selected-weight` | The font weight of a selected item | `bold` | +| `--paper-item-selected` | Mixin applied to selected paper-items | `{}` | +| `--paper-item-disabled-color` | The color for disabled paper-items | `--disabled-text-color` | +| `--paper-item-disabled` | Mixin applied to disabled paper-items | `{}` | +| `--paper-item-focused` | Mixin applied to focused paper-items | `{}` | +| `--paper-item-focused-before` | Mixin applied to :before focused paper-items | `{}` | + +### Accessibility + +This element has `role="listitem"` by default. Depending on usage, it may be more appropriate to set +`role="menuitem"`, `role="menuitemcheckbox"` or `role="menuitemradio"`. + +```html + + + Show your status + + + +``` + + + +## <paper-icon-item> + +`` is a convenience element to make an item with icon. It is an interactive list +item with a fixed-width icon area, according to Material Design. This is useful if the icons are of +varying widths, but you want the item bodies to line up. Use this like a ``. The child +node with the slot `item-icon` is placed in the icon area. + +```html + + + Favorite + + +
+ Avatar +
+``` + +### Styling + +The following custom properties and mixins are available for styling: + +| Custom property | Description | Default | +| --- | --- | --- | +| `--paper-item-icon-width` | Width of the icon area | `56px` | +| `--paper-item-icon` | Mixin applied to the icon area | `{}` | +| `--paper-icon-item` | Mixin applied to the item | `{}` | +| `--paper-item-selected-weight` | The font weight of a selected item | `bold` | +| `--paper-item-selected` | Mixin applied to selected paper-items | `{}` | +| `--paper-item-disabled-color` | The color for disabled paper-items | `--disabled-text-color` | +| `--paper-item-disabled` | Mixin applied to disabled paper-items | `{}` | +| `--paper-item-focused` | Mixin applied to focused paper-items | `{}` | +| `--paper-item-focused-before` | Mixin applied to :before focused paper-items | `{}` | + +### Changes in 2.0 + +Distribution is now done with the `slot="item-icon"` attributes (replacing the `item-icon` attribute): + + + + Favorite + + +## <paper-item-body> + +Use `` in a `` or `` to make two- or +three- line items. It is a flex item that is a vertical flexbox. + +```html + + +
Show your status
+
Your status is visible to everyone
+
+
+``` + +The child elements with the `secondary` attribute is given secondary text styling. + +### Styling + +The following custom properties and mixins are available for styling: + +| Custom property | Description | Default | +| --- | --- | --- | +| `--paper-item-body-two-line-min-height` | Minimum height of a two-line item | `72px` | +| `--paper-item-body-three-line-min-height` | Minimum height of a three-line item | `88px` | +| `--paper-item-body-secondary-color` | Foreground color for the `secondary` area | `--secondary-text-color` | +| `--paper-item-body-secondary` | Mixin applied to the `secondary` area | `{}` | + + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-item), + [Demo](https://www.webcomponents.org/element/@polymer/paper-item/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-item +``` + +### In an html file +```html + + + + + + Item + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-item/paper-item.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + Item + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-item +cd paper-item +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-listbox@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-listbox.svg)](https://www.npmjs.com/package/@polymer/paper-listbox) +[![Build status](https://travis-ci.org/PolymerElements/paper-listbox.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-listbox) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-listbox) + +## <paper-listbox> +`` implements an accessible listbox control with Material Design styling. The focused item +is highlighted, and the selected item has bolded text. + +```html + + Item 1 + Item 2 + +``` + +An initial selection can be specified with the `selected` attribute. + +```html + + Item 1 + Item 2 + +``` + +Make a multi-select listbox with the `multi` attribute. Items in a multi-select listbox can be deselected, +and multiple item can be selected. + +```html + + Item 1 + Item 2 + +``` + +### Styling + +The following custom properties and mixins are available for styling: + +| Custom property | Description | Default | +| --- | --- | --- | +| `--paper-listbox-background-color` | Menu background color | `--primary-background-color` | +| `--paper-listbox-color` | Menu foreground color | `--primary-text-color` | +| `--paper-listbox` | Mixin applied to the listbox | `{}` | + +### Accessibility + +`` has `role="listbox"` by default. A multi-select listbox will also have +`aria-multiselectable` set. It implements key bindings to navigate through the listbox with the up and +down arrow keys, esc to exit the listbox, and enter to activate a listbox item. Typing the first letter +of a listbox item will also focus it. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-listbox), + [Demo](https://www.webcomponents.org/element/@polymer/paper-listbox/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-listbox +``` + +### In an html file +```html + + + + + + +
item 1
+
item 2
+
item 3
+
+ + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-listbox/paper-listbox.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + +
item 1
+
item 2
+
item 3
+
+ `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-listbox +cd paper-listbox +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-menu-button@3.1.0 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-menu-button.svg)](https://www.npmjs.com/package/@polymer/paper-menu-button) +[![Build status](https://travis-ci.org/PolymerElements/paper-menu-button.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-menu-button) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-menu-button) + +## <paper-menu-button> + +`paper-menu-button` allows one to compose a designated "trigger" element with +another element that represents "content", to create a dropdown menu that +displays the "content" when the "trigger" is clicked. + +The child element assigned to the `dropdown-trigger` slot will be used as the +"trigger" element. The child element assigned to the `dropdown-content` slot will be +used as the "content" element. + +The `paper-menu-button` is sensitive to its content's `iron-select` events. If +the "content" element triggers an `iron-select` event, the `paper-menu-button` +will close automatically. + +### Styling + +The following custom properties and mixins are also available for styling: + +| Custom property | Description | Default | +| --- | --- | --- | +| `--paper-menu-button-dropdown-background` | Background color of the paper-menu-button dropdown | `--primary-background-color` | +| `--paper-menu-button` | Mixin applied to the paper-menu-button | `{}` | +| `--paper-menu-button-disabled` | Mixin applied to the paper-menu-button when disabled | `{}` | +| `--paper-menu-button-dropdown` | Mixin applied to the paper-menu-button dropdown | `{}` | +| `--paper-menu-button-content` | Mixin applied to the paper-menu-button content | `{}` | + +## paper-menu-button-animations.js + +Defines these animations: +- <paper-menu-grow-height-animation> +- <paper-menu-grow-width-animation> +- <paper-menu-shrink-height-animation> +- <paper-menu-shrink-width-animation> + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-menu-button), + [Demo](https://www.webcomponents.org/element/@polymer/paper-menu-button/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-menu-button +``` + +### In an html file +```html + + + + + + + + + Share + Settings + Help + + + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-icon-button/paper-icon-button.js'; +import '@polymer/paper-item/paper-item.js'; +import '@polymer/paper-listbox/paper-listbox.js'; +import '@polymer/paper-menu-button/paper-menu-button.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + + + Share + Settings + Help + + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-menu-button +cd paper-menu-button +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-progress@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-progress.svg)](https://www.npmjs.com/package/@polymer/paper-progress) +[![Build status](https://travis-ci.org/PolymerElements/paper-progress.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-progress) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-progress) + +## <paper-progress> + +The progress bars are for situations where the percentage completed can be +determined. They give users a quick sense of how much longer an operation +will take. + +There is also a secondary progress which is useful for displaying intermediate +progress, such as the buffer level during a streaming playback progress bar. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-progress), + [Demo](https://www.webcomponents.org/element/@polymer/paper-progress/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-progress +``` + +### In an html file +```html + + + + + + + + + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-progress/paper-progress.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-progress +cd paper-progress +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-ripple@3.0.2 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-ripple.svg)](https://www.npmjs.com/package/@polymer/paper-ripple) +[![Build status](https://travis-ci.org/PolymerElements/paper-ripple.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-ripple) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-ripple) + +## <paper-ripple> +`paper-ripple` provides a visual effect that other paper elements can +use to simulate a rippling effect emanating from the point of contact. The +effect can be visualized as a concentric circle with motion. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-ripple), + [Demo](https://www.webcomponents.org/element/@polymer/paper-ripple/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-ripple +``` + +### In an html file +```html + + + + + +
+ Click here +
+ + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-ripple/paper-ripple.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` +
+ Click here +
+ `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-ripple +cd paper-ripple +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-styles@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-styles.svg)](https://www.npmjs.com/package/@polymer/paper-styles) +[![Build status](https://travis-ci.org/PolymerElements/paper-styles.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-styles) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-styles) + +## <paper-styles> +1. [default-theme.js](https://github.com/PolymerElements/paper-styles/blob/master/default-theme.html): text, +background and accent colors that match the default Material Design theme + +1. [shadow.js](https://github.com/PolymerElements/paper-styles/blob/master/shadow.html): Material Design +[elevation](https://material.io/design/environment/light-shadows.html#shadows) and shadow styles + +1. [typography.js](https://github.com/PolymerElements/paper-styles/blob/master/typography.html): +Material Design [font](http://www.google.com/design/spec/style/typography.html#typography-styles) styles and sizes + +1. [demo-pages.js](https://github.com/PolymerElements/paper-styles/blob/master/demo-pages.html): generic styles +used in the PolymerElements demo pages + +1. [color.js](https://github.com/PolymerElements/paper-styles/blob/master/color.html): +a complete list of the colors defined in the Material Design [palette](https://www.google.com/design/spec/style/color.html) + +We recommend importing each of these individual files, and using the style mixins +available in each ones, rather than the aggregated `paper-styles.html` as a whole. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-styles), + [Demo](https://www.webcomponents.org/element/@polymer/paper-styles/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-styles +``` + +### In an html file +```html + + + + + +
Headline
+
This is a lifted paper
+ + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-styles/typography.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + +
Headline
+
This is a lifted paper
+ `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-styles +cd paper-styles +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-tabs@3.1.0 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-tabs.svg)](https://www.npmjs.com/package/@polymer/paper-tabs) +[![Build status](https://travis-ci.org/PolymerElements/paper-tabs.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-tabs) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-tabs) + +## <paper-tabs> + +`` makes it easy to explore and switch between different views or +functional aspects of an app, or to browse categorized data sets. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-tabs), + [Demo](https://www.webcomponents.org/element/@polymer/paper-tabs/demo/demo/index.html). + +## Usage + +### Installation + +``` +npm install --save @polymer/paper-tabs +``` + +### In an HTML file + +```html + + + + + + + Tab 0 + Tab 1 + Tab 2 + Tab 3 + + + +``` + +### In a Polymer 3 element + +```js +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +import '@polymer/paper-tabs/paper-tabs.js'; +import '@polymer/paper-tabs/paper-tab.js'; + +class ExampleElement extends PolymerElement { + static get template() { + return html` + + Tab 0 + Tab 1 + Tab 2 + Tab 3 + + `; + } +} + +customElements.define('example-element', ExampleElement); +``` + +## Contributing + +If you want to send a PR to this element, here are the instructions for running +the tests and demo locally: + +### Installation + +```sh +git clone https://github.com/PolymerElements/paper-tabs +cd paper-tabs +npm install +npm install -g polymer-cli +``` + +### Running the demo locally + +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests + +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-toast@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-toast.svg)](https://www.npmjs.com/package/@polymer/paper-toast) +[![Build status](https://travis-ci.org/PolymerElements/paper-toast.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-toast) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-toast) + +## <paper-toast> +`paper-toast` provides a subtle notification toast. Only one `paper-toast` will +be visible on screen. + +Use `opened` to show the toast: + +Example: + +```html + +``` + +Also `open()` or `show()` can be used to show the toast: + +Example: + +```html +Open Toast + + +... + +openToast: function() { + this.$.toast.open(); +} +``` + +Set `duration` to 0, a negative number or Infinity to persist the toast on screen: + +Example: + +```html + + Show more + +``` + +`` is affected by the [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context) of its container. Adding `` inside elements that create a new stacking context - e.g. ``, `` or `` - might result in toasts partially obstructed or clipped. Add `` to the top level (``) element, outside the structure, e.g.: + +```html + + + + +``` + +You can then use custom events to communicate with it from within child components, using `addEventListener` and `dispatchEvent`. + +### Styling + +The following custom properties and mixins are available for styling: + +| Custom property | Description | Default | +| --- | --- | --- | +| `--paper-toast-background-color` | The paper-toast background-color | `#323232` | +| `--paper-toast-color` | The paper-toast color | `#f1f1f1` | + +This element applies the mixin `--paper-font-common-base` but does not import `paper-styles/typography.html`. +In order to apply the `Roboto` font to this element, make sure you've imported `paper-styles/typography.html`. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-toast), + [Demo](https://www.webcomponents.org/element/@polymer/paper-toast/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-toast +``` + +### In an html file +```html + + + + + + + + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-toast/paper-toast.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` + + `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-toast +cd paper-toast +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/paper-tooltip@3.0.1 + +This package contains the following license and notice below: + +[![Published on NPM](https://img.shields.io/npm/v/@polymer/paper-tooltip.svg)](https://www.npmjs.com/package/@polymer/paper-tooltip) +[![Build status](https://travis-ci.org/PolymerElements/paper-tooltip.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-tooltip) +[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://webcomponents.org/element/@polymer/paper-tooltip) + +## <paper-tooltip> +`` is a label that appears on hover and focus when the user +hovers over an element with the cursor or with the keyboard. It will be centered +to an anchor element specified in the `for` attribute, or, if that doesn't exist, +centered to the parent node containing it. + +See: [Documentation](https://www.webcomponents.org/element/@polymer/paper-tooltip), + [Demo](https://www.webcomponents.org/element/@polymer/paper-tooltip/demo/demo/index.html). + +## Usage + +### Installation +``` +npm install --save @polymer/paper-tooltip +``` + +### In an html file +```html + + + + + +
+ + Tooltip text +
+ +
+ + Tooltip text +
+ + +``` +### In a Polymer 3 element +```js +import {PolymerElement, html} from '@polymer/polymer'; +import '@polymer/paper-tooltip/paper-tooltip.js'; + +class SampleElement extends PolymerElement { + static get template() { + return html` +
+ + Tooltip text +
+ +
+ + Tooltip text +
+ `; + } +} +customElements.define('sample-element', SampleElement); +``` + +## Contributing +If you want to send a PR to this element, here are +the instructions for running the tests and demo locally: + +### Installation +```sh +git clone https://github.com/PolymerElements/paper-tooltip +cd paper-tooltip +npm install +npm install -g polymer-cli +``` + +### Running the demo locally +```sh +polymer serve --npm +open http://127.0.0.1:/demo/ +``` + +### Running the tests +```sh +polymer test --npm +``` + +----------- + +The following NPM package may be included in this product: + + - @polymer/polymer@3.4.1 + +This package contains the following license and notice below: + +// Copyright (c) 2017 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + +The following NPM packages may be included in this product: + + - @sentry/browser@6.7.1 + - @sentry/core@6.7.1 + - @sentry/hub@6.7.1 + - @sentry/minimal@6.7.1 + - @sentry/node@6.7.1 + - @sentry/types@6.7.1 + - @sentry/utils@6.7.1 + +These packages each contain the following license and notice below: + +BSD 3-Clause License + +Copyright (c) 2019, Sentry +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + +The following NPM package may be included in this product: + + - @sentry/electron@2.5.4 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) 2018 Sentry (https://sentry.io/) and individual contributors. +Copyright (c) 2017 Tim Fish + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - @sentry/tracing@6.7.1 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) 2020 Sentry (https://sentry.io/) and individual contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - @types/semver@7.3.9 + +This package contains the following license and notice below: + +MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + 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 + +----------- + +The following NPM packages may be included in this product: + + - @webcomponents/shadycss@1.11.0 + - @webcomponents/webcomponentsjs@2.6.0 + +These packages each contain the following license and notice below: + +# License + +Everything in this repo is BSD style license unless otherwise specified. + +Copyright (c) 2015 The Polymer Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. +* Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + +The following NPM package may be included in this product: + + - abort-controller@3.0.0 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) 2017 Toru Nagashima + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - accepts@1.3.8 + - mime-types@2.1.35 + +These packages each contain the following license and notice below: + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - agent-base@6.0.2 + +This package contains the following license and notice below: + +agent-base +========== +### Turn a function into an [`http.Agent`][http.Agent] instance +[![Build Status](https://github.com/TooTallNate/node-agent-base/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-agent-base/actions?workflow=Node+CI) + +This module provides an `http.Agent` generator. That is, you pass it an async +callback function, and it returns a new `http.Agent` instance that will invoke the +given callback function when sending outbound HTTP requests. + +#### Some subclasses: + +Here's some more interesting uses of `agent-base`. +Send a pull request to list yours! + + * [`http-proxy-agent`][http-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTP endpoints + * [`https-proxy-agent`][https-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTPS endpoints + * [`pac-proxy-agent`][pac-proxy-agent]: A PAC file proxy `http.Agent` implementation for HTTP and HTTPS + * [`socks-proxy-agent`][socks-proxy-agent]: A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS + + +Installation +------------ + +Install with `npm`: + +``` bash +$ npm install agent-base +``` + + +Example +------- + +Here's a minimal example that creates a new `net.Socket` connection to the server +for every HTTP request (i.e. the equivalent of `agent: false` option): + +```js +var net = require('net'); +var tls = require('tls'); +var url = require('url'); +var http = require('http'); +var agent = require('agent-base'); + +var endpoint = 'http://nodejs.org/api/'; +var parsed = url.parse(endpoint); + +// This is the important part! +parsed.agent = agent(function (req, opts) { + var socket; + // `secureEndpoint` is true when using the https module + if (opts.secureEndpoint) { + socket = tls.connect(opts); + } else { + socket = net.connect(opts); + } + return socket; +}); + +// Everything else works just like normal... +http.get(parsed, function (res) { + console.log('"response" event!', res.headers); + res.pipe(process.stdout); +}); +``` + +Returning a Promise or using an `async` function is also supported: + +```js +agent(async function (req, opts) { + await sleep(1000); + // etc… +}); +``` + +Return another `http.Agent` instance to "pass through" the responsibility +for that HTTP request to that agent: + +```js +agent(function (req, opts) { + return opts.secureEndpoint ? https.globalAgent : http.globalAgent; +}); +``` + + +API +--- + +## Agent(Function callback[, Object options]) → [http.Agent][] + +Creates a base `http.Agent` that will execute the callback function `callback` +for every HTTP request that it is used as the `agent` for. The callback function +is responsible for creating a `stream.Duplex` instance of some kind that will be +used as the underlying socket in the HTTP request. + +The `options` object accepts the following properties: + + * `timeout` - Number - Timeout for the `callback()` function in milliseconds. Defaults to Infinity (optional). + +The callback function should have the following signature: + +### callback(http.ClientRequest req, Object options, Function cb) → undefined + +The ClientRequest `req` can be accessed to read request headers and +and the path, etc. The `options` object contains the options passed +to the `http.request()`/`https.request()` function call, and is formatted +to be directly passed to `net.connect()`/`tls.connect()`, or however +else you want a Socket to be created. Pass the created socket to +the callback function `cb` once created, and the HTTP request will +continue to proceed. + +If the `https` module is used to invoke the HTTP request, then the +`secureEndpoint` property on `options` _will be set to `true`_. + + +License +------- + +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +[http-proxy-agent]: https://github.com/TooTallNate/node-http-proxy-agent +[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent +[pac-proxy-agent]: https://github.com/TooTallNate/node-pac-proxy-agent +[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent +[http.Agent]: https://nodejs.org/api/http.html#http_class_http_agent + +----------- + +The following NPM package may be included in this product: + + - ajv@6.12.6 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2015-2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - argparse@2.0.1 + +This package contains the following license and notice below: + +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +----------- + +The following NPM packages may be included in this product: + + - array-flatten@1.1.1 + - path-to-regexp@0.1.7 + +These packages each contain the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - arrify@2.0.1 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - asn1@0.2.4 + +This package contains the following license and notice below: + +Copyright (c) 2011 Mark Cavage, All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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 + +----------- + +The following NPM package may be included in this product: + + - assert-plus@1.0.0 + +This package contains the following license and notice below: + +# assert-plus + +This library is a super small wrapper over node's assert module that has two +things: (1) the ability to disable assertions with the environment variable +NODE\_NDEBUG, and (2) some API wrappers for argument testing. Like +`assert.string(myArg, 'myArg')`. As a simple example, most of my code looks +like this: + +```javascript + var assert = require('assert-plus'); + + function fooAccount(options, callback) { + assert.object(options, 'options'); + assert.number(options.id, 'options.id'); + assert.bool(options.isManager, 'options.isManager'); + assert.string(options.name, 'options.name'); + assert.arrayOfString(options.email, 'options.email'); + assert.func(callback, 'callback'); + + // Do stuff + callback(null, {}); + } +``` + +# API + +All methods that *aren't* part of node's core assert API are simply assumed to +take an argument, and then a string 'name' that's not a message; `AssertionError` +will be thrown if the assertion fails with a message like: + + AssertionError: foo (string) is required + at test (/home/mark/work/foo/foo.js:3:9) + at Object. (/home/mark/work/foo/foo.js:15:1) + at Module._compile (module.js:446:26) + at Object..js (module.js:464:10) + at Module.load (module.js:353:31) + at Function._load (module.js:311:12) + at Array.0 (module.js:484:10) + at EventEmitter._tickCallback (node.js:190:38) + +from: + +```javascript + function test(foo) { + assert.string(foo, 'foo'); + } +``` + +There you go. You can check that arrays are of a homogeneous type with `Arrayof$Type`: + +```javascript + function test(foo) { + assert.arrayOfString(foo, 'foo'); + } +``` + +You can assert IFF an argument is not `undefined` (i.e., an optional arg): + +```javascript + assert.optionalString(foo, 'foo'); +``` + +Lastly, you can opt-out of assertion checking altogether by setting the +environment variable `NODE_NDEBUG=1`. This is pseudo-useful if you have +lots of assertions, and don't want to pay `typeof ()` taxes to v8 in +production. Be advised: The standard functions re-exported from `assert` are +also disabled in assert-plus if NDEBUG is specified. Using them directly from +the `assert` module avoids this behavior. + +The complete list of APIs is: + +* assert.array +* assert.bool +* assert.buffer +* assert.func +* assert.number +* assert.finite +* assert.object +* assert.string +* assert.stream +* assert.date +* assert.regexp +* assert.uuid +* assert.arrayOfArray +* assert.arrayOfBool +* assert.arrayOfBuffer +* assert.arrayOfFunc +* assert.arrayOfNumber +* assert.arrayOfFinite +* assert.arrayOfObject +* assert.arrayOfString +* assert.arrayOfStream +* assert.arrayOfDate +* assert.arrayOfRegexp +* assert.arrayOfUuid +* assert.optionalArray +* assert.optionalBool +* assert.optionalBuffer +* assert.optionalFunc +* assert.optionalNumber +* assert.optionalFinite +* assert.optionalObject +* assert.optionalString +* assert.optionalStream +* assert.optionalDate +* assert.optionalRegexp +* assert.optionalUuid +* assert.optionalArrayOfArray +* assert.optionalArrayOfBool +* assert.optionalArrayOfBuffer +* assert.optionalArrayOfFunc +* assert.optionalArrayOfNumber +* assert.optionalArrayOfFinite +* assert.optionalArrayOfObject +* assert.optionalArrayOfString +* assert.optionalArrayOfStream +* assert.optionalArrayOfDate +* assert.optionalArrayOfRegexp +* assert.optionalArrayOfUuid +* assert.AssertionError +* assert.fail +* assert.ok +* assert.equal +* assert.notEqual +* assert.deepEqual +* assert.notDeepEqual +* assert.strictEqual +* assert.notStrictEqual +* assert.throws +* assert.doesNotThrow +* assert.ifError + +# Installation + + npm install assert-plus + +## License + +The MIT License (MIT) +Copyright (c) 2012 Mark Cavage + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +## Bugs + +See . + +----------- + +The following NPM package may be included in this product: + + - asynckit@0.4.0 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2016 Alex Indigo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - aws-sign2@0.7.0 + - forever-agent@0.6.1 + - oauth-sign@0.9.0 + - request@2.88.2 + - tunnel-agent@0.6.0 + +These packages each contain the following license and notice below: + +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: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +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 + +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 + +----------- + +The following NPM package may be included in this product: + + - aws4@1.11.0 + +This package contains the following license and notice below: + +Copyright 2013 Michael Hart (michael.hart.au@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - base64-js@1.5.1 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Jameson Little + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - bcrypt-pbkdf@1.0.2 + +This package contains the following license and notice below: + +The Blowfish portions are under the following license: + +Blowfish block cipher for OpenBSD +Copyright 1997 Niels Provos +All rights reserved. + +Implementation advice by David Mazieres . + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +The bcrypt_pbkdf portions are under the following license: + +Copyright (c) 2013 Ted Unangst + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +Performance improvements (Javascript-specific): + +Copyright 2016, Joyent Inc +Author: Alex Wilson + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +----------- + +The following NPM package may be included in this product: + + - bignumber.js@9.0.2 + +This package contains the following license and notice below: + +The MIT License (MIT) +===================== + +Copyright © `<2021>` `Michael Mclaughlin` + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - body-parser@1.20.0 + - type-is@1.6.18 + +These packages each contain the following license and notice below: + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - buffer-equal-constant-time@1.0.1 + +This package contains the following license and notice below: + +Copyright (c) 2013, GoInstant Inc., a salesforce.com company +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of salesforce.com, nor GoInstant, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + +The following NPM packages may be included in this product: + + - builder-util-runtime@8.7.5 + - electron-updater@4.3.9 + +These packages each contain the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2015 Loopline Systems + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - bytes@3.1.2 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - call-bind@1.0.2 + - get-intrinsic@1.1.1 + +These packages each contain the following license and notice below: + +MIT License + +Copyright (c) 2020 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - caseless@0.12.0 + +This package contains the following license and notice below: + +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: +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +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 +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 + +----------- + +The following NPM package may be included in this product: + + - circle-flags@1.0.0 + +This package contains the following license and notice below: + +# MIT License + +Copyright (c) 2022 HatScripts + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - clipboard-polyfill@2.8.6 + +This package contains the following license and notice below: + +# License + +The MIT License (MIT) + +Copyright (c) 2014 Lucas Garron + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - combined-stream@1.0.8 + - delayed-stream@1.0.0 + +These packages each contain the following license and notice below: + +Copyright (c) 2011 Debuggable Limited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - content-disposition@0.5.4 + - forwarded@0.2.0 + - vary@1.1.2 + +These packages each contain the following license and notice below: + +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - content-type@1.0.4 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - cookie-signature@1.0.6 + +This package contains the following license and notice below: + +# cookie-signature + + Sign and unsign cookies. + +## Example + +```js +var cookie = require('cookie-signature'); + +var val = cookie.sign('hello', 'tobiiscool'); +val.should.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI'); + +var val = cookie.sign('hello', 'tobiiscool'); +cookie.unsign(val, 'tobiiscool').should.equal('hello'); +cookie.unsign(val, 'luna').should.be.false; +``` + +## License + +(The MIT License) + +Copyright (c) 2012 LearnBoost <tj@learnboost.com> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - cookie@0.4.2 + - cookie@0.5.0 + +These packages each contain the following license and notice below: + +(The MIT License) + +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - core-util-is@1.0.2 + +This package contains the following license and notice below: + +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - dashdash@1.14.1 + +This package contains the following license and notice below: + +# This is the MIT license + +Copyright (c) 2013 Trent Mick. All rights reserved. +Copyright (c) 2013 Joyent Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - debug@2.6.9 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - debug@4.3.4 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - depd@2.0.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2014-2018 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - destroy@1.2.0 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2015-2022 Douglas Christopher Wilson doug@somethingdoug.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - dotenv@8.2.0 + +This package contains the following license and notice below: + +Copyright (c) 2015, Scott Motte +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + +The following NPM package may be included in this product: + + - ecc-jsbn@0.1.2 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Jeremie Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - ecdsa-sig-formatter@1.0.11 + +This package contains the following license and notice below: + +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 2015 D2L Corporation + + 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. + +----------- + +The following NPM package may be included in this product: + + - ee-first@1.1.1 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - encodeurl@1.0.2 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - escape-html@1.0.3 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2012-2013 TJ Holowaychuk +Copyright (c) 2015 Andreas Lubbe +Copyright (c) 2015 Tiancheng "Timothy" Gu + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - etag@1.8.1 + - proxy-addr@2.0.7 + +These packages each contain the following license and notice below: + +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - event-target-shim@5.0.1 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2015 Toru Nagashima + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - express@4.18.1 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2009-2014 TJ Holowaychuk +Copyright (c) 2013-2014 Roman Shtylman +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - extend@3.0.2 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Stefan Thomas + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - extsprintf@1.3.0 + - jsprim@1.4.2 + +These packages each contain the following license and notice below: + +Copyright (c) 2012, Joyent, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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 + +----------- + +The following NPM packages may be included in this product: + + - fast-deep-equal@3.1.3 + - json-schema-traverse@0.4.1 + +These packages each contain the following license and notice below: + +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - fast-json-stable-stringify@2.1.0 + +This package contains the following license and notice below: + +This software is released under the MIT license: + +Copyright (c) 2017 Evgeny Poberezkin +Copyright (c) 2013 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - fast-text-encoding@1.0.3 + - outline-manager@1.14.0 + +These packages each contain the following license and notice below: + +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. + +----------- + +The following NPM package may be included in this product: + + - finalhandler@1.2.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - form-data@2.3.3 + +This package contains the following license and notice below: + +Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 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. + +----------- + +The following NPM package may be included in this product: + + - fresh@0.5.2 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - fs-extra@10.1.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2011-2017 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - function-bind@1.1.1 + +This package contains the following license and notice below: + +Copyright (c) 2013 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - gaxios@4.3.3 + - gaxios@5.0.0 + - gcp-metadata@5.0.0 + - google-auth-library@8.0.2 + +These packages each contain the following license and notice below: + +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. + +----------- + +The following NPM packages may be included in this product: + + - getpass@0.1.7 + - http-signature@1.2.0 + - sshpk@1.16.1 + +These packages each contain the following license and notice below: + +Copyright Joyent, Inc. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - google-p12-pem@3.1.4 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Ryan Seys + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - graceful-fs@4.2.8 + +This package contains the following license and notice below: + +The ISC License + +Copyright (c) Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +----------- + +The following NPM package may be included in this product: + + - gtoken@5.3.2 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Ryan Seys + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - har-schema@2.0.0 + +This package contains the following license and notice below: + +Copyright (c) 2015, Ahmad Nassri + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +----------- + +The following NPM package may be included in this product: + + - har-validator@5.1.5 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) 2018 Ahmad Nassri + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - has-symbols@1.0.3 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) 2016 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - has@1.0.3 + +This package contains the following license and notice below: + +Copyright (c) 2013 Thiago de Arruda + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - http-errors@2.0.0 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - https-proxy-agent@5.0.0 + +This package contains the following license and notice below: + +https-proxy-agent +================ +### An HTTP(s) proxy `http.Agent` implementation for HTTPS +[![Build Status](https://github.com/TooTallNate/node-https-proxy-agent/workflows/Node%20CI/badge.svg)](https://github.com/TooTallNate/node-https-proxy-agent/actions?workflow=Node+CI) + +This module provides an `http.Agent` implementation that connects to a specified +HTTP or HTTPS proxy server, and can be used with the built-in `https` module. + +Specifically, this `Agent` implementation connects to an intermediary "proxy" +server and issues the [CONNECT HTTP method][CONNECT], which tells the proxy to +open a direct TCP connection to the destination server. + +Since this agent implements the CONNECT HTTP method, it also works with other +protocols that use this method when connecting over proxies (i.e. WebSockets). +See the "Examples" section below for more. + + +Installation +------------ + +Install with `npm`: + +``` bash +$ npm install https-proxy-agent +``` + + +Examples +-------- + +#### `https` module example + +``` js +var url = require('url'); +var https = require('https'); +var HttpsProxyAgent = require('https-proxy-agent'); + +// HTTP/HTTPS proxy to connect to +var proxy = process.env.http_proxy || 'http://168.63.76.32:3128'; +console.log('using proxy server %j', proxy); + +// HTTPS endpoint for the proxy to connect to +var endpoint = process.argv[2] || 'https://graph.facebook.com/tootallnate'; +console.log('attempting to GET %j', endpoint); +var options = url.parse(endpoint); + +// create an instance of the `HttpsProxyAgent` class with the proxy server information +var agent = new HttpsProxyAgent(proxy); +options.agent = agent; + +https.get(options, function (res) { + console.log('"response" event!', res.headers); + res.pipe(process.stdout); +}); +``` + +#### `ws` WebSocket connection example + +``` js +var url = require('url'); +var WebSocket = require('ws'); +var HttpsProxyAgent = require('https-proxy-agent'); + +// HTTP/HTTPS proxy to connect to +var proxy = process.env.http_proxy || 'http://168.63.76.32:3128'; +console.log('using proxy server %j', proxy); + +// WebSocket endpoint for the proxy to connect to +var endpoint = process.argv[2] || 'ws://echo.websocket.org'; +var parsed = url.parse(endpoint); +console.log('attempting to connect to WebSocket %j', endpoint); + +// create an instance of the `HttpsProxyAgent` class with the proxy server information +var options = url.parse(proxy); + +var agent = new HttpsProxyAgent(options); + +// finally, initiate the WebSocket connection +var socket = new WebSocket(endpoint, { agent: agent }); + +socket.on('open', function () { + console.log('"open" event!'); + socket.send('hello world'); +}); + +socket.on('message', function (data, flags) { + console.log('"message" event! %j %j', data, flags); + socket.close(); +}); +``` + +API +--- + +### new HttpsProxyAgent(Object options) + +The `HttpsProxyAgent` class implements an `http.Agent` subclass that connects +to the specified "HTTP(s) proxy server" in order to proxy HTTPS and/or WebSocket +requests. This is achieved by using the [HTTP `CONNECT` method][CONNECT]. + +The `options` argument may either be a string URI of the proxy server to use, or an +"options" object with more specific properties: + + * `host` - String - Proxy host to connect to (may use `hostname` as well). Required. + * `port` - Number - Proxy port to connect to. Required. + * `protocol` - String - If `https:`, then use TLS to connect to the proxy. + * `headers` - Object - Additional HTTP headers to be sent on the HTTP CONNECT method. + * Any other options given are passed to the `net.connect()`/`tls.connect()` functions. + + +License +------- + +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +[CONNECT]: http://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_Tunneling + +----------- + +The following NPM package may be included in this product: + + - iconv-lite@0.4.24 + +This package contains the following license and notice below: + +Copyright (c) 2011 Alexander Shtuchkin + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - inherits@2.0.4 + +This package contains the following license and notice below: + +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +----------- + +The following NPM packages may be included in this product: + + - intl-format-cache@4.3.1 + - intl-messageformat-parser@3.6.4 + - intl-messageformat@7.8.4 + +These packages each contain the following license and notice below: + +Copyright (c) 2019, Oath Inc. + +Licensed under the terms of the New BSD license. See below for terms. + +Redistribution and use of this software in source and binary forms, +with or without modification, are permitted provided that the following +conditions are met: + +- Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +- Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +- Neither the name of Oath Inc. nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of Oath Inc. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + +The following NPM package may be included in this product: + + - intl-messageformat-parser@1.4.0 + +This package contains the following license and notice below: + +Copyright 2014 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +Inspired by and derived from: +messageformat.js https://github.com/SlexAxton/messageformat.js +Copyright 2014 Alex Sexton +Apache License, Version 2.0 + +----------- + +The following NPM package may be included in this product: + + - intl-messageformat@2.2.0 + +This package contains the following license and notice below: + +Copyright 2013 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ +Pluralization rules built from +https://github.com/papandreou/node-cldr +which is licensed under the BSD license and has the following license: + +Copyright (c) 2012, Andreas Lind Petersen +All rights reserved. + +See the following for more details: +https://github.com/papandreou/node-cldr/blob/master/LICENSE + +----------- + +The following NPM package may be included in this product: + + - ipaddr.js@1.9.1 + +This package contains the following license and notice below: + +Copyright (C) 2011-2017 whitequark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - is-stream@2.0.1 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - is-typedarray@1.0.0 + +This package contains the following license and notice below: + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - isstream@0.1.2 + +This package contains the following license and notice below: + +The MIT License (MIT) +===================== + +Copyright (c) 2015 Rod Vagg +--------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - js-yaml@4.1.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (C) 2011-2015 by Vitaly Puzrin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - jsbn@0.1.1 + +This package contains the following license and notice below: + +Licensing +--------- + +This software is covered under the following copyright: + +/* + * Copyright (c) 2003-2005 Tom Wu + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, + * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY + * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + * + * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF + * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * In addition, the following condition applies: + * + * All redistributions must retain an intact copy of this copyright notice + * and disclaimer. + */ + +Address all questions regarding this license to: + + Tom Wu + tjw@cs.Stanford.EDU + +----------- + +The following NPM package may be included in this product: + + - json-bigint@1.0.0 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2013 Andrey Sidorov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - json-schema@0.4.0 + +This package contains the following license and notice below: + +Dojo is available under *either* the terms of the BSD 3-Clause "New" License *or* the +Academic Free License version 2.1. As a recipient of Dojo, you may choose which +license to receive this code under (except as noted in per-module LICENSE +files). Some modules may not be the copyright of the Dojo Foundation. These +modules contain explicit declarations of copyright in both the LICENSE files in +the directories in which they reside and in the code itself. No external +contributions are allowed under licenses which are fundamentally incompatible +with the AFL-2.1 OR and BSD-3-Clause licenses that Dojo is distributed under. + +The text of the AFL-2.1 and BSD-3-Clause licenses is reproduced below. + +------------------------------------------------------------------------------- +BSD 3-Clause "New" License: +********************** + +Copyright (c) 2005-2015, The Dojo Foundation +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Dojo Foundation nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------- +The Academic Free License, v. 2.1: +********************************** + +This Academic Free License (the "License") applies to any original work of +authorship (the "Original Work") whose owner (the "Licensor") has placed the +following notice immediately following the copyright notice for the Original +Work: + +Licensed under the Academic Free License version 2.1 + +1) Grant of Copyright License. Licensor hereby grants You a world-wide, +royalty-free, non-exclusive, perpetual, sublicenseable license to do the +following: + +a) to reproduce the Original Work in copies; + +b) to prepare derivative works ("Derivative Works") based upon the Original +Work; + +c) to distribute copies of the Original Work and Derivative Works to the +public; + +d) to perform the Original Work publicly; and + +e) to display the Original Work publicly. + +2) Grant of Patent License. Licensor hereby grants You a world-wide, +royalty-free, non-exclusive, perpetual, sublicenseable license, under patent +claims owned or controlled by the Licensor that are embodied in the Original +Work as furnished by the Licensor, to make, use, sell and offer for sale the +Original Work and Derivative Works. + +3) Grant of Source Code License. The term "Source Code" means the preferred +form of the Original Work for making modifications to it and all available +documentation describing how to modify the Original Work. Licensor hereby +agrees to provide a machine-readable copy of the Source Code of the Original +Work along with each copy of the Original Work that Licensor distributes. +Licensor reserves the right to satisfy this obligation by placing a +machine-readable copy of the Source Code in an information repository +reasonably calculated to permit inexpensive and convenient access by You for as +long as Licensor continues to distribute the Original Work, and by publishing +the address of that information repository in a notice immediately following +the copyright notice that applies to the Original Work. + +4) Exclusions From License Grant. Neither the names of Licensor, nor the names +of any contributors to the Original Work, nor any of their trademarks or +service marks, may be used to endorse or promote products derived from this +Original Work without express prior written permission of the Licensor. Nothing +in this License shall be deemed to grant any rights to trademarks, copyrights, +patents, trade secrets or any other intellectual property of Licensor except as +expressly stated herein. No patent license is granted to make, use, sell or +offer to sell embodiments of any patent claims other than the licensed claims +defined in Section 2. No right is granted to the trademarks of Licensor even if +such marks are included in the Original Work. Nothing in this License shall be +interpreted to prohibit Licensor from licensing under different terms from this +License any Original Work that Licensor otherwise would have a right to +license. + +5) This section intentionally omitted. + +6) Attribution Rights. You must retain, in the Source Code of any Derivative +Works that You create, all copyright, patent or trademark notices from the +Source Code of the Original Work, as well as any notices of licensing and any +descriptive text identified therein as an "Attribution Notice." You must cause +the Source Code for any Derivative Works that You create to carry a prominent +Attribution Notice reasonably calculated to inform recipients that You have +modified the Original Work. + +7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that +the copyright in and to the Original Work and the patent rights granted herein +by Licensor are owned by the Licensor or are sublicensed to You under the terms +of this License with the permission of the contributor(s) of those copyrights +and patent rights. Except as expressly stated in the immediately proceeding +sentence, the Original Work is provided under this License on an "AS IS" BASIS +and WITHOUT WARRANTY, either express or implied, including, without limitation, +the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. +This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No +license to Original Work is granted hereunder except under this disclaimer. + +8) Limitation of Liability. Under no circumstances and under no legal theory, +whether in tort (including negligence), contract, or otherwise, shall the +Licensor be liable to any person for any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License +or the use of the Original Work including, without limitation, damages for loss +of goodwill, work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses. This limitation of liability shall not +apply to liability for death or personal injury resulting from Licensor's +negligence to the extent applicable law prohibits such limitation. Some +jurisdictions do not allow the exclusion or limitation of incidental or +consequential damages, so this exclusion and limitation may not apply to You. + +9) Acceptance and Termination. If You distribute copies of the Original Work or +a Derivative Work, You must make a reasonable effort under the circumstances to +obtain the express assent of recipients to the terms of this License. Nothing +else but this License (or another written agreement between Licensor and You) +grants You permission to create Derivative Works based upon the Original Work +or to exercise any of the rights granted in Section 1 herein, and any attempt +to do so except under the terms of this License (or another written agreement +between Licensor and You) is expressly prohibited by U.S. copyright law, the +equivalent laws of other countries, and by international treaty. Therefore, by +exercising any of the rights granted to You in Section 1 herein, You indicate +Your acceptance of this License and all of its terms and conditions. + +10) Termination for Patent Action. This License shall terminate automatically +and You may no longer exercise any of the rights granted to You by this License +as of the date You commence an action, including a cross-claim or counterclaim, +against Licensor or any licensee alleging that the Original Work infringes a +patent. This termination provision shall not apply for an action alleging +patent infringement by combinations of the Original Work with other software or +hardware. + +11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this +License may be brought only in the courts of a jurisdiction wherein the +Licensor resides or in which Licensor conducts its primary business, and under +the laws of that jurisdiction excluding its conflict-of-law provisions. The +application of the United Nations Convention on Contracts for the International +Sale of Goods is expressly excluded. Any use of the Original Work outside the +scope of this License or after its termination shall be subject to the +requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et +seq., the equivalent laws of other countries, and international treaty. This +section shall survive the termination of this License. + +12) Attorneys Fees. In any action to enforce the terms of this License or +seeking damages relating thereto, the prevailing party shall be entitled to +recover its costs and expenses, including, without limitation, reasonable +attorneys' fees and costs incurred in connection with such action, including +any appeal of such action. This section shall survive the termination of this +License. + +13) Miscellaneous. This License represents the complete agreement concerning +the subject matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent necessary to +make it enforceable. + +14) Definition of "You" in This License. "You" throughout this License, whether +in upper or lower case, means an individual or a legal entity exercising rights +under, and complying with all of the terms of, this License. For legal +entities, "You" includes any entity that controls, is controlled by, or is +under common control with you. For 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. + +15) Right to Use. You may use the Original Work in all ways not otherwise +restricted or conditioned by this License or by law, and Licensor promises not +to interfere with or be responsible for such uses by You. + +This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. +Permission is hereby granted to copy and distribute this license without +modification. This license may not be modified without the express written +permission of its copyright owner. + +----------- + +The following NPM packages may be included in this product: + + - json-stringify-safe@5.0.1 + - lru-cache@6.0.0 + - semver@7.3.7 + - yallist@4.0.0 + +These packages each contain the following license and notice below: + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +----------- + +The following NPM package may be included in this product: + + - jsonfile@6.1.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2012-2015, JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - jsonic@0.3.1 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2013-2017 Richard Rodger + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - jwa@2.0.0 + - jws@4.0.0 + +These packages each contain the following license and notice below: + +Copyright (c) 2013 Brian J. Brennan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - lazy-val@1.0.5 + +This package contains the following license and notice below: + +## lazy-val + +Lazy value. + +```typescript +class Lazy { + constructor(creator: () => Promise) + readonly hasValue: boolean + value: Promise +} +``` + +----------- + +The following NPM packages may be included in this product: + + - lit-element@2.5.1 + - lit-html@1.4.1 + +These packages each contain the following license and notice below: + +BSD 3-Clause License + +Copyright (c) 2017, The Polymer Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + +The following NPM package may be included in this product: + + - lodash.escaperegexp@4.1.2 + +This package contains the following license and notice below: + +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + +----------- + +The following NPM package may be included in this product: + + - lodash.isequal@4.5.0 + +This package contains the following license and notice below: + +Copyright JS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + +----------- + +The following NPM package may be included in this product: + + - lru_map@0.3.3 + +This package contains the following license and notice below: + +# Least Recently Used (LRU) cache algorithm + +A finite key-value map using the [Least Recently Used (LRU)](http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used) algorithm, where the most recently-used items are "kept alive" while older, less-recently used items are evicted to make room for newer items. + +Useful when you want to limit use of memory to only hold commonly-used things. + +[![Build status](https://travis-ci.org/rsms/js-lru.svg?branch=master)](https://travis-ci.org/rsms/js-lru) + +## Terminology & design + +- Based on a doubly-linked list for low complexity random shuffling of entries. + +- The cache object iself has a "head" (least recently used entry) and a + "tail" (most recently used entry). + +- The "oldest" and "newest" are list entries -- an entry might have a "newer" and + an "older" entry (doubly-linked, "older" being close to "head" and "newer" + being closer to "tail"). + +- Key lookup is done through a key-entry mapping native object, which on most + platforms mean `O(1)` complexity. This comes at a very low memory cost (for + storing two extra pointers for each entry). + +Fancy ASCII art illustration of the general design: + +```txt + entry entry entry entry + ______ ______ ______ ______ + | head |.newer => | |.newer => | |.newer => | tail | +.oldest = | A | | B | | C | | D | = .newest + |______| <= older.|______| <= older.|______| <= older.|______| + + removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added +``` + +## Example + +```js +let c = new LRUMap(3) +c.set('adam', 29) +c.set('john', 26) +c.set('angela', 24) +c.toString() // -> "adam:29 < john:26 < angela:24" +c.get('john') // -> 26 + +// Now 'john' is the most recently used entry, since we just requested it +c.toString() // -> "adam:29 < angela:24 < john:26" +c.set('zorro', 141) // -> {key:adam, value:29} + +// Because we only have room for 3 entries, adding 'zorro' caused 'adam' +// to be removed in order to make room for the new entry +c.toString() // -> "angela:24 < john:26 < zorro:141" +``` + +# Usage + +**Recommended:** Copy the code in lru.js or copy the lru.js and lru.d.ts files into your source directory. For minimal functionality, you only need the lines up until the comment that says "Following code is optional". + +**Using NPM:** [`yarn add lru_map`](https://www.npmjs.com/package/lru_map) (note that because NPM is one large flat namespace, you need to import the module as "lru_map" rather than simply "lru".) + +**Using AMD:** An [AMD](https://github.com/amdjs/amdjs-api/blob/master/AMD.md#amd) module loader like [`amdld`](https://github.com/rsms/js-amdld) can be used to load this module as well. There should be nothing to configure. + +**Testing**: + +- Run tests with `npm test` +- Run benchmarks with `npm run benchmark` + +**ES compatibility:** This implementation is compatible with modern JavaScript environments and depend on the following features not found in ES5: + +- `const` and `let` keywords +- `Symbol` including `Symbol.iterator` +- `Map` + +> Note: If you need ES5 compatibility e.g. to use with older browsers, [please use version 2](https://github.com/rsms/js-lru/tree/v2) which has a slightly less feature-full API but is well-tested and about as fast as this implementation. + +**Using with TypeScript** + +This module comes with complete typing coverage for use with TypeScript. If you copied the code or files rather than using a module loader, make sure to include `lru.d.ts` into the same location where you put `lru.js`. + +```ts +import {LRUMap} from './lru' +// import {LRUMap} from 'lru' // when using via AMD +// import {LRUMap} from 'lru_map' // when using from NPM +console.log('LRUMap:', LRUMap) +``` + +# API + +The API imitates that of [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), which means that in most cases you can use `LRUMap` as a drop-in replacement for `Map`. + +```ts +export class LRUMap { + // Construct a new cache object which will hold up to limit entries. + // When the size == limit, a `put` operation will evict the oldest entry. + // + // If `entries` is provided, all entries are added to the new map. + // `entries` should be an Array or other iterable object whose elements are + // key-value pairs (2-element Arrays). Each key-value pair is added to the new Map. + // null is treated as undefined. + constructor(limit :number, entries? :Iterable<[K,V]>); + + // Convenience constructor equivalent to `new LRUMap(count(entries), entries)` + constructor(entries :Iterable<[K,V]>); + + // Current number of items + size :number; + + // Maximum number of items this map can hold + limit :number; + + // Least recently-used entry. Invalidated when map is modified. + oldest :Entry; + + // Most recently-used entry. Invalidated when map is modified. + newest :Entry; + + // Replace all values in this map with key-value pairs (2-element Arrays) from + // provided iterable. + assign(entries :Iterable<[K,V]>) : void; + + // Put into the cache associated with . Replaces any existing entry + // with the same key. Returns `this`. + set(key :K, value :V) : LRUMap; + + // Purge the least recently used (oldest) entry from the cache. + // Returns the removed entry or undefined if the cache was empty. + shift() : [K,V] | undefined; + + // Get and register recent use of . + // Returns the value associated with or undefined if not in cache. + get(key :K) : V | undefined; + + // Check if there's a value for key in the cache without registering recent use. + has(key :K) : boolean; + + // Access value for without registering recent use. Useful if you do not + // want to chage the state of the map, but only "peek" at it. + // Returns the value associated with if found, or undefined if not found. + find(key :K) : V | undefined; + + // Remove entry from cache and return its value. + // Returns the removed value, or undefined if not found. + delete(key :K) : V | undefined; + + // Removes all entries + clear() : void; + + // Returns an iterator over all keys, starting with the oldest. + keys() : Iterator; + + // Returns an iterator over all values, starting with the oldest. + values() : Iterator; + + // Returns an iterator over all entries, starting with the oldest. + entries() : Iterator<[K,V]>; + + // Returns an iterator over all entries, starting with the oldest. + [Symbol.iterator]() : Iterator<[K,V]>; + + // Call `fun` for each entry, starting with the oldest entry. + forEach(fun :(value :V, key :K, m :LRUMap)=>void, thisArg? :any) : void; + + // Returns an object suitable for JSON encoding + toJSON() : Array<{key :K, value :V}>; + + // Returns a human-readable text representation + toString() : string; +} + +// An entry holds the key and value, and pointers to any older and newer entries. +// Entries might hold references to adjacent entries in the internal linked-list. +// Therefore you should never store or modify Entry objects. Instead, reference the +// key and value of an entry when needed. +interface Entry { + key :K; + value :V; +} +``` + +If you need to perform any form of finalization of items as they are evicted from the cache, wrapping the `shift` method is a good way to do it: + +```js +let c = new LRUMap(123); +c.shift = function() { + let entry = LRUMap.prototype.shift.call(this); + doSomethingWith(entry); + return entry; +} +``` + +The internals calls `shift` as entries need to be evicted, so this method is guaranteed to be called for any item that's removed from the cache. The returned entry must not include any strong references to other entries. See note in the documentation of `LRUMap.prototype.set()`. + + +# MIT license + +Copyright (c) 2010-2016 Rasmus Andersson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - media-typer@0.3.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - merge-descriptors@1.0.1 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - methods@1.1.2 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2013-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - mime-db@1.52.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - mime@1.6.0 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - ms@2.0.0 + - ms@2.1.2 + +These packages each contain the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2016 Zeit, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - ms@2.1.3 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2020 Vercel, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - negotiator@0.6.3 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2012-2014 Federico Romero +Copyright (c) 2012-2014 Isaac Z. Schlueter +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - node-fetch@2.6.7 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2016 David Frank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - node-forge@1.3.1 + +This package contains the following license and notice below: + +You may use the Forge project under the terms of either the BSD License or the +GNU General Public License (GPL) Version 2. + +The BSD License is recommended for most projects. It is simple and easy to +understand and it places almost no restrictions on what you can do with the +Forge project. + +If the GPL suits your project better you are also free to use Forge under +that license. + +You don't have to do anything special to choose one license or the other and +you don't have to notify anyone which license you are using. You are free to +use this project in commercial projects as long as the copyright header is +left intact. + +If you are a commercial entity and use this set of libraries in your +commercial software then reasonable payment to Digital Bazaar, if you can +afford it, is not required but is expected and would be appreciated. If this +library saves you time, then it's saving you money. The cost of developing +the Forge software was on the order of several hundred hours and tens of +thousands of dollars. We are attempting to strike a balance between helping +the development community while not being taken advantage of by lucrative +commercial entities for our efforts. + +------------------------------------------------------------------------------- +New BSD License (3-clause) +Copyright (c) 2010, Digital Bazaar, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Digital Bazaar, Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------- + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +----------- + +The following NPM package may be included in this product: + + - object-inspect@1.12.2 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) 2013 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - on-finished@2.4.1 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - parseurl@1.3.3 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - performance-now@2.1.0 + +This package contains the following license and notice below: + +Copyright (c) 2013 Braveg1rl + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - psl@1.8.0 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2017 Lupo Montero lupomontero@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - punycode@2.1.1 + +This package contains the following license and notice below: + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - qs@6.10.3 + - qs@6.5.3 + +These packages each contain the following license and notice below: + +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + +The following NPM package may be included in this product: + + - range-parser@1.2.1 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM packages may be included in this product: + + - safe-buffer@5.1.2 + - safe-buffer@5.2.1 + +These packages each contain the following license and notice below: + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - safer-buffer@2.1.2 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) 2018 Nikita Skovoroda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - sax@1.2.4 + +This package contains the following license and notice below: + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +==== + +`String.fromCodePoint` by Mathias Bynens used according to terms of MIT +License, as follows: + + Copyright Mathias Bynens + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + 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. + +----------- + +The following NPM package may be included in this product: + + - send@0.18.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - serve-static@1.15.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2010 Sencha Inc. +Copyright (c) 2011 LearnBoost +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - setprototypeof@1.2.0 + +This package contains the following license and notice below: + +Copyright (c) 2015, Wes Todd + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +----------- + +The following NPM package may be included in this product: + + - side-channel@1.0.4 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) 2019 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - statuses@2.0.1 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - toidentifier@1.0.1 + +This package contains the following license and notice below: + +MIT License + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - tough-cookie@2.5.0 + +This package contains the following license and notice below: + +Copyright (c) 2015, Salesforce.com, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + +The following NPM packages may be included in this product: + + - tslib@1.14.1 + - tslib@2.4.0 + +These packages each contain the following license and notice below: + +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +----------- + +The following NPM package may be included in this product: + + - tweetnacl@0.14.5 + +This package contains the following license and notice below: + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to + +----------- + +The following NPM package may be included in this product: + + - universalify@2.0.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2017, Ryan Zimmerman + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - unpipe@1.0.0 + +This package contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - uri-js@4.4.1 + +This package contains the following license and notice below: + +Copyright 2011 Gary Court. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY GARY COURT "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARY COURT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of Gary Court. + +----------- + +The following NPM package may be included in this product: + + - utils-merge@1.0.1 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2013-2017 Jared Hanson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - uuid@3.4.0 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2010-2016 Robert Kieffer and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + +----------- + +The following NPM package may be included in this product: + + - verror@1.10.0 + +This package contains the following license and notice below: + +Copyright (c) 2016, Joyent, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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 + +----------- + +The following NPM package may be included in this product: + + - web-animations-js@2.3.2 + +This package contains the following license and notice below: + +What is Web Animations? +----------------------- + +A new JavaScript API for driving animated content on the web. By unifying +the animation features of SVG and CSS, Web Animations unlocks features +previously only usable declaratively, and exposes powerful, high-performance +animation capabilities to developers. + +What is in this repository? +--------------------------- + +A JavaScript implementation of the Web Animations API that provides Web +Animation features in browsers that do not support it natively. The polyfill +falls back to the native implementation when one is available. + +Quick start +----------- + +Here's a simple example of an animation that fades and scales a `
`. +[Try it as a live demo.](http://jsbin.com/yageyezabo/edit?html,js,output) + +```html + + + + +
Hello world!
+ + + +``` + +Documentation +------------- + +* [Codelab tutorial](https://github.com/web-animations/web-animations-codelabs) +* [Examples of usage](/docs/examples.md) +* [Live demos](https://web-animations.github.io/web-animations-demos) +* [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/Element/animate) +* [W3C specification](https://drafts.csswg.org/web-animations/) + +We love feedback! +----------------- + +* For feedback on the API and the specification: + * File an issue on GitHub: + * Alternatively, send an email to with subject line +"[web-animations] ... message topic ..." +([archives](http://lists.w3.org/Archives/Public/public-fx/)). + +* For issues with the polyfill, report them on GitHub: +. + +Keep up-to-date +--------------- + +Breaking polyfill changes will be announced on this low-volume mailing list: +[web-animations-changes@googlegroups.com](https://groups.google.com/forum/#!forum/web-animations-changes). + +More info +--------- + +* [Technical details about the polyfill](/docs/support.md) + * [Browser support](/docs/support.md#browser-support) + * [Fallback to native](/docs/support.md#native-fallback) + * [Feature list](/docs/support.md#features) + * [Feature deprecation and removal processes](/docs/support.md#process-for-breaking-changes) +* To test experimental API features, try one of the + [experimental targets](/docs/experimental.md) + +----------- + +The following NPM package may be included in this product: + + - webidl-conversions@3.0.1 + +This package contains the following license and notice below: + +# The BSD 2-Clause License + +Copyright (c) 2014, Domenic Denicola +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------- + +The following NPM package may be included in this product: + + - whatwg-url@5.0.0 + +This package contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2015–2016 Sebastian Mayr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. + +----------- + +This file was generated with generate-license-file! https://www.npmjs.com/package/generate-license-file------ + +IP Geolocation by DB-IP (https://db-ip.com) + +This database is licensed under a Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file diff --git a/src/server_manager/web_app/ui_components/outline-about-dialog.ts b/src/server_manager/web_app/ui_components/outline-about-dialog.ts new file mode 100644 index 000000000..d43d72faf --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-about-dialog.ts @@ -0,0 +1,102 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; + +import '@polymer/paper-dialog/paper-dialog'; +import './cloud-install-styles'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +export interface OutlineAboutDialog extends Element { + open(): void; +} + +Polymer({ + _template: html` + + + +
+ +
+

+

+ > +

+

+ + + +

+
+ [[localize('close')]] +
+
+ `, + + is: 'outline-about-dialog', + + properties: { + localize: Function, + outlineVersion: String, + }, + + open() { + this.$.dialog.open(); + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-cloud-instructions-view.ts b/src/server_manager/web_app/ui_components/outline-cloud-instructions-view.ts new file mode 100644 index 000000000..8d7357281 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-cloud-instructions-view.ts @@ -0,0 +1,106 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; + +import '@polymer/iron-icons/iron-icons'; +import '@polymer/paper-button/paper-button'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; +Polymer({ + _template: html` + +
+
+
[[title]]
+
+ [[localize('manual-server-show-me')]] +
+
+
+ +
+ +
+ +
+
+ `, + + is: 'outline-cloud-instructions-view', + + properties: { + title: String, + imagePath: String, + thumbnailPath: String, + instructions: Array, + localize: Function, + }, + + _openImage() { + this.fire('OpenImageRequested', {imagePath: this.imagePath}); + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-do-oauth-step.ts b/src/server_manager/web_app/ui_components/outline-do-oauth-step.ts new file mode 100644 index 000000000..668f74c11 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-do-oauth-step.ts @@ -0,0 +1,180 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; + +import '@polymer/iron-pages/iron-pages'; +import './cloud-install-styles'; +import './outline-step-view'; + +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +export interface OutlineDoOauthStep extends Element { + onCancel: Function; + showConnectAccount(): void; + showAccountActive(): void; + showBilling(): void; + showEmailVerification(): void; +} + +Polymer({ + _template: html` + + + + + [[localize('oauth-connect-title')]] + [[localize('oauth-connect-description')]] + +
+ +

[[localize('oauth-connect-tag')]]

+
+ [[localize('cancel')]] +
+
+ + + [[localize('oauth-activate-account')]] + [[localize('oauth-verify')]] + +
+ +

[[localize('oauth-verify-tag')]]

+
+ [[localize('oauth-sign-out')]] +
+
+ + + [[localize('oauth-activate-account')]] + [[localize('oauth-billing')]] + +
+ +

[[localize('oauth-billing-tag')]]

+
+ [[localize('oauth-sign-out')]] +
+
+ + + [[localize('oauth-activate-account')]] + [[localize('oauth-account-active')]] + +
+ +

[[localize('oauth-account-active-tag')]]

+
+ [[localize('oauth-sign-out')]] +
+
+
+ `, + + is: 'outline-do-oauth-step', + + properties: { + currentPage: { + type: String, + value: 'connectAccount', + }, + cancelButtonText: { + type: String, + value: 'Cancel', + }, + localize: { + type: Function, + }, + onCancel: Function, + }, + + _cancelTapped() { + if (this.onCancel) { + this.onCancel(); + } + }, + + showEmailVerification() { + this.currentPage = 'verifyEmail'; + }, + + showBilling() { + this.currentPage = 'enterBilling'; + }, + + showAccountActive() { + this.currentPage = 'accountActive'; + }, + + showConnectAccount() { + this.currentPage = 'connectAccount'; + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-feedback-dialog.ts b/src/server_manager/web_app/ui_components/outline-feedback-dialog.ts new file mode 100644 index 000000000..5e96ea4ba --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-feedback-dialog.ts @@ -0,0 +1,277 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/paper-dialog/paper-dialog'; +import '@polymer/paper-dropdown-menu/paper-dropdown-menu'; +import '@polymer/paper-item/paper-item'; +import '@polymer/paper-listbox/paper-listbox'; +import '@polymer/paper-input/paper-input'; +import '@polymer/paper-input/paper-textarea'; +// This is needed to fix the "KeyframeEffect is not defined" +// see https://github.com/PolymerElements/paper-swatch-picker/issues/36 +import 'web-animations-js/web-animations-next.min'; + +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +export interface OutlineFeedbackDialog extends Element { + open(prepopulatedMessage?: string, showInstallationFailed?: boolean): void; +} + +export interface FeedbackDetail { + feedbackCategory: string; + userFeedback: string; + userEmail: string; + cloudProvider?: string; +} + +Polymer({ + _template: html` + + + +

[[title]]

+
+

[[feedbackExplanation]]

+ + + [[localize('feedback-general')]] + [[localize('feedback-install')]] + [[localize('feedback-connection')]] + [[localize('feedback-connection-others')]] + [[localize('feedback-management')]] + [[localize('feedback-suggestion')]] + + + + + DigitalOcean + Amazon Web Services + Google Cloud Platform + [[localize('feedback-other')]] + + + +

+ [[localize('feedback-disclaimer')]] +

+ +

+
+ +

+ [[localize('cancel')]] + [[localize('feedback-submit')]] +

+
+ `, + + is: 'outline-feedback-dialog', + + properties: { + title: String, + feedbackExplanation: String, + feedbackCategories: { + type: Object, + readOnly: true, + value: { + // Maps a category to its `feedbackCategoryListbox` item index. + GENERAL: 0, + INSTALLATION: 1, + CONNECTION: 2, + CONNECTION_OTHERS: 3, + MANAGEMENT: 4, + SUGGESTIONS: 5, + }, + }, + hasEnteredEmail: { + type: Boolean, + value: false, + }, + shouldShowCloudProvider: { + type: Boolean, + value: false, + }, + shouldShowLanguageDisclaimer: { + type: Boolean, + computed: '_computeShouldShowLanguageDisclaimer(hasEnteredEmail)', + }, + localize: { + type: Function, + }, + }, + + open(prepopulatedMessage: string, showInstallationFailed: boolean) { + // The localized category doesn't get displayed the first time opening the + // dialog (or after updating language) because the selected list item won't + // notice the localization change. + // This is a known issue and here is a workaround (force the selected item change): + // https://github.com/PolymerElements/paper-dropdown-menu/issues/159#issuecomment-229958448 + this.$.feedbackCategoryListbox.selected = undefined; + + // Clear all fields, in case feedback had already been entered. + if (showInstallationFailed) { + this.title = this.localize('feedback-title-install'); + this.feedbackExplanation = this.localize('feedback-explanation-install'); + this.$.dialog.classList.add('installationFailed'); + this.$.feedbackCategoryListbox.selected = this.feedbackCategories.INSTALLATION; + } else { + this.title = this.localize('feedback-title-generic'); + this.feedbackExplanation = ''; + this.$.dialog.classList.remove('installationFailed'); + this.$.feedbackCategoryListbox.selected = this.feedbackCategories.GENERAL; + } + this.$.userFeedback.invalid = false; + this.$.userFeedback.value = prepopulatedMessage || ''; + this.$.userEmail.value = ''; + this.$.cloudProviderListbox.selected = undefined; + this.$.dialog.open(); + }, + + submitTappedHandler() { + // Verify that userFeedback is entered. + if (!this.$.userFeedback.value) { + this.$.userFeedback.invalid = true; + return; + } + const data: FeedbackDetail = { + feedbackCategory: this.$.feedbackCategory.selectedItemLabel, + userFeedback: this.$.userFeedback.value, + userEmail: this.$.userEmail.value, + }; + const selectedCloudProvider = this.$.cloudProvider.selectedItemLabel; + if (this.shouldShowCloudProvider && !!selectedCloudProvider) { + data.cloudProvider = selectedCloudProvider; + } + this.fire('SubmitFeedback', data); + this.$.dialog.close(); + }, + + userEmailValueChanged() { + this.hasEnteredEmail = !!this.$.userEmail.value; + }, + + feedbackCategoryChanged() { + const selectedCategory = this.$.feedbackCategoryListbox.selected; + if ( + selectedCategory === this.feedbackCategories.INSTALLATION || + selectedCategory === this.feedbackCategories.CONNECTION || + selectedCategory === this.feedbackCategories.CONNECTION_OTHERS + ) { + this.shouldShowCloudProvider = true; + } else { + this.shouldShowCloudProvider = false; + } + this.$.dialog.notifyResize(); + }, + + userFeedbackValueChanged() { + // Hides any error message when the user starts typing feedback. + this.$.userFeedback.invalid = false; + + // Make the paper-dialog (vertically) re-center. + this.$.dialog.notifyResize(); + }, + + // Returns whether the window's locale is English (i.e. EN, en-US) and the user has + // entered their email. + _computeShouldShowLanguageDisclaimer(hasEnteredEmail: boolean) { + return !window.navigator.language.match(/^en/i) && hasEnteredEmail; + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-gcp-oauth-step.ts b/src/server_manager/web_app/ui_components/outline-gcp-oauth-step.ts new file mode 100644 index 000000000..9fe8da72f --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-gcp-oauth-step.ts @@ -0,0 +1,108 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import '@polymer/polymer/polymer-legacy'; +import '@polymer/iron-pages/iron-pages'; +import './outline-step-view'; + +import {css, customElement, html, LitElement, property} from 'lit-element'; +import {COMMON_STYLES} from '../ui_components/cloud-install-styles'; + +@customElement('outline-gcp-oauth-step') +export class GcpConnectAccountApp extends LitElement { + @property({type: Function}) onCancel: Function; + @property({type: Function}) localize: Function; + + static get styles() { + return [ + COMMON_STYLES, + css` + :host { + } + .container { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + align-items: center; + padding: 132px 0; + font-size: 14px; + } + #connectAccount img { + width: 48px; + height: 48px; + margin-bottom: 12px; + } + .card { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: space-between; + margin: 24px 0; + padding: 24px; + background: var(--background-contrast-color); + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.14), 0 2px 2px 0 rgba(0, 0, 0, 0.12), + 0 1px 3px 0 rgba(0, 0, 0, 0.2); + border-radius: 2px; + } + @media (min-width: 1025px) { + paper-card { + /* Set min with for the paper-card to grow responsively. */ + min-width: 600px; + } + } + .card p { + color: var(--light-gray); + width: 100%; + text-align: center; + } + .card paper-button { + color: var(--light-gray); + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 2px; + } + .card paper-button[disabled] { + color: var(--medium-gray); + background: transparent; + } + /* Mirror images */ + :host(:dir(rtl)) .mirror { + transform: scaleX(-1); + } + `, + ]; + // TODO: RTL + } + + render() { + return html` + ${this.localize('gcp-oauth-connect-title')} + ${this.localize('oauth-connect-description')} + +
+ +

${this.localize('oauth-connect-tag')}

+
+ ${this.localize('cancel')} +
+
`; + } + + private onCancelTapped() { + if (this.onCancel) { + this.onCancel(); + } + } +} diff --git a/src/server_manager/web_app/ui_components/outline-help-bubble.ts b/src/server_manager/web_app/ui_components/outline-help-bubble.ts new file mode 100644 index 000000000..b8ae5af01 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-help-bubble.ts @@ -0,0 +1,175 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; + +import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +export interface OutlineHelpBubble extends Element { + show(positionTarget: Element, arrowDirection: string, leftOrRightOffset: string): void; + hide(): void; +} + +Polymer({ + _template: html` + + +
+
+
+
+
+ `, + + is: 'outline-help-bubble', + + behaviors: [IronFitBehavior], + + ready() { + // Prevent help bubble from overlapping with it's positionTarget. + this.setAttribute('no-overlap', true); + + // Help bubble should default to hidden until show is called. + this.setAttribute('hidden', true); + }, + + show(positionTarget: Element, arrowDirection: string, leftOrRightOffset: string) { + this.removeAttribute('hidden'); + + // Set arrow direction. + this.classList.add('showArrow-' + arrowDirection); + + // Apply left or right offset to arrow, e.g. display an up-pointing + // arrow on the top right. + const isUpOrDown = arrowDirection === 'up' || arrowDirection === 'down'; + if (isUpOrDown && (leftOrRightOffset === 'left' || leftOrRightOffset === 'right')) { + this.classList.add('offset-' + leftOrRightOffset); + } + + // Position the help bubble. + this.positionTarget = positionTarget; + this.refit(); + + // Listen to scroll and resize events so the help bubble can reposition if needed. + window.addEventListener('scroll', this.refit.bind(this)); + window.addEventListener('resize', this.refit.bind(this)); + }, + + hide() { + this.setAttribute('hidden', true); + window.removeEventListener('scroll', this.refit.bind(this)); + window.removeEventListener('resize', this.refit.bind(this)); + this.fire('outline-help-bubble-dismissed'); + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-iconset.ts b/src/server_manager/web_app/ui_components/outline-iconset.ts new file mode 100644 index 000000000..24838a839 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-iconset.ts @@ -0,0 +1,55 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ + +import '@polymer/iron-iconset-svg/iron-iconset-svg'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +const template = html` + + + + + + + + + + + + + + + + + + + + + + + + +`; + +document.head.appendChild(template.content); diff --git a/src/server_manager/web_app/ui_components/outline-intro-step.ts b/src/server_manager/web_app/ui_components/outline-intro-step.ts new file mode 100644 index 000000000..590725c91 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-intro-step.ts @@ -0,0 +1,444 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; + +import '@polymer/paper-button/paper-button'; +import './cloud-install-styles'; +import './outline-step-view'; +import './style.css'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +const DO_CARD_HTML = html` + +
+
+
+ [[localize('setup-recommended')]] +
+ + +
+
DigitalOcean
+
+
+
    +
  • [[localize('setup-do-easiest')]]
  • +
  • [[localize('setup-do-cost')]]
  • +
  • [[localize('setup-do-data')]]
  • +
  • [[localize('setup-cancel')]]
  • +
+

+ [[localize('setup-do-create')]] +

+
+
+ +
+`; + +const GCP_STYLES = html` + +`; + +const GCP_CARD_HTML = html` + ${GCP_STYLES} + +
+
+
+ [[localize('setup-recommended')]] +
+ + +
+
Google Cloud Platform
+
+
+
    +
  • [[localize('setup-gcp-easy')]]
  • +
  • +
  • +
  • [[localize('setup-cancel')]]
  • +
+

+ [[localize('setup-gcp-create')]] +

+
+
+ +
+`; + +// TODO: Delete this card once we have full confidence in the new GCP flow. +const GCP_LEGACY_CARD_HTML = html` + ${GCP_STYLES} +
+
+
[[localize('setup-advanced')]]
+ +
+
Google Cloud Platform
+
+
+
    +
  • [[localize('setup-step-by-step')]]
  • +
  • [[localize('setup-firewall-instructions')]]
  • +
  • [[localize('setup-simple-commands')]]
  • +
+
+
+ +
+`; + +const AWS_CARD_HTML = html` + +
+
+
[[localize('setup-advanced')]]
+ +
+
Amazon Lightsail
+
+
+
    +
  • [[localize('setup-step-by-step')]]
  • +
  • [[localize('setup-firewall-instructions')]]
  • +
  • [[localize('setup-simple-commands')]]
  • +
+
+
+ +
+`; + +const MANUAL_CARD_HTML = html` + +
+
+
[[localize('setup-advanced')]]
+ +
+
[[localize('setup-anywhere')]]
+
+
+
    +
  • [[localize('setup-tested')]]
  • +
  • [[localize('setup-simple-commands')]]
  • +
+
+
+ +
+`; + +Polymer({ + _template: html` + + + + + [[localize('setup-title')]] + [[localize('setup-description')]] + +
+ ${DO_CARD_HTML} ${GCP_CARD_HTML} ${GCP_LEGACY_CARD_HTML} ${AWS_CARD_HTML} + ${MANUAL_CARD_HTML} +
+
+ `, + + is: 'outline-intro-step', + + properties: { + digitalOceanAccountName: { + type: String, + value: null, + }, + gcpAccountName: { + type: String, + value: null, + }, + localize: { + type: Function, + }, + }, + + _openLinkFreeTier: '', + _openLinkIpPrice: '', + _openLinkFreeTrial: '', + _closeLink: '', + + _computeIsAccountConnected(accountName: string) { + return Boolean(accountName); + }, + + _showNewGcpFlow(gcpAccountName: string) { + return outline.gcpAuthEnabled || this._computeIsAccountConnected(gcpAccountName); + }, + + connectToDigitalOceanTapped() { + if (this.digitalOceanAccountName) { + this.fire('CreateDigitalOceanServerRequested'); + } else { + this.fire('ConnectDigitalOceanAccountRequested'); + } + }, + + setUpGenericCloudProviderTapped() { + this.fire('SetUpGenericCloudProviderRequested'); + }, + + setUpAwsTapped() { + this.fire('SetUpAwsRequested'); + }, + + setUpGcpTapped() { + if (this.gcpAccountName) { + this.fire('CreateGcpServerRequested'); + } else { + this.fire('ConnectGcpAccountRequested'); + } + }, + + setUpGcpAdvancedTapped() { + this.fire('SetUpGcpRequested'); + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-language-picker.ts b/src/server_manager/web_app/ui_components/outline-language-picker.ts new file mode 100644 index 000000000..48e97580d --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-language-picker.ts @@ -0,0 +1,102 @@ +/* + Copyright 2018 The Outline Authors + 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. +*/ +import '@polymer/polymer/polymer-legacy'; +import '@polymer/paper-dropdown-menu/paper-dropdown-menu'; +import '@polymer/paper-listbox/paper-listbox'; +import '@polymer/paper-item/paper-item'; +import '@polymer/iron-icon/iron-icon'; +import '@polymer/iron-icons/iron-icons'; +import './cloud-install-styles'; + +import {html, PolymerElement} from '@polymer/polymer'; +import type {PolymerElementProperties} from '@polymer/polymer/interfaces'; + +export type LanguageDef = { + id: string; + name: string; + dir: 'ltr' | 'rtl'; +}; + +export class OutlineLanguagePicker extends PolymerElement { + static get template() { + return html` + + + + + + `; + } + + static get is() { + return 'outline-language-picker'; + } + + static get properties(): PolymerElementProperties { + return { + selectedLanguage: {type: String}, + languages: {type: Array}, + }; + } + + selectedLanguage = ''; + languages: LanguageDef[] = []; + + _shouldHideCheckmark(language: string, languageCode: string) { + return language !== languageCode; + } + + _languageChanged(event: CustomEvent) { + const languageCode = event.detail.value; + const languageDir = this.languages.find((lang) => lang.id === languageCode).dir; + + const params = {bubbles: true, composed: true, detail: {languageCode, languageDir}}; + const customEvent = new CustomEvent('SetLanguageRequested', params); + this.dispatchEvent(customEvent); + } +} + +customElements.define(OutlineLanguagePicker.is, OutlineLanguagePicker); diff --git a/src/server_manager/web_app/ui_components/outline-manual-server-entry.ts b/src/server_manager/web_app/ui_components/outline-manual-server-entry.ts new file mode 100644 index 000000000..b4be06f3c --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-manual-server-entry.ts @@ -0,0 +1,503 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; +import '@polymer/iron-collapse/iron-collapse'; +import '@polymer/iron-icon/iron-icon'; +import '@polymer/iron-icons/iron-icons'; +import '@polymer/paper-button/paper-button'; +import '@polymer/paper-dialog/paper-dialog'; +import '@polymer/paper-input/paper-textarea'; +import '@polymer/paper-progress/paper-progress'; +import './cloud-install-styles'; +import './outline-cloud-instructions-view'; +import './outline-step-view'; +import './style.css'; + +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +import type {IronCollapseElement} from '@polymer/iron-collapse/iron-collapse'; +import type {IronIconElement} from '@polymer/iron-icon/iron-icon'; + +export interface OutlineManualServerEntry extends Element { + clear(): void; + retryTapped(): void; + cancelTapped(): void; + cloudProvider: 'generic' | 'aws' | 'gcp'; + enableDoneButton: boolean; + showConnection: boolean; +} + +Polymer({ + _template: html` + + + + [[localize('manual-server-title')]] + [[localize('manual-server-description', 'cloudProvider', cloudProviderName)]] + +
+ +
+
[[localize('experimental')]]
+ [[localize('setup-gcp-promo')]] +
+
+
+ + 1 +
[[localize('gcp-create-server')]]
+ +
+ +
+ +
    +
  1. +
  2. [[localize('gcp-name-your-project')]]
  3. +
  4. [[localize('gcp-click-create')]]
  5. +
+
+
+
+ +
    +
  1. +
  2. [[localize('gcp-firewall-create-1')]]
  3. +
  4. [[localize('gcp-firewall-create-2')]]
  5. +
  6. [[localize('gcp-firewall-create-3')]]
  7. +
  8. [[localize('gcp-firewall-create-4')]]
  9. +
  10. [[localize('gcp-click-create')]]
  11. +
+
+
+
+ +
    +
  1. +
  2. [[localize('gcp-type-outline-server')]]
  3. +
  4. [[localize('gcp-select-region')]]
  5. +
  6. [[localize('gcp-select-machine-type')]]
  7. +
  8. [[localize('gcp-select-networking')]]
  9. +
  10. [[localize('gcp-type-network-tag')]]
  11. +
  12. [[localize('gcp-click-create')]]
  13. +
+
+
+
+
+ +
+
+ 1 +
[[localize('manual-server-firewall')]]
+ +
+ +
+ +
    +
  1. +
  2. [[localize('aws-lightsail-firewall-1')]]
  3. +
  4. [[localize('aws-lightsail-firewall-2')]]
  5. +
  6. [[localize('aws-lightsail-firewall-3')]]
  7. +
  8. [[localize('aws-lightsail-firewall-4')]]
  9. +
  10. [[localize('aws-lightsail-firewall-5')]]
  11. +
+
+
+
+
+ +
+
+ [[installScriptStepNumber]] +
[[localize('manual-server-install-run')]]
+
+
+
+ sudo bash -c "$(wget -qO- + https://raw.githubusercontent.com/Jigsaw-Code/outline-server/master/src/server_manager/install_scripts/install_server.sh)" +
+
+
+ +
+
+ [[pasteJsonStepNumber]] +
[[localize('manual-server-install-paste')]]
+
+
+ +
+
+
+ [[localize('cancel')]] + [[localize('done')]] +
+ +
+
+ `, + + is: 'outline-manual-server-entry', + + properties: { + placeholderText: { + type: String, + value: + '{"apiUrl":"https://xxx.xxx.xxx.xxx:xxxxx/xxxxxxxxxxxxxxxxxxxxxx","certSha256":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}', + }, + showConnection: Boolean, + cloudProvider: { + type: String, + value: 'generic', + }, + cloudProviderName: { + type: String, + computed: '_computeCloudProviderName(cloudProvider)', + }, + isCloudProviderAws: { + type: Boolean, + computed: '_computeIsCloudProviderAws(cloudProvider)', + }, + isCloudProviderGcp: { + type: Boolean, + computed: '_computeIsCloudProviderGcp(cloudProvider)', + }, + isGenericCloudProvider: { + type: Boolean, + computed: '_computeIsGenericCloudProvider(cloudProvider)', + }, + installScriptStepNumber: { + type: Number, + computed: '_computeInstallScriptStepNumber(isGenericCloudProvider)', + }, + pasteJsonStepNumber: { + type: Number, + computed: '_computePasteJsonStepNumber(installScriptStepNumber)', + }, + enableDoneButton: { + type: Boolean, + value: false, + }, + localize: { + type: Function, + }, + }, + + doneTapped() { + this.showConnection = true; + this.fire('ManualServerEntered', { + userInput: this.$.serverConfig.value, + }); + }, + + cancelTapped() { + this.fire('ManualServerEntryCancelled'); + }, + + retryTapped() { + this.showConnection = false; + this.doneTapped(); + }, + + gcpNewFlowTapped() { + this.fire('ConnectGcpAccountRequested'); + }, + + clear() { + this.$.serverConfig.value = ''; + this.showConnection = false; + for (const dropdown of this.root.querySelectorAll('.instructions-collapse')) { + dropdown.hide(); + } + }, + + _computeCloudProviderName(cloudProvider: string) { + switch (cloudProvider) { + case 'aws': + return 'Amazon Web Services'; + case 'gcp': + return 'Google Cloud Platform'; + default: + return ''; + } + }, + + _computeIsCloudProviderAws(cloudProvider: string) { + return cloudProvider === 'aws'; + }, + + _computeIsCloudProviderGcp(cloudProvider: string) { + return cloudProvider === 'gcp'; + }, + + _computeIsGenericCloudProvider(cloudProvider: string) { + return cloudProvider === 'generic'; + }, + + _computeInstallScriptStepNumber(isGenericCloudProvider: boolean) { + return isGenericCloudProvider ? 1 : 2; + }, + + _computePasteJsonStepNumber(installScriptStepNumber: number) { + return installScriptStepNumber + 1; + }, + + _toggleAwsDropDown() { + this._toggleDropDown(this.$.awsDropDown, this.$.awsDropDownIcon); + }, + + _toggleGcpFirewallDropDown() { + this._toggleDropDown(this.$.gcpFirewallDropDown, this.$.gcpFirewallDropDownIcon); + }, + + _toggleGcpCreateServerDropDown() { + this._toggleDropDown(this.$.gcpCreateServerDropDown, this.$.gcpCreateServerDropDownIcon); + }, + + _toggleGcpCreateProjectDropDown() { + this._toggleDropDown(this.$.gcpCreateProjectDropDown, this.$.gcpCreateProjectDropDownIcon); + }, + + _toggleDropDown(dropDown: IronCollapseElement, icon: IronIconElement) { + dropDown.toggle(); + icon.icon = dropDown.opened ? 'arrow-drop-up' : 'arrow-drop-down'; + }, + + onServerConfigChanged() { + this.fire('ManualServerEdited', { + userInput: this.$.serverConfig.value, + }); + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-metrics-option-dialog.ts b/src/server_manager/web_app/ui_components/outline-metrics-option-dialog.ts new file mode 100644 index 000000000..2756098a4 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-metrics-option-dialog.ts @@ -0,0 +1,72 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; + +import '@polymer/paper-button/paper-button'; +import '@polymer/paper-dialog/paper-dialog'; +import './cloud-install-styles'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +export interface OutlineMetricsOptionDialog extends Element { + showMetricsOptInDialog(): void; +} + +Polymer({ + _template: html` + + +
+

[[localize('metrics-title')]]

+

+
+ [[localize('metrics-skip')]] + [[localize('metrics-share')]] +
+
+ `, + + is: 'outline-metrics-option-dialog', + + properties: { + localize: { + type: Function, + }, + }, + + showMetricsOptInDialog() { + this.$.metricsEnabledDialog.open(); + }, + + enableMetricsRequested() { + this.fire('EnableMetricsRequested'); + }, + + disableMetricsRequested() { + this.fire('DisableMetricsRequested'); + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-modal-dialog.ts b/src/server_manager/web_app/ui_components/outline-modal-dialog.ts new file mode 100644 index 000000000..dec422cda --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-modal-dialog.ts @@ -0,0 +1,82 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/paper-dialog/paper-dialog'; + +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +export interface OutlineModalDialog extends Element { + open(title: string, text: string, buttons: string[]): Promise; + close(): void; +} + +Polymer({ + _template: html` + + + +

[[title]]

+
[[text]]
+

+ +

+
+ `, + + is: 'outline-modal-dialog', + + properties: { + title: String, + text: String, + buttons: Array, + }, + + // Returns a Promise which fulfills with the index of the button clicked. + open(title: string, text: string, buttons: string[]) { + this.title = title; + this.text = text; + this.buttons = buttons; + this.$.dialog.open(); + return new Promise((fulfill, reject) => { + this.fulfill = fulfill; + this.reject = reject; + }); + }, + + close() { + this.$.dialog.close(); + }, + + buttonTapped(event: Event & {model: {index: number}}) { + if (!this.fulfill) { + console.error('outline-modal-dialog: this.fulfill not defined'); + return; + } + this.fulfill(event.model.index); + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-per-key-data-limit-dialog.ts b/src/server_manager/web_app/ui_components/outline-per-key-data-limit-dialog.ts new file mode 100644 index 000000000..9bf93b857 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-per-key-data-limit-dialog.ts @@ -0,0 +1,369 @@ +/* + Copyright 2020 The Outline Authors + + 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. +*/ + +import '@polymer/paper-button/paper-button'; +import '@polymer/paper-checkbox/paper-checkbox'; +import '@polymer/paper-dialog/paper-dialog'; +import '@polymer/paper-dropdown-menu/paper-dropdown-menu'; +import '@polymer/paper-input/paper-input'; +import '@polymer/paper-item/paper-item'; +import '@polymer/paper-listbox/paper-listbox'; + +import {PaperDialogElement} from '@polymer/paper-dialog/paper-dialog'; +import {PaperInputElement} from '@polymer/paper-input/paper-input'; +import {PaperListboxElement} from '@polymer/paper-listbox/paper-listbox'; +import {css, customElement, html, internalProperty, LitElement, property} from 'lit-element'; + +import { + bytesToDisplayDataAmount, + DisplayDataAmount, + displayDataAmountToBytes, + formatBytesParts, +} from '../data_formatting'; + +import {COMMON_STYLES} from './cloud-install-styles'; + +/** + * A floating window representing settings specific to individual access keys. Its state is + * dynamically set when opened using the open() method instead of with any in-HTML attributes. + * + * This element relies on conceptual separation of the data limit as input by the user, the data + * limit of the UI key, and the actual data limit as saved on the server. App controls the UI data + * limit and the request to the server, and the display key in the element is never itself changed. + */ +@customElement('outline-per-key-data-limit-dialog') +export class OutlinePerKeyDataLimitDialog extends LitElement { + static get styles() { + return [ + COMMON_STYLES, + css` + #container { + width: 100%; + display: flex; + flex-flow: column nowrap; + } + + #dataLimitIcon { + /* Split the padding evenly between the icon and the section to be bidirectional. */ + padding: 0 12px; + } + + #headerSection { + display: flex; + flex-direction: row; + padding: 0 12px; + } + + #headerSection h3 { + font-size: 18px; + font-weight: 500; + color: rgba(0, 0, 0, 0.87); + line-height: 24px; + } + + #menuSection { + flex: 1; + padding: 0 78px; + margin-top: 10px; + } + + #buttonsSection { + margin-top: 10px; + display: flex; + flex-direction: row-reverse; + } + + paper-button { + display: flex; + height: 36px; + text-align: center; + } + + #save { + background-color: var(--primary-green); + color: #fff; + } + + #save[disabled] { + color: var(--dark-gray); + background-color: rgba(0, 0, 0, 0.13); + } + + #menu { + display: flex; + flex-flow: row nowrap; + } + + #unitsDropdown { + width: 50px; + padding: 0 10px; + } + + paper-checkbox { + /* We want the ink to be the color we're going to, not coming from */ + --paper-checkbox-checked-color: var(--primary-green); + --paper-checkbox-checked-ink-color: var(--dark-gray); + --paper-checkbox-unchecked-color: var(--dark-gray); + --paper-checkbox-unchecked-ink-color: var(--primary-green); + } + + paper-listbox paper-item:hover { + cursor: pointer; + background-color: var(--background-contrast-color); + color: #fff; + } + `, + ]; + } + + /** + * @member _keyName The displayed name of the UI access key representing the key we're working on. + */ + @internalProperty() _keyName = ''; + /** + * @member _activeDataLimitBytes The data limit, if it exists, on the access key we're working on. + */ + @internalProperty() _initialDataLimitBytes: number = undefined; + /** + * @member _showDataLimit Whether the menu for inputting the data limit should be shown. + * Controlled by the checkbox. + */ + @internalProperty() _showDataLimit = false; + /** + * @member _enableSave Whether the save button is enabled. Controlled by the validator on the + * input. + */ + @internalProperty() _enableSave = false; + + /** + * @member language The ISO 3166-1 alpha-2 language code used for i18n. + */ + @property({type: String}) language = ''; + @property({type: Function}) localize: Function; + + private _onDataLimitSet?: OnSetDataLimitHandler; + private _onDataLimitRemoved?: OnRemoveDataLimitHandler; + + override render() { + return html` + + +
+ +

${this.localize('per-key-data-limit-dialog-title', 'keyName', this._keyName)}

+
+ +
+ ${this.localize('save')} + ${this.localize('cancel')} +
+
+ `; + } + + private renderDataLimit() { + return html` + + `; + } + + private _queryAs(selector: string): T { + return this.shadowRoot.querySelector(selector) as T; + } + + private get _input(): PaperInputElement { + return this._queryAs('#dataLimitInput'); + } + + private _dataLimitValue() { + return Number(this._input?.value) ?? 0; + } + + private _dataLimitUnit(): 'GB' | 'MB' { + return this._queryAs('#unitsListbox').selected as 'GB' | 'MB'; + } + + private _getInternationalizedUnit(bytes: number) { + return formatBytesParts(bytes, this.language).unit; + } + + private _initialUnit(): 'GB' | 'MB' { + return bytesToDisplayDataAmount(this._initialDataLimitBytes)?.unit || 'GB'; + } + + private _initialValue() { + return bytesToDisplayDataAmount(this._initialDataLimitBytes)?.value.toString() || ''; + } + + private async _setCustomLimitTapped() { + this._showDataLimit = !this._showDataLimit; + if (this._showDataLimit) { + await this.updateComplete; + this._input?.focus(); + } + } + + private _setSaveButtonDisabledState() { + this._enableSave = !this._input?.invalid ?? false; + } + + private async _onSaveButtonTapped() { + const change = this._dataLimitChange(); + if (change === Change.UNCHANGED) { + return; + } + const result = + change === Change.SET + ? await this._onDataLimitSet(displayDataAmountToBytes(this.inputDataLimit())) + : await this._onDataLimitRemoved(); + if (result) { + this.close(); + } + } + + /** + * Calculates what type of change, or none at all, the user made. + */ + private _dataLimitChange(): Change { + if (this._showDataLimit) { + if (this._initialDataLimitBytes === undefined) { + // The user must have clicked the checkbox and input a limit. + return Change.SET; + } + const inputLimit = displayDataAmountToBytes(this.inputDataLimit()); + if (inputLimit !== this._initialDataLimitBytes) { + return Change.SET; + } + return Change.UNCHANGED; + } + if (this._initialDataLimitBytes !== undefined) { + // The user must have unchecked the checkbox. + return Change.REMOVED; + } + return Change.UNCHANGED; + } + + /** + * The current data limit as input by the user, but not necessarily as saved. + */ + public inputDataLimit(): DisplayDataAmount { + return this._showDataLimit + ? {unit: this._dataLimitUnit(), value: this._dataLimitValue()} + : null; + } + + /** + * Opens the dialog to display data limit information about the given key. + * + * @param keyName - The displayed name of the access key. + * @param keyDataLimit - The display data limit of the access key, or undefined. + * @param serverDefaultLimit - The default data limit for the server, or undefined if there is + * none. + * @param language - The app's current language + * @param onDataLimitSet - Callback for when a data limit is imposed. Must return whether or not + * the data limit was set successfully. Must not throw or change the dialog's state. + * @param onDataLimitRemoved - Callback for when a data limit is removed. Must return whether or + * not the data limit was removed successfully. Must not throw or change the dialog's state. + */ + public open( + keyName: string, + keyLimitBytes: number, + onDataLimitSet: OnSetDataLimitHandler, + onDataLimitRemoved: OnRemoveDataLimitHandler + ) { + this._keyName = keyName; + this._initialDataLimitBytes = keyLimitBytes; + this._showDataLimit = this._initialDataLimitBytes !== undefined; + this._onDataLimitSet = onDataLimitSet; + this._onDataLimitRemoved = onDataLimitRemoved; + + this._queryAs('#unitsListbox')?.select(this._initialUnit()); + this._setSaveButtonDisabledState(); + this._queryAs('#container').open(); + } + + private _onDialogOpenedChanged(openedChanged: CustomEvent<{value: boolean}>) { + const dialogWasClosed = !openedChanged.detail.value; + if (dialogWasClosed) { + delete this._onDataLimitSet; + delete this._onDataLimitRemoved; + } + } + + /** + * Closes the dialog. + */ + public close() { + this._queryAs('#container').close(); + } +} + +enum Change { + SET, // A data limit was added or the existing data limit changed + REMOVED, // The data limit for the key was removed + UNCHANGED, // No functional change happened. +} + +type OnSetDataLimitHandler = (dataLimitBytes: number) => Promise; +type OnRemoveDataLimitHandler = () => Promise; diff --git a/src/server_manager/web_app/ui_components/outline-progress-spinner.ts b/src/server_manager/web_app/ui_components/outline-progress-spinner.ts new file mode 100644 index 000000000..804c25543 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-progress-spinner.ts @@ -0,0 +1,82 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; + +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +// This is similar to the client's, except this spins endlessly and does not +// support different connection states. +Polymer({ + _template: html` + +
+ + + +
+ `, + + is: 'outline-progress-spinner', +}); diff --git a/src/server_manager/web_app/ui_components/outline-region-picker-step.ts b/src/server_manager/web_app/ui_components/outline-region-picker-step.ts new file mode 100644 index 000000000..6b6fa48ea --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-region-picker-step.ts @@ -0,0 +1,228 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/paper-button/paper-button'; +import '@polymer/paper-progress/paper-progress'; +import './outline-step-view'; + +import {css, customElement, html, LitElement, property} from 'lit-element'; + +import {COMMON_STYLES} from './cloud-install-styles'; +import {CloudLocationOption} from '../../model/location'; +import {getShortName, localizeCountry} from '../location_formatting'; + +const FLAG_IMAGE_DIR = 'images/flags'; + +// TODO: Reorganize type definitions to improve separation between +// model and view. +export interface RegionPickerOption extends CloudLocationOption { + markedBestValue?: boolean; +} + +@customElement('outline-region-picker-step') +export class OutlineRegionPicker extends LitElement { + @property({type: Array}) options: RegionPickerOption[] = []; + @property({type: Number}) selectedIndex = -1; + @property({type: Boolean}) isServerBeingCreated = false; + @property({type: Function}) localize: (msgId: string, ...params: string[]) => string; + @property({type: String}) language: string; + + static get styles() { + return [ + COMMON_STYLES, + css` + input[type='radio'] { + display: none; + } + input[type='radio']:checked + label.city-button { + background-color: rgba(255, 255, 255, 0.08); + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.14), 0 2px 2px 0 rgba(0, 0, 0, 0.12), + 0 1px 3px 0 rgba(0, 0, 0, 0.2); + border-radius: 4px; + border: 2px solid var(--primary-green); + } + input[type='radio'] + label.city-button:hover { + border: 2px solid var(--primary-green); + } + input[type='radio'] + label.city-button { + display: inline-block; + flex: 1; + /* Distribute space evenly, accounting for margins, so there are always 4 cards per row. */ + min-width: calc(25% - 24px); + position: relative; + margin: 4px; + text-align: center; + border: 2px solid rgba(0, 0, 0, 0); + cursor: pointer; + transition: 0.5s; + background: var(--background-contrast-color); + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.14), 0 2px 2px 0 rgba(0, 0, 0, 0.12), + 0 1px 3px 0 rgba(0, 0, 0, 0.2); + border-radius: 4px; + } + input[type='radio']:disabled + label.city-button { + /* TODO(alalama): make it look good and indicate disabled */ + filter: blur(2px); + } + .geo-name { + color: var(--light-gray); + font-size: 16px; + line-height: 19px; + } + .country-name { + color: var(--medium-gray); + font-size: 12px; + line-height: 19px; + text-transform: uppercase; + } + paper-button { + background: var(--primary-green); + color: #fff; + text-align: center; + font-size: 14px; + } + .flag { + width: 100%; + height: 100%; + } + .flag-overlay { + display: inline-block; + width: 100px; + height: 100px; + border: 4px solid rgba(255, 255, 255, 0.1); + border-radius: 50%; /* Make a circle */ + position: relative; /* Ensure the gradient uses the correct origin point. */ + margin-bottom: 12px; + } + .flag-overlay::after { + content: ''; + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + border-radius: inherit; + background: linear-gradient(to right, rgba(20, 20, 20, 0.2) 0%, rgba(0, 0, 0, 0) 100%); + } + .best-value-label { + background-color: var(--primary-green); + color: #374248; + position: absolute; + top: 117px; + left: 50%; + transform: translate(-50%, 0); + display: flex; + align-items: center; + min-height: 20px; + border-radius: 10px; + padding: 0px 10px 0px 10px; + font-size: 12px; + line-height: 14px; + } + .card-content { + display: flex; + flex-flow: wrap; + padding-top: 24px; + } + label.city-button { + padding: 28px 8px 11px 8px; + } + `, + ]; + } + + render() { + return html` + + ${this.localize('region-title')} + ${this.localize('region-description')} + + + ${this.localize('region-setup')} + + +
+ ${this.options.map((option, index) => { + return html` + `; + })} +
+ ${this.isServerBeingCreated + ? html`` + : ''} +
+ `; + } + + reset(): void { + this.isServerBeingCreated = false; + this.selectedIndex = -1; + } + + _isCreateButtonEnabled(isCreatingServer: boolean, selectedIndex: number): boolean { + return !isCreatingServer && selectedIndex >= 0; + } + + _locationSelected(event: Event): void { + const inputEl = event.target as HTMLInputElement; + this.selectedIndex = Number.parseInt(inputEl.value, 10); + } + + _flagImage(item: CloudLocationOption): string { + const countryCode = item.cloudLocation.location?.countryCode?.toLowerCase(); + const fileName = countryCode ? `${countryCode}.svg` : 'unknown.png'; + return `${FLAG_IMAGE_DIR}/${fileName}`; + } + + _handleCreateServerTap(): void { + this.isServerBeingCreated = true; + const selectedOption = this.options[this.selectedIndex]; + const params = { + bubbles: true, + composed: true, + detail: {selectedLocation: selectedOption.cloudLocation}, + }; + const customEvent = new CustomEvent('RegionSelected', params); + this.dispatchEvent(customEvent); + } +} diff --git a/src/server_manager/web_app/ui_components/outline-server-list.ts b/src/server_manager/web_app/ui_components/outline-server-list.ts new file mode 100644 index 000000000..f0654e377 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-server-list.ts @@ -0,0 +1,76 @@ +// Copyright 2021 The Outline Authors +// +// 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. + +import './outline-server-view'; + +import {customElement, html, LitElement, property} from 'lit-element'; +import {repeat} from 'lit-html/directives/repeat'; + +import type {DisplayCloudId} from './cloud-assets'; +import type {ServerView} from './outline-server-view'; + +export interface ServerViewListEntry { + id: string; + name: string; + cloudId: DisplayCloudId; +} + +@customElement('outline-server-list') +export class OutlineServerList extends LitElement { + @property({type: Array}) serverList: ServerViewListEntry[]; + @property({type: String}) selectedServerId: string; + @property({type: Function}) localize: Function; + @property({type: String}) language: string; + + render() { + if (!this.serverList) { + return; + } + return html`
+ ${repeat( + this.serverList, + (e) => e.id, + (e) => html` + + + ` + )} +
`; + } + + async getServerView(serverId: string): Promise { + if (!serverId) { + return null; + } + // We need to wait updates to be completed or the view may not yet be there. + await this.updateComplete; + const selector = `#${this.makeViewId(serverId)}`; + return this.shadowRoot.querySelector(selector); + } + + // Wrapper to encode a string in base64. This is necessary to set the server view IDs to + // the display server IDs, which are URLs, so they can be used with selector methods. The IDs + // are never decoded. + private makeViewId(serverId: string): string { + return `serverView-${btoa(serverId).replace(/=/g, '')}`; + } +} diff --git a/src/server_manager/web_app/ui_components/outline-server-progress-step.ts b/src/server_manager/web_app/ui_components/outline-server-progress-step.ts new file mode 100644 index 000000000..f05cc00ac --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-server-progress-step.ts @@ -0,0 +1,93 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/paper-progress/paper-progress'; +import '@polymer/paper-button/paper-button'; +import './outline-progress-spinner'; +import './outline-step-view'; +import {css, customElement, html, LitElement, property} from 'lit-element'; +import {COMMON_STYLES} from './cloud-install-styles'; + +@customElement('outline-server-progress-step') +export class OutlineServerProgressStep extends LitElement { + @property({type: String}) serverName: string; + @property({type: Number}) progress = 0; + @property({type: Function}) localize: Function; + + static get styles() { + return [ + COMMON_STYLES, + css` + :host { + text-align: center; + } + .card { + margin-top: 72px; + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.14), 0 2px 2px 0 rgba(0, 0, 0, 0.12), + 0 1px 3px 0 rgba(0, 0, 0, 0.2); + border-radius: 2px; + color: var(--light-gray); + background: var(--background-contrast-color); + display: flex; + flex-direction: column; + align-items: center; + } + .servername { + margin: 24px 0 72px 0; + text-align: center; + } + .card p { + font-size: 14px; + color: var(--light-gray); + } + outline-progress-spinner { + margin-top: 72px; + } + paper-button { + width: 100%; + border: 1px solid var(--light-gray); + border-radius: 2px; + color: var(--light-gray); + } + `, + ]; + } + + override render() { + return html` + ${this.localize('setup-do-title')} + ${this.localize('setup-do-description')} + + + ${this.localize('cancel')} + + +
+ +
+

${this.serverName}

+
+ +
+
`; + } + + private handleCancelTapped() { + // Set event options required to escape the shadow DOM. + this.dispatchEvent( + new CustomEvent('CancelServerCreationRequested', {bubbles: true, composed: true}) + ); + } +} diff --git a/src/server_manager/web_app/ui_components/outline-server-settings-styles.ts b/src/server_manager/web_app/ui_components/outline-server-settings-styles.ts new file mode 100644 index 000000000..e0710b8fe --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-server-settings-styles.ts @@ -0,0 +1,68 @@ +/* + Copyright 2020 The Outline Authors + + 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. +*/ + +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +// outline-server-settings-styles +// This file holds common styles for outline-server-settings and outline-validated-input +const styleElement = document.createElement('dom-module'); +styleElement.appendChild(html` `); + +styleElement.register('outline-server-settings-styles'); diff --git a/src/server_manager/web_app/ui_components/outline-server-settings.ts b/src/server_manager/web_app/ui_components/outline-server-settings.ts new file mode 100644 index 000000000..3cd7c05ed --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-server-settings.ts @@ -0,0 +1,496 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; + +import '@polymer/paper-button/paper-button'; +import '@polymer/paper-checkbox/paper-checkbox'; +import '@polymer/paper-dropdown-menu/paper-dropdown-menu'; +import '@polymer/paper-input/paper-input'; +import './cloud-install-styles'; +import './outline-server-settings-styles'; +import './outline-iconset'; +import './outline-validated-input'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +import {formatBytesParts} from '../data_formatting'; +import {getCloudName, getCloudIcon} from './cloud-assets'; +import {getShortName} from '../location_formatting'; + +export interface OutlineServerSettings extends Element { + setServerName(name: string): void; +} + +Polymer({ + _template: html` + + + +
+
+ +
+ +
+

[[_getCloudName(cloudId)]]

+ + + +
+
+
+ +
+

[[localize('settings-server-info')]]

+ + +

[[localize('settings-server-rename')]]

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

[[localize('data-limits')]]

+

[[localize('data-limits-description')]]

+
+ + + + [[localize('enabled')]] + [[localize('disabled')]] + + +
+
+ +

+
+
+ + + + [[_getInternationalizedUnit(1000000, language)]] + [[_getInternationalizedUnit(1000000000, language)]] + + +
+
+
+ +
+ +
+

[[localize('experiments')]]

+

[[localize('experiments-description')]]

+
+ +

+
+
+
+ +
+ +
+
+ +

[[localize('settings-metrics-header')]]

+
+

+
+
+
+
+ `, + + is: 'outline-server-settings', + + properties: { + serverName: String, + metricsEnabled: Boolean, + // Initialize to null so we can use the hidden attribute, which does not work well with + // undefined values. + metricsId: {type: String, value: null}, + serverHostname: {type: String, value: null}, + serverManagementApiUrl: {type: String, value: null}, + serverPortForNewAccessKeys: {type: Number, value: null}, + serverVersion: {type: String, value: null}, + isAccessKeyPortEditable: {type: Boolean, value: false}, + isDefaultDataLimitEnabled: {type: Boolean, notify: true}, + defaultDataLimit: {type: Object, value: null}, // type: app.DisplayDataAmount + supportsDefaultDataLimit: {type: Boolean, value: false}, // Whether the server supports default data limits. + showFeatureMetricsDisclaimer: {type: Boolean, value: false}, + isHostnameEditable: {type: Boolean, value: true}, + serverCreationDate: {type: Date, value: '1970-01-01T00:00:00.000Z'}, + cloudLocation: {type: Object, value: null}, + cloudId: {type: String, value: null}, + serverMonthlyCost: {type: String, value: null}, + serverMonthlyTransferLimit: {type: String, value: null}, + language: {type: String, value: 'en'}, + localize: {type: Function}, + shouldShowExperiments: {type: Boolean, value: false}, + }, + + setServerName(name: string) { + this.initialName = name; + this.name = name; + }, + + _handleNameInputKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') { + this.serverName = this.initialName; + this.$.serverNameInput.blur(); + } else if (event.key === 'Enter') { + this.$.serverNameInput.blur(); + } + }, + + _handleNameInputBlur(_event: FocusEvent) { + const newName = this.serverName; + if (!newName) { + this.serverName = this.initialName; + return; + } + // Fire signal if name has changed. + if (newName !== this.initialName) { + this.fire('ServerRenameRequested', {newName}); + } + }, + + _metricsEnabledChanged() { + const metricsSignal = this.metricsEnabled + ? 'EnableMetricsRequested' + : 'DisableMetricsRequested'; + this.fire(metricsSignal); + }, + + _defaultDataLimitEnabledChanged(e: CustomEvent) { + const wasDataLimitEnabled = this.isDefaultDataLimitEnabled; + const isDataLimitEnabled = e.detail.value === 'enabled'; + if (isDataLimitEnabled === undefined || wasDataLimitEnabled === undefined) { + return; + } else if (isDataLimitEnabled === wasDataLimitEnabled) { + return; + } + this.isDefaultDataLimitEnabled = isDataLimitEnabled; + if (isDataLimitEnabled) { + this._requestSetDefaultDataLimit(); + } else { + this.fire('RemoveDefaultDataLimitRequested'); + } + }, + + _handleDefaultDataLimitInputKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') { + this.$.defaultDataLimitInput.value = this.defaultDataLimit.value; + this.$.defaultDataLimitInput.blur(); + } else if (event.key === 'Enter') { + this.$.defaultDataLimitInput.blur(); + } + }, + + _requestSetDefaultDataLimit() { + if (this.$.defaultDataLimitInput.invalid) { + return; + } + const value = Number(this.$.defaultDataLimitInput.value); + const unit = this.$.defaultDataLimitUnits.selected; + this.fire('SetDefaultDataLimitRequested', {limit: {value, unit}}); + }, + + _computeDataLimitsEnabledName(isDefaultDataLimitEnabled: boolean) { + return isDefaultDataLimitEnabled ? 'enabled' : 'disabled'; + }, + + _validatePort(value: string) { + const port = Number(value); + const valid = !Number.isNaN(port) && port >= 1 && port <= 65535 && Number.isInteger(port); + return valid ? '' : this.localize('error-keys-port-bad-input'); + }, + + _getShortName: getShortName, + _getCloudIcon: getCloudIcon, + _getCloudName: getCloudName, + + _getInternationalizedUnit(bytesAmount: number, language: string) { + return formatBytesParts(bytesAmount, language).unit; + }, + + _formatDate(language: string, date: Date) { + return date.toLocaleString(language, {year: 'numeric', month: 'long', day: 'numeric'}); + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-server-view.ts b/src/server_manager/web_app/ui_components/outline-server-view.ts new file mode 100644 index 000000000..d4c7d9182 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-server-view.ts @@ -0,0 +1,1149 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/paper-dialog/paper-dialog'; +import '@polymer/iron-icons/iron-icons'; +import '@polymer/iron-pages/iron-pages'; +import '@polymer/iron-icons/editor-icons'; +import '@polymer/iron-icons/social-icons'; +import '@polymer/paper-icon-button/paper-icon-button'; +import '@polymer/paper-item/paper-item'; +import '@polymer/paper-listbox/paper-listbox'; +import '@polymer/paper-menu-button/paper-menu-button'; +import '@polymer/paper-progress/paper-progress'; +import '@polymer/paper-tabs/paper-tabs'; +import '@polymer/paper-tooltip/paper-tooltip'; +import './cloud-install-styles'; +import './outline-iconset'; +import './outline-help-bubble'; +import './outline-metrics-option-dialog'; +import './outline-server-progress-step'; +import './outline-server-settings'; +import './outline-share-dialog'; +import './outline-sort-span'; +import {html, PolymerElement} from '@polymer/polymer'; +import {DirMixin} from '@polymer/polymer/lib/mixins/dir-mixin'; + +import * as formatting from '../data_formatting'; +import {getShortName} from '../location_formatting'; +import {getCloudIcon} from './cloud-assets'; + +import type {PolymerElementProperties} from '@polymer/polymer/interfaces'; +import type {DomRepeat} from '@polymer/polymer/lib/elements/dom-repeat'; +import type {CloudLocation} from '../../model/location'; +import type {AccessKeyId} from '../../model/server'; +import type {OutlineHelpBubble} from './outline-help-bubble'; +import type {OutlineServerSettings} from './outline-server-settings'; + +export const MY_CONNECTION_USER_ID = '0'; + +const progressBarMaxWidthPx = 72; + +// Makes an CustomEvent that bubbles up beyond the shadow root. +function makePublicEvent(eventName: string, detail?: object) { + const params: CustomEventInit = {bubbles: true, composed: true}; + if (detail !== undefined) { + params.detail = detail; + } + return new CustomEvent(eventName, params); +} + +function compare(a: T, b: T): -1 | 0 | 1 { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } + return 0; +} + +interface KeyRowEvent extends Event { + model: {index: number; item: DisplayAccessKey}; +} + +/** + * Allows using an optional number as a boolean value without 0 being falsey. + * @returns True if x is neither null nor undefined + */ +function exists(x: number): boolean { + return x !== null && x !== undefined; +} + +/** An access key to be displayed */ +export type DisplayAccessKey = { + id: string; + placeholderName: string; + name: string; + accessUrl: string; + transferredBytes: number; + /** The data limit assigned to the key, if it exists. */ + dataLimitBytes?: number; + dataLimit?: formatting.DisplayDataAmount; +}; + +export class ServerView extends DirMixin(PolymerElement) { + static get template() { + return html` + + + +
+ + +
${this.unreachableViewTemplate}
+
${this.managementViewTemplate}
+
+
+ + + +

[[localize('server-help-connection-title')]]

+

[[localize('server-help-connection-description')]]

+ [[localize('server-help-connection-ok')]] +
+ + +

[[localize('server-help-access-key-title')]]

+

[[localize('server-help-access-key-description')]]

+ [[localize('server-help-access-key-next')]] +
+ +

[[localize('data-limits-dialog-title')]]

+

[[localize('data-limits-dialog-text')]]

+ [[localize('ok')]] +
+ `; + } + + static get unreachableViewTemplate() { + return html`
+
+

[[serverName]]

+
+
+
+ +

[[localize('server-unreachable')]]

+

+
[[localize('server-unreachable-description')]]
+ [[localize('server-unreachable-managed-description')]] + [[localize('server-unreachable-manual-description')]] +
+ [[localize('server-remove')]] + [[localize('server-destroy')]] + [[localize('retry')]] +
+
`; + } + + static get managementViewTemplate() { + return html`
+
+

[[serverName]]

+ + + + + [[localize('server-destroy')]] + + + [[localize('server-remove')]] + + + +
+
[[getShortName(cloudLocation, localize)]]
+
+
+
+ + [[localize('server-connections')]] + [[localize('server-settings')]] + +
+ +
+
+
+ +
+

[[_formatInboundBytesValue(totalInboundBytes, language)]]

+

[[_formatInboundBytesUnit(totalInboundBytes, language)]]

+
+

[[localize('server-data-transfer')]]

+
+
+
+ +
+
+

+ [[_computeManagedServerUtilizationPercentage(totalInboundBytes, + monthlyOutboundTransferBytes)]] +

+

/[[_formatBytesTransferred(monthlyOutboundTransferBytes, language)]]

+
+

[[localize('server-data-used')]]

+
+
+ +
+

[[accessKeyRows.length]]

+

[[localize('server-keys')]]

+
+

[[localize('server-access')]]

+
+
+ +
+ +
+ + [[localize('server-access-keys')]] + + + [[localize('server-usage')]] + + +
+
+ + +
+ +
+ + +
+ [[localize('server-access-key-new')]] +
+
+
+
+
+
+ + +
+
`; + } + + static get is() { + return 'outline-server-view'; + } + + static get properties(): PolymerElementProperties { + return { + metricsId: String, + serverId: String, + serverName: String, + serverHostname: String, + serverVersion: String, + isHostnameEditable: Boolean, + serverManagementApiUrl: String, + serverPortForNewAccessKeys: Number, + isAccessKeyPortEditable: Boolean, + serverCreationDate: Date, + cloudLocation: Object, + cloudId: String, + defaultDataLimitBytes: Number, + isDefaultDataLimitEnabled: Boolean, + supportsDefaultDataLimit: Boolean, + showFeatureMetricsDisclaimer: Boolean, + installProgress: Number, + isServerReachable: Boolean, + retryDisplayingServer: Function, + totalInboundBytes: Number, + baselineDataTransfer: Number, + accessKeyRows: Array, + hasNonAdminAccessKeys: Boolean, + metricsEnabled: Boolean, + monthlyOutboundTransferBytes: Number, + monthlyCost: Number, + accessKeySortBy: String, + accessKeySortDirection: Number, + language: String, + localize: Function, + selectedPage: String, + selectedTab: String, + }; + } + + static get observers() { + return ['_accessKeysAddedOrRemoved(accessKeyRows.splices)']; + } + + serverId = ''; + metricsId = ''; + serverName = ''; + serverHostname = ''; + serverVersion = ''; + isHostnameEditable = false; + serverManagementApiUrl = ''; + serverPortForNewAccessKeys: number = null; + isAccessKeyPortEditable = false; + serverCreationDate = new Date(0); + cloudLocation: CloudLocation = null; + cloudId = ''; + readonly getShortName = getShortName; + readonly getCloudIcon = getCloudIcon; + defaultDataLimitBytes: number = null; + isDefaultDataLimitEnabled = false; + hasPerKeyDataLimitDialog = false; + /** Whether the server supports default data limits. */ + supportsDefaultDataLimit = false; + showFeatureMetricsDisclaimer = false; + installProgress = 0; + isServerReachable = false; + /** Callback for retrying to display an unreachable server. */ + retryDisplayingServer: () => void = null; + totalInboundBytes = 0; + /** The number to which access key transfer amounts are compared for progress bar display */ + baselineDataTransfer = Number.POSITIVE_INFINITY; + accessKeyRows: DisplayAccessKey[] = []; + hasNonAdminAccessKeys = false; + metricsEnabled = false; + // Initialize monthlyOutboundTransferBytes and monthlyCost to 0, so they can + // be bound to hidden attributes. Initializing to undefined does not + // cause hidden$=... expressions to be evaluated and so elements may be + // shown incorrectly. See: + // https://stackoverflow.com/questions/33700125/polymer-1-0-hidden-attribute-negate-operator + // https://www.polymer-project.org/1.0/docs/devguide/data-binding.html + monthlyOutboundTransferBytes = 0; + monthlyCost = 0; + accessKeySortBy = 'name'; + /** The direction to sort: 1 == ascending, -1 == descending */ + accessKeySortDirection: -1 | 1 = 1; + language = 'en'; + localize: (msgId: string, ...params: string[]) => string = null; + selectedPage: 'progressView' | 'unreachableView' | 'managementView' = 'managementView'; + selectedTab: 'connections' | 'settings' = 'connections'; + + addAccessKey(accessKey: DisplayAccessKey) { + // TODO(fortuna): Restore loading animation. + // TODO(fortuna): Restore highlighting. + this.push('accessKeyRows', accessKey); + // Force render the access key list so that the input is present in the DOM + this.$.accessKeysContainer.querySelector('dom-repeat').render(); + const input = this.shadowRoot.querySelector(`#access-key-${accessKey.id}`); + input.select(); + } + + removeAccessKey(accessKeyId: string) { + for (let ui = 0; ui < this.accessKeyRows.length; ui++) { + if (this.accessKeyRows[ui].id === accessKeyId) { + this.splice('accessKeyRows', ui, 1); + return; + } + } + } + + updateAccessKeyRow(accessKeyId: string, fields: object) { + let newAccessKeyRow; + for (const accessKeyRowIndex in this.accessKeyRows) { + if (this.accessKeyRows[accessKeyRowIndex].id === accessKeyId) { + newAccessKeyRow = Object.assign({}, this.get(['accessKeyRows', accessKeyRowIndex]), fields); + this.set(['accessKeyRows', accessKeyRowIndex], newAccessKeyRow); + return; + } + } + } + + // Help bubbles should be shown after this outline-server-view + // is on the screen (e.g. selected in iron-pages). If help bubbles + // are initialized before this point, setPosition will not work and + // they will appear in the top left of the view. + showGetConnectedHelpBubble() { + return this._showHelpBubble('getConnectedHelpBubble', 'accessKeysContainer'); + } + + showAddAccessKeyHelpBubble() { + return this._showHelpBubble('addAccessKeyHelpBubble', 'addAccessKeyRow', 'down', 'left'); + } + + showDataLimitsHelpBubble() { + return this._showHelpBubble('dataLimitsHelpBubble', 'settingsTab', 'up', 'right'); + } + + /** Returns the UI access key with the given ID. */ + findUiKey(id: AccessKeyId): DisplayAccessKey { + return this.accessKeyRows.find((key) => key.id === id); + } + + _closeAddAccessKeyHelpBubble() { + (this.$.addAccessKeyHelpBubble as OutlineHelpBubble).hide(); + } + + _closeGetConnectedHelpBubble() { + (this.$.getConnectedHelpBubble as OutlineHelpBubble).hide(); + } + + _closeDataLimitsHelpBubble() { + (this.$.dataLimitsHelpBubble as OutlineHelpBubble).hide(); + } + + _handleAddAccessKeyPressed() { + this.dispatchEvent(makePublicEvent('AddAccessKeyRequested')); + (this.$.addAccessKeyHelpBubble as OutlineHelpBubble).hide(); + } + + _handleNameInputKeyDown(event: KeyRowEvent & KeyboardEvent) { + const input = event.target as HTMLInputElement; + if (event.key === 'Escape') { + const accessKey = event.model.item; + input.value = accessKey.name; + input.blur(); + } else if (event.key === 'Enter') { + input.blur(); + } + } + + _handleNameInputBlur(event: KeyRowEvent & FocusEvent) { + const input = event.target as HTMLInputElement; + const accessKey = event.model.item; + const displayName = input.value; + if (displayName === accessKey.name) { + return; + } + input.disabled = true; + this.dispatchEvent( + makePublicEvent('RenameAccessKeyRequested', { + accessKeyId: accessKey.id, + newName: displayName, + entry: { + commitName: () => { + input.disabled = false; + // Update accessKeyRows so the UI is updated. + this.accessKeyRows = this.accessKeyRows.map((row) => { + if (row.id !== accessKey.id) { + return row; + } + return {...row, name: displayName}; + }); + }, + revertName: () => { + input.value = accessKey.name; + input.disabled = false; + }, + }, + }) + ); + } + + _handleShowPerKeyDataLimitDialogPressed(event: KeyRowEvent) { + const accessKey = event.model?.item; + const keyId = accessKey.id; + const keyDataLimitBytes = accessKey.dataLimitBytes; + const keyName = accessKey.name || accessKey.placeholderName; + const defaultDataLimitBytes = this.isDefaultDataLimitEnabled + ? this.defaultDataLimitBytes + : undefined; + const serverId = this.serverId; + this.dispatchEvent( + makePublicEvent('OpenPerKeyDataLimitDialogRequested', { + keyId, + keyDataLimitBytes, + keyName, + serverId, + defaultDataLimitBytes, + }) + ); + } + + _handleRenameAccessKeyPressed(event: KeyRowEvent) { + const input = this.$.accessKeysContainer.querySelectorAll( + '.access-key-row .access-key-container > input' + )[event.model.index]; + // This needs to be deferred because the closing menu messes up with the focus. + window.setTimeout(() => { + input.focus(); + }, 0); + } + + _handleShareCodePressed(event: KeyRowEvent) { + const accessKey = event.model.item; + this.dispatchEvent( + makePublicEvent('OpenShareDialogRequested', {accessKey: accessKey.accessUrl}) + ); + } + + _handleRemoveAccessKeyPressed(e: KeyRowEvent) { + const accessKey = e.model.item; + this.dispatchEvent(makePublicEvent('RemoveAccessKeyRequested', {accessKeyId: accessKey.id})); + } + + _formatDataLimitForKey(key: DisplayAccessKey, language: string, localize: Function) { + return this._formatDisplayDataLimit(this._activeDataLimitForKey(key), language, localize); + } + + _computeDisplayDataLimit(limit?: number) { + return formatting.bytesToDisplayDataAmount(limit); + } + + _formatDisplayDataLimit(limit: number, language: string, localize: Function) { + return exists(limit) ? formatting.formatBytes(limit, language) : localize('no-data-limit'); + } + + _formatInboundBytesUnit(totalBytes: number, language: string) { + // This happens during app startup before we set the language + if (!language) { + return ''; + } + return formatting.formatBytesParts(totalBytes, language).unit; + } + + _formatInboundBytesValue(totalBytes: number, language: string) { + // This happens during app startup before we set the language + if (!language) { + return ''; + } + return formatting.formatBytesParts(totalBytes, language).value; + } + + _formatBytesTransferred(numBytes: number, language: string, emptyValue = '') { + if (!numBytes) { + // numBytes may not be set for manual servers, or may be 0 for + // unused access keys. + return emptyValue; + } + return formatting.formatBytes(numBytes, language); + } + + _formatMonthlyCost(monthlyCost: number, language: string) { + if (!monthlyCost) { + return ''; + } + return new Intl.NumberFormat(language, { + style: 'currency', + currency: 'USD', + currencyDisplay: 'code', + }).format(monthlyCost); + } + + _computeManagedServerUtilizationPercentage(numBytes: number, monthlyLimitBytes: number) { + let utilizationPercentage = 0; + if (monthlyLimitBytes && numBytes) { + utilizationPercentage = Math.round((numBytes / monthlyLimitBytes) * 100); + } + if (document.documentElement.dir === 'rtl') { + return `%${utilizationPercentage}`; + } + return `${utilizationPercentage}%`; + } + + _accessKeysAddedOrRemoved(_changeRecord: unknown) { + // Check for user key and regular access keys. + let hasNonAdminAccessKeys = true; + for (const ui in this.accessKeyRows) { + if (this.accessKeyRows[ui].id === MY_CONNECTION_USER_ID) { + hasNonAdminAccessKeys = false; + break; + } + } + this.hasNonAdminAccessKeys = hasNonAdminAccessKeys; + } + + _selectedTabChanged() { + if (this.selectedTab === 'settings') { + this._closeAddAccessKeyHelpBubble(); + this._closeGetConnectedHelpBubble(); + this._closeDataLimitsHelpBubble(); + (this.$.serverSettings as OutlineServerSettings).setServerName(this.serverName); + } + } + + _showHelpBubble( + helpBubbleId: string, + positionTargetId: string, + arrowDirection = 'down', + arrowAlignment = 'right' + ) { + return new Promise((resolve) => { + const helpBubble = this.$[helpBubbleId] as OutlineHelpBubble; + helpBubble.show(this.$[positionTargetId], arrowDirection, arrowAlignment); + helpBubble.addEventListener('outline-help-bubble-dismissed', resolve); + }); + } + + isRegularConnection(item: DisplayAccessKey) { + return item.id !== MY_CONNECTION_USER_ID; + } + + _computeColumnDirection( + columnName: string, + accessKeySortBy: string, + accessKeySortDirection: -1 | 1 + ) { + if (columnName === accessKeySortBy) { + return accessKeySortDirection; + } + return 0; + } + + _setSortByOrToggleDirection(e: MouseEvent) { + const element = e.target as HTMLElement; + const sortBy = element.dataset.sortBy; + if (this.accessKeySortBy !== sortBy) { + this.accessKeySortBy = sortBy; + this.accessKeySortDirection = sortBy === 'usage' ? -1 : 1; + } else { + this.accessKeySortDirection *= -1; + } + } + + _sortAccessKeys(accessKeySortBy: string, accessKeySortDirection: -1 | 1) { + if (accessKeySortBy === 'usage') { + return (a: DisplayAccessKey, b: DisplayAccessKey) => { + return (a.transferredBytes - b.transferredBytes) * accessKeySortDirection; + }; + } + // Default to sorting by name. + return (a: DisplayAccessKey, b: DisplayAccessKey) => { + if (a.name && b.name) { + return compare(a.name.toUpperCase(), b.name.toUpperCase()) * accessKeySortDirection; + } else if (a.name) { + return -1; + } else if (b.name) { + return 1; + } else { + return 0; + } + }; + } + + destroyServer() { + this.dispatchEvent(makePublicEvent('DeleteServerRequested', {serverId: this.serverId})); + } + + removeServer() { + this.dispatchEvent(makePublicEvent('ForgetServerRequested', {serverId: this.serverId})); + } + + _isServerManaged(cloudId: string) { + return !!cloudId; + } + + _activeDataLimitForKey(accessKey?: DisplayAccessKey): number { + if (!accessKey) { + // We're in app startup + return null; + } + + if (exists(accessKey.dataLimitBytes)) { + return accessKey.dataLimitBytes; + } + + return this.isDefaultDataLimitEnabled ? this.defaultDataLimitBytes : null; + } + + _computePaperProgressClass(accessKey: DisplayAccessKey) { + return exists(this._activeDataLimitForKey(accessKey)) ? 'data-limits' : ''; + } + + _getRelevantTransferAmountForKey(accessKey: DisplayAccessKey) { + if (!accessKey) { + // We're in app startup + return null; + } + const activeLimit = this._activeDataLimitForKey(accessKey); + return exists(activeLimit) ? activeLimit : accessKey.transferredBytes; + } + + _computeProgressWidthStyling(accessKey: DisplayAccessKey, baselineDataTransfer: number) { + const relativeTransfer = this._getRelevantTransferAmountForKey(accessKey); + const width = Math.floor((progressBarMaxWidthPx * relativeTransfer) / baselineDataTransfer); + // It's important that there's no space in between width and "px" in order for Chrome to accept + // the inline style string. + return `width: ${width}px;`; + } + + _getDataLimitsUsageString(accessKey: DisplayAccessKey, language: string, localize: Function) { + if (!accessKey) { + // We're in app startup + return ''; + } + + const activeDataLimit = this._activeDataLimitForKey(accessKey); + const used = this._formatBytesTransferred(accessKey.transferredBytes, language, '0'); + const total = this._formatDisplayDataLimit(activeDataLimit, language, localize); + return localize('data-limits-usage', 'used', used, 'total', total); + } +} + +customElements.define(ServerView.is, ServerView); diff --git a/src/server_manager/web_app/ui_components/outline-share-dialog.ts b/src/server_manager/web_app/ui_components/outline-share-dialog.ts new file mode 100644 index 000000000..f806acf8e --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-share-dialog.ts @@ -0,0 +1,178 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/paper-dialog/paper-dialog'; + +import '@polymer/paper-dialog-scrollable/paper-dialog-scrollable'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; +import * as clipboard from 'clipboard-polyfill'; + +export interface OutlineShareDialog extends Element { + open(accessKey: string, s3url: string): void; +} + +// TODO(alalama): add a language selector. This should be a separate instance of +// Polymer.AppLocalizeBehavior so the app language is not changed. Consider refactoring l10n into a +// separate Polymer behavior. +Polymer({ + _template: html` + + + +
+

[[localize('share-title')]]

+

+
+
+ +
+ [[localize('share-invite-copy')]] +
+ + + +
+ [[localize('share-invite-copy-access-key')]] + [[localize('done')]] +
+
+ `, + + is: 'outline-share-dialog', + + properties: { + localize: {type: Function}, + }, + + open(accessKey: string, s3Url: string) { + this.accessKey = accessKey; + this.s3Url = s3Url; + this.$.copyInvitationIndicator.setAttribute('hidden', true); + this.$.copyAccessKeyIndicator.setAttribute('hidden', true); + this.$.dialog.open(); + }, + + copyInvite() { + const dt = new clipboard.DT(); + dt.setData('text/plain', this.$.selectableInviteText.innerText); + dt.setData('text/html', this.$.selectableInviteText.innerHTML); + clipboard.write(dt); + this.$.copyInvitationIndicator.removeAttribute('hidden'); + }, + + copyAccessKey() { + const dt = new clipboard.DT(); + dt.setData('text/plain', this.$.selectableAccessKey.innerText); + dt.setData('text/html', this.$.selectableAccessKey.innerHTML); + clipboard.write(dt); + this.$.copyAccessKeyIndicator.removeAttribute('hidden'); + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-sort-span.ts b/src/server_manager/web_app/ui_components/outline-sort-span.ts new file mode 100644 index 000000000..46ef01bca --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-sort-span.ts @@ -0,0 +1,47 @@ +// Copyright 2020 The Outline Authors +// +// 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. + +import '@polymer/iron-icon/iron-icon'; +import '@polymer/iron-icons/iron-icons'; + +import {css, html, LitElement, PropertyDeclarations} from 'lit-element'; + +export class SortSpan extends LitElement { + static get styles() { + return css` + :host { + display: inline-block; + cursor: pointer; + user-select: none; + } + `; + } + static get properties(): PropertyDeclarations { + return {direction: {type: Number}}; + } + + direction: -1 | 0 | 1 = 0; + + override render() { + let arrow = html``; + if (this.direction === -1) { + arrow = html``; + } else if (this.direction === 1) { + arrow = html``; + } + return html`${arrow}`; + } +} + +customElements.define('outline-sort-span', SortSpan); diff --git a/src/server_manager/web_app/ui_components/outline-step-view.ts b/src/server_manager/web_app/ui_components/outline-step-view.ts new file mode 100644 index 000000000..10a56bb83 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-step-view.ts @@ -0,0 +1,80 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import '@polymer/polymer/polymer-legacy'; + +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; +Polymer({ + _template: html` + +
+
+
+
+
+
+
+
+ +
+ `, + + is: 'outline-step-view', + + properties: { + displayAction: { + type: Boolean, + value: false, + }, + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-survey-dialog.ts b/src/server_manager/web_app/ui_components/outline-survey-dialog.ts new file mode 100644 index 000000000..ccaf11423 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-survey-dialog.ts @@ -0,0 +1,131 @@ +/* + Copyright 2020 The Outline Authors + + 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. +*/ +import '@polymer/neon-animation/animations/slide-down-animation'; +import '@polymer/neon-animation/animations/slide-from-bottom-animation'; +import '@polymer/paper-button/paper-button'; +import '@polymer/paper-dialog/paper-dialog'; + +import {DirMixin} from '@polymer/polymer/lib/mixins/dir-mixin'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; +import {PolymerElement} from '@polymer/polymer/polymer-element'; + +import type {PolymerElementProperties} from '@polymer/polymer/interfaces'; +import type {PaperDialogElement} from '@polymer/paper-dialog/paper-dialog'; + +class OutlineSurveyDialog extends DirMixin(PolymerElement) { + static get template() { + return html` + + + +
+ +
+

[[title]]

+
+ + [[localize('survey-decline')]] + + + [[localize('survey-go-to-survey')]] + +
+
+
+
+
+

[[localize('survey-disclaimer')]]

+
+
+ `; + } + + static get is() { + return 'outline-survey-dialog'; + } + + static get properties(): PolymerElementProperties { + return { + localize: Function, + surveyLink: String, + title: String, + }; + } + + surveyLink: string; + + open(title: string, surveyLink: string) { + this.title = title; + this.surveyLink = surveyLink; + const dialog = this.$.dialog as PaperDialogElement; + dialog.horizontalAlign = this.dir === 'ltr' ? 'left' : 'right'; + dialog.open(); + } +} +customElements.define(OutlineSurveyDialog.is, OutlineSurveyDialog); diff --git a/src/server_manager/web_app/ui_components/outline-tos-view.ts b/src/server_manager/web_app/ui_components/outline-tos-view.ts new file mode 100644 index 000000000..454f39ff9 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-tos-view.ts @@ -0,0 +1,80 @@ +/* + Copyright 2018 The Outline Authors + + 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. +*/ +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; + +import {html} from '@polymer/polymer/lib/utils/html-tag'; +Polymer({ + _template: html` + + +
+ +
+
+ + [[localize('okay')]] +
+ `, + + is: 'outline-tos-view', + + properties: { + hasAcceptedTermsOfService: { + type: Boolean, + value: false, + notify: true, + }, + localize: { + type: Function, + }, + }, + + acceptTermsOfService() { + this.hasAcceptedTermsOfService = true; + }, +}); diff --git a/src/server_manager/web_app/ui_components/outline-validated-input.ts b/src/server_manager/web_app/ui_components/outline-validated-input.ts new file mode 100644 index 000000000..d78935066 --- /dev/null +++ b/src/server_manager/web_app/ui_components/outline-validated-input.ts @@ -0,0 +1,222 @@ +/* + Copyright 2020 The Outline Authors + + 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. +*/ +import './cloud-install-styles'; + +import './outline-server-settings-styles'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn'; +import {html} from '@polymer/polymer/lib/utils/html-tag'; + +// outline-validated-input +// This is an input, with a cancel and a save button, which performs client-side validation and has +// an event-based hook for server-side evaluation. +// +// Attributes: +// * editable: Is this input editable? Default: false +// * visible: Is this input visible? Default: false +// * label: The label for the input. Default: null +// * value: The initial value entered in the input. SHOULD be a valid value. Default: null +// * allowed-pattern: Regex describing what inputs are allowed. Users will be prevented from +// entering inputs which don't follow the pattern. Default: ".*" +// * max-length: The number of characters allowed in the input. Default: Number.POSITIVE_INFINITY +// * client-side-validator: A function which takes a string and returns either an empty string on +// success or an error message on failure. This function will be called on every keystroke. +// Default: () => "" +// * event: The name of the event fired when the save button is tapped. Passes the current input +// value as "value" and the Polymer object as "ui". The handler for this event MUST call +// ui.setSavedState() or ui.setErrorState(message) depending on its result. Default: null +Polymer({ + _template: html` + + + +
+ + + + + [[localize('cancel')]] + + + [[localize('save')]] + +
+ `, + + is: 'outline-validated-input', + + properties: { + // Properties affecting the whole element + editable: {type: Boolean, value: false}, + visible: {type: Boolean, value: false}, + // Properties affecting the input + label: {type: String, value: null}, + allowedPattern: {type: String, value: '.*'}, + maxLength: {type: Number, value: Number.POSITIVE_INFINITY}, + value: {type: String, value: null}, + // `value` here is evaluated. If it were simply `() => "`, then clientSideValidator + // would default to just "", not a function returning "". + // Note also that we can't use paper-input's validator attribute because it will fight + // with any server-side validation and cause unpredictable results. + clientSideValidator: { + type: Function, + value: () => { + return () => ''; + }, + }, + // Properties affecting the buttons + _showButtons: {type: Boolean, value: false}, + _enableButtons: {type: Boolean, value: false}, + // Other properties + event: {type: String, value: null}, + localize: {type: Function}, + }, + + _onKeyUp(e: KeyboardEvent) { + const input = this.$.input; + if (e.key === 'Escape') { + this._cancel(); + input.blur(); + return; + } else if (e.key === 'Enter') { + if (!input.invalid) { + this._save(); + input.blur(); + } + return; + } + const validationError = this.clientSideValidator(input.value); + if (validationError) { + input.invalid = true; + this.$.saveButton.disabled = true; + input.errorMessage = validationError; + } else { + input.invalid = false; + this.$.saveButton.disabled = false; + } + }, + + _cancel() { + const input = this.$.input; + input.value = this.value; + input.invalid = false; + this._showButtons = false; + }, + + _save() { + const input = this.$.input; + const value = input.value; + if (value === this.value) { + this._cancel(); + return; + } + this.$.cancelButton.disabled = true; + this.$.saveButton.disabled = true; + input.readonly = true; + input.invalid = false; + + // We use this heuristic to avoid having to pass a constructor as an attribute + const numberValue = Number(value); + const typedValue = Number.isNaN(numberValue) ? value : numberValue; + + this.fire(this.event, { + validatedInput: typedValue, + ui: this, + }); + }, + + _enterEditingState() { + if (!this.editable) { + return; + } + this._showButtons = true; + this.$.cancelButton.disabled = false; + this.$.saveButton.disabled = this.$.input.invalid; + }, + + enterSavedState() { + const input = this.$.input; + this.value = input.value; + this._showButtons = false; + input.readonly = false; + }, + + enterErrorState(message: string) { + const input = this.$.input; + this._enableButtons = true; + input.errorMessage = message; + input.invalid = true; + input.readonly = false; + input.focus(); + }, +}); diff --git a/src/server_manager/web_app/ui_components/style.css b/src/server_manager/web_app/ui_components/style.css new file mode 100644 index 000000000..3b6a2381f --- /dev/null +++ b/src/server_manager/web_app/ui_components/style.css @@ -0,0 +1,144 @@ +/* + * Copyright 2018 The Outline Authors + * + * 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. + */ + +/* Copy of style.css with changes for new Cloud Installation Page */ + +/* If Polymer fails to load, this prevents the styles in elements + from being erroneously displayed to the user. */ +core-style { + display: none; +} + +paper-dropdown { + box-shadow: 0px 0px 20px #999999; +} + +/* Show gray background when a dialog is opened */ +core-overlay-layer.core-opened { + background-color: rgba(0, 0, 0, 0.5); + width: 100%; + height: 100%; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; + background-color: #263238; +} + +body { + font-family: "Roboto", sans-serif; + font-size: 14px; + font-weight: 400; + color: rgba(0, 0, 0, 0.54); + -webkit-font-smoothing: antialiased; +} + +div#wrapper { + min-height: 100%; + display: flex; + display: -webkit-flex; + flex-direction: column; + -webkit-flex-direction: column; +} + +section:not(#header):not(:last-of-type) { + border-bottom: 1px solid #eee; + padding: 100px 40px; +} + +@media screen and (max-width: 630px) { + section:not(#header):not(:last-of-type) { + padding: 70px 40px; + } +} + +section:last-of-type { + border-bottom: none; + padding: 100px 40px 0; +} + +/* headings */ + +h1, +h2, +h3, +h4 { + color: rgba(0, 0, 0, 0.87); + line-height: 1.5em; +} + +h1 { + font-size: 40px; + padding-top: 12px; + margin-bottom: 17px; +} + +h2 { + font-weight: 500; + font-size: 16px; +} + +h3 { + font-weight: 400; + line-height: 28px; + font-size: 16px; + margin: 0px 0px 14px 0px; + padding: 14px 12% 32px 12%; +} + +h3 span { + font-weight: 500; +} + +h4 { + font-size: 14px; + font-weight: 500; +} + +/* normal text and links */ +p { + color: var(--medium-gray); + font-size: 14px; + line-height: 1.5em; +} + +a { + text-decoration: none; +} + +a:hover { + color: #00ac9b; +} + +p a { + color: #808080; +} + +/* pre */ +pre { + font-family: monospace; +} + +paper-menu { + white-space: nowrap; +} + +paper-progress { + border-radius: 2px; +} diff --git a/src/shadowbox/CHANGELOG.md b/src/shadowbox/CHANGELOG.md new file mode 100644 index 000000000..af5a38eb9 --- /dev/null +++ b/src/shadowbox/CHANGELOG.md @@ -0,0 +1,20 @@ +# 1.7.2 +- Fixes + - Fix reporting of country metrics and improve logging output (https://github.com/Jigsaw-Code/outline-server/pull/1242) + +# 1.7.1 +- Fixes + - Corner case of isPortUsed that could result in infinite restart loop (https://github.com/Jigsaw-Code/outline-server/pull/1238) + - Prevent excessive logging (https://github.com/Jigsaw-Code/outline-server/pull/1232) + +# 1.7.0 + +- Features + - Add encryption cipher selection to create access key API (https://github.com/Jigsaw-Code/outline-server/pull/1002) + - Make access key secrets longer (https://github.com/Jigsaw-Code/outline-server/pull/1098) +- Fixes + - Race condition on concurrent API calls (https://github.com/Jigsaw-Code/outline-server/pull/995) +- Upgrades (https://github.com/Jigsaw-Code/outline-server/pull/1211) + - Base image to `node:16.18.0-alpine3.16` + - outline-ss-server from 1.3.5 to [1.4.0](https://github.com/Jigsaw-Code/outline-ss-server/releases/tag/v1.4.0) + - Prometheus from 2.33.5 to [2.37.1](https://github.com/prometheus/prometheus/releases/tag/v2.37.1) diff --git a/src/shadowbox/README.md b/src/shadowbox/README.md new file mode 100644 index 000000000..189f9f275 --- /dev/null +++ b/src/shadowbox/README.md @@ -0,0 +1,204 @@ +# Outline Server + +The internal name for the Outline server is "Shadowbox". It is a server set up +that runs a user management API and starts Shadowsocks instances on demand. + +It aims to make it as easy as possible to set up and share a Shadowsocks +server. It's managed by the Outline Manager and used as proxy by the Outline +client apps. Shadowbox is also compatible with standard Shadowsocks clients. + +## Self-hosted installation + +To install and run Shadowbox on your own server, run + +``` +sudo bash -c "$(wget -qO- https://raw.githubusercontent.com/Jigsaw-Code/outline-server/master/src/server_manager/install_scripts/install_server.sh)" +``` + +You can specify flags to customize the installation. For example, to use hostname `myserver.com` and the port 443 for access keys, you can run: + +``` +sudo bash -c "$(wget -qO- https://raw.githubusercontent.com/Jigsaw-Code/outline-server/master/src/server_manager/install_scripts/install_server.sh)" install_server.sh --hostname=myserver.com --keys-port=443 +``` + +Use `sudo --preserve-env` if you need to pass environment variables. Use `bash -x` if you need to debug the installation. + +## Running from source code + +### Prerequisites + +Shadowbox supports running on linux and macOS hosts. + +Besides [Node](https://nodejs.org/en/download/) you will also need: + +1. [Docker 1.13+](https://docs.docker.com/engine/installation/) +2. [docker-compose 1.11+](https://docs.docker.com/compose/install/) + +### Running Shadowbox as a Node.js app + +Build and run the server as a Node.js app: + +``` +npm run action shadowbox/server/start +``` + +The output will be at `build/shadowbox/app`. + +### Running Shadowbox as a Docker container + +### With docker command + +Build the image and run server: + +``` +npm run action shadowbox/docker/start +``` + +You should be able to successfully query the management API: + +``` +curl --insecure https://[::]:8081/TestApiPrefix/server +``` + +To build the image only: + +``` +npm run action shadowbox/docker/build +``` + +Debug image: + +``` +docker run --rm -it --entrypoint=sh outline/shadowbox +``` + +Or a running container: + +``` +docker exec -it shadowbox sh +``` + +Delete dangling images: + +``` +docker rmi $(docker images -f dangling=true -q) +``` + +## Access Keys Management API + +In order to utilize the Management API, you'll need to know the apiUrl for your Outline server. +You can obtain this information from the "Settings" tab of the server page in the Outline Manager. +Alternatively, you can check the 'access.txt' file under the '/opt/outline' directory of an Outline server. An example apiUrl is: https://1.2.3.4:1234/3pQ4jf6qSr5WVeMO0XOo4z. + +See [Full API Documentation](https://redocly.github.io/redoc/?url=https://raw.githubusercontent.com/Jigsaw-Code/outline-server/master/src/shadowbox/server/api.yml). +The OpenAPI specification can be found at [api.yml](./server/api.yml). + +### Examples + +Start by storing the apiURL you see see in that file, as a variable. For example: + +``` +API_URL=https://1.2.3.4:1234/3pQ4jf6qSr5WVeMO0XOo4z +``` + +You can then perform the following operations on the server, remotely. + +List access keys + +``` +curl --insecure $API_URL/access-keys/ +``` + +Create an access key + +``` +curl --insecure -X POST $API_URL/access-keys +``` + +Get an access key (e.g. get access key 1) + +``` +curl --insecure $API_URL/access-keys/1 +``` + +Rename an access key +(e.g. rename access key 2 to 'albion') + +``` +curl --insecure -X PUT -F 'name=albion' $API_URL/access-keys/2/name +``` + +Remove an access key +(e.g. remove access key 2) + +``` +curl --insecure -X DELETE $API_URL/access-keys/2 +``` + +Set a data limit for all access keys +(e.g. limit outbound data transfer access keys to 1MB over 30 days) + +``` +curl -v --insecure -X PUT -H "Content-Type: application/json" -d '{"limit": {"bytes": 1000}}' $API_URL/experimental/access-key-data-limit +``` + +Remove the access key data limit + +``` +curl -v --insecure -X DELETE $API_URL/experimental/access-key-data-limit +``` + +## Testing + +### Manual + +After building a docker image with some local changes, +upload it to your favorite registry +(e.g. Docker Hub, quay.io, etc.). + +Then set your `SB_IMAGE` environment variable to point to the image you just +uploaded (e.g. `export SB_IMAGE=yourdockerhubusername/shadowbox`) and +run `npm run action server_manager/electron_app/start` and your droplet should be created with your +modified image. + +### Automated + +To run the integration test: + +``` +npm run action shadowbox/integration_test/start +``` + +This will set up three containers and two networks: + +``` +client <-> shadowbox <-> target +``` + +`client` can only access `target` via shadowbox. We create a user on `shadowbox` then connect using the Shadowsocks client. + +To test clients that rely on fetching a docker image from Dockerhub, you can push an image to your account and modify the +client to use your image. To push your own image: + +``` +npm run action shadowbox/docker/build && docker tag quay.io/outline/shadowbox $USER/shadowbox && docker push $USER/shadowbox +``` + +If you need to test an unsigned image (e.g. your dev one): + +``` +DOCKER_CONTENT_TRUST=0 SB_IMAGE=$USER/shadowbox npm run action shadowbox/integration_test/start +``` + +You can add tags if you need different versions in different clients. + +### Testing Changes to the Server Config + +If your change includes new fields in the server config which are needed at server +start-up time, then you mey need to remove the pre-existing test config: + +``` +rm /tmp/outline/persisted-state/shadowbox_server_config.json +``` + +This will warn about deleting a write-protected file, which is okay to ignore. You will then need to hand-edit the JSON string in src/shadowbox/docker/start.action.sh. diff --git a/src/shadowbox/docker/Dockerfile b/src/shadowbox/docker/Dockerfile new file mode 100644 index 000000000..955506c13 --- /dev/null +++ b/src/shadowbox/docker/Dockerfile @@ -0,0 +1,71 @@ +# Copyright 2018 The Outline Authors +# +# 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. + +ARG NODE_IMAGE + +# Multi-stage build: use a build image to prevent bloating the shadowbox image with dependencies. +# Run `npm ci` and build inside the container to package the right dependencies for the image. +FROM ${NODE_IMAGE} AS build + +# make for building third_party/prometheus and perl-utils for shasum. +RUN apk add --no-cache --upgrade bash make perl-utils +WORKDIR / + +# Don't copy node_modules and other things not needed for install. +COPY package.json package-lock.json ./ +COPY src/shadowbox/package.json src/shadowbox/ +RUN npm ci + +# We copy the source code only after npm ci, so that source code changes don't trigger re-installs. +COPY scripts scripts/ +COPY src src/ +COPY tsconfig.json ./ +COPY third_party third_party + +ARG ARCH + +RUN ARCH=${ARCH} ROOT_DIR=/ npm run action shadowbox/server/build + +# shadowbox image +FROM ${NODE_IMAGE} + +# Save metadata on the software versions we are using. +LABEL shadowbox.node_version=16.18.0 + +ARG GITHUB_RELEASE +LABEL shadowbox.github.release="${GITHUB_RELEASE}" + +# We use curl to detect the server's public IP. We need to use the --date option in `date` to +# safely grab the ip-to-country database +RUN apk add --no-cache --upgrade coreutils curl + +COPY src/shadowbox/scripts scripts/ +COPY src/shadowbox/scripts/update_mmdb.sh /etc/periodic/weekly/update_mmdb + +RUN /etc/periodic/weekly/update_mmdb + +# Create default state directory. +RUN mkdir -p /root/shadowbox/persisted-state + +# Install shadowbox. +WORKDIR /opt/outline-server + +# The shadowbox build directory has the following structure: +# - app/ (bundled node app) +# - bin/ (binary dependencies) +# - package.json (shadowbox package.json) +COPY --from=build /build/shadowbox/ . + +COPY src/shadowbox/docker/cmd.sh / +CMD /cmd.sh diff --git a/src/shadowbox/docker/build.action.sh b/src/shadowbox/docker/build.action.sh new file mode 100755 index 000000000..93fc6bd30 --- /dev/null +++ b/src/shadowbox/docker/build.action.sh @@ -0,0 +1,48 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +export DOCKER_CONTENT_TRUST="${DOCKER_CONTENT_TRUST:-1}" +# Enable Docker BuildKit (https://docs.docker.com/develop/develop-images/build_enhancements) +export DOCKER_BUILDKIT=1 + +# Docker image build architecture. Supported architectures: x86_64, arm64 +export ARCH=${ARCH:-x86_64} + +# Newer node images have no valid content trust data. +# Pin the image node:16.18.0-alpine3.16 by hash. +# See image at https://hub.docker.com/_/node/tags?page=1&name=18.18.0-alpine3.18 +readonly NODE_IMAGE=$( + if [[ "${ARCH}" == "x86_64" ]]; then + echo "node@sha256:a0b787b0d53feacfa6d606fb555e0dbfebab30573277f1fe25148b05b66fa097" + elif [[ "${ARCH}" == "arm64" ]]; then + echo "node@sha256:b4b7a1dd149c65ee6025956ac065a843b4409a62068bd2b0cbafbb30ca2fab3b" + else + echo "Unsupported architecture" + exit 1 + fi +) + +# Doing an explicit `docker pull` of the container base image to work around an issue where +# Travis fails to pull the base image when using BuildKit. Seems to be related to: +# https://github.com/moby/buildkit/issues/606 and https://github.com/moby/buildkit/issues/1397 +docker pull "${NODE_IMAGE}" +docker build --force-rm \ + --build-arg ARCH="${ARCH}" \ + --build-arg NODE_IMAGE="${NODE_IMAGE}" \ + --build-arg GITHUB_RELEASE="${TRAVIS_TAG:-none}" \ + -f src/shadowbox/docker/Dockerfile \ + -t "${SB_IMAGE:-outline/shadowbox}" \ + "${ROOT_DIR}" diff --git a/src/shadowbox/docker/cmd.sh b/src/shadowbox/docker/cmd.sh new file mode 100755 index 000000000..faa8d40cb --- /dev/null +++ b/src/shadowbox/docker/cmd.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# +# Copyright 2018 The Outline Authors +# +# 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. + +export SB_PUBLIC_IP="${SB_PUBLIC_IP:-$(curl --silent https://ipinfo.io/ip)}" +export SB_METRICS_URL="${SB_METRICS_URL:-https://prod.metrics.getoutline.org}" + +# Make sure we don't leak readable files to other users. +umask 0007 + +# Start cron, which is used to check for updates to the IP-to-country database +crond + +node app/main.js diff --git a/src/shadowbox/docker/start.action.sh b/src/shadowbox/docker/start.action.sh new file mode 100755 index 000000000..2be9048d4 --- /dev/null +++ b/src/shadowbox/docker/start.action.sh @@ -0,0 +1,60 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +run_action shadowbox/docker/build + +RUN_ID="${RUN_ID:-$(date +%Y-%m-%d-%H%M%S)}" +readonly RUN_ID +readonly RUN_DIR="/tmp/outline/${RUN_ID}" +echo "Using directory ${RUN_DIR}" + +readonly HOST_STATE_DIR="${RUN_DIR}/persisted-state" +readonly CONTAINER_STATE_DIR='/root/shadowbox/persisted-state' +readonly STATE_CONFIG="${HOST_STATE_DIR}/shadowbox_server_config.json" + +declare -ir ACCESS_KEY_PORT=${ACCESS_KEY_PORT:-9999} +declare -ir SB_API_PORT=${SB_API_PORT:-8081} + +[[ -d "${HOST_STATE_DIR}" ]] || mkdir -p "${HOST_STATE_DIR}" +[[ -e "${STATE_CONFIG}" ]] || echo "{\"hostname\":\"127.0.0.1\", \"portForNewAccessKeys\": ${ACCESS_KEY_PORT}}" > "${STATE_CONFIG}" +# shellcheck source=../scripts/make_test_certificate.sh +source "${ROOT_DIR}/src/shadowbox/scripts/make_test_certificate.sh" "${RUN_DIR}" + +# TODO: mount a folder rather than individual files. +declare -ar docker_bindings=( + -v "${HOST_STATE_DIR}:${CONTAINER_STATE_DIR}" + -e "SB_STATE_DIR=${CONTAINER_STATE_DIR}" + -v "${SB_CERTIFICATE_FILE}:${SB_CERTIFICATE_FILE}" + -v "${SB_PRIVATE_KEY_FILE}:${SB_PRIVATE_KEY_FILE}" + -e "LOG_LEVEL=${LOG_LEVEL:-debug}" + -e "SB_API_PORT=${SB_API_PORT}" + -e "SB_API_PREFIX=TestApiPrefix" + -e "SB_CERTIFICATE_FILE=${SB_CERTIFICATE_FILE}" + -e "SB_PRIVATE_KEY_FILE=${SB_PRIVATE_KEY_FILE}" + -e "SB_METRICS_URL=${SB_METRICS_URL:-https://dev.metrics.getoutline.org}" +) + +readonly IMAGE="${SB_IMAGE:-outline/shadowbox}" +echo "Running image ${IMAGE}" + +declare -a NET_BINDINGS=("--network=host") +if [[ "$(uname)" == "Darwin" ]]; then + # Docker does not support the --network=host option on macOS. Instead, publish the management API + # and access key ports to the host. + NET_BINDINGS=(-p "${SB_API_PORT}:${SB_API_PORT}" -p "${ACCESS_KEY_PORT}:${ACCESS_KEY_PORT}" -p "${ACCESS_KEY_PORT}:${ACCESS_KEY_PORT}/udp") +fi; + +docker run --rm -it "${NET_BINDINGS[@]}" --name shadowbox "${docker_bindings[@]}" "${IMAGE}" diff --git a/src/shadowbox/infrastructure/clock.ts b/src/shadowbox/infrastructure/clock.ts new file mode 100644 index 000000000..e4d34abad --- /dev/null +++ b/src/shadowbox/infrastructure/clock.ts @@ -0,0 +1,50 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +export interface Clock { + // Returns the current time in milliseconds from the epoch. + now(): number; + setInterval(callback: () => void, intervalMs: number): void; +} + +export class RealClock implements Clock { + now(): number { + return Date.now(); + } + + setInterval(callback, intervalMs: number): void { + setInterval(callback, intervalMs); + } +} + +// Fake clock where you manually set what is "now" and can trigger the scheduled callbacks. +// Useful for tests. +export class ManualClock implements Clock { + public nowMs = 0; + private callbacks = [] as (() => void)[]; + + now(): number { + return this.nowMs; + } + + setInterval(callback, _intervalMs): void { + this.callbacks.push(callback); + } + + async runCallbacks() { + for (const callback of this.callbacks) { + await callback(); + } + } +} diff --git a/src/shadowbox/infrastructure/file.spec.ts b/src/shadowbox/infrastructure/file.spec.ts new file mode 100644 index 000000000..bb8b773ed --- /dev/null +++ b/src/shadowbox/infrastructure/file.spec.ts @@ -0,0 +1,67 @@ +import * as fs from 'fs'; +import * as tmp from 'tmp'; +import * as file from './file'; + +describe('file', () => { + tmp.setGracefulCleanup(); + + describe('readFileIfExists', () => { + let tmpFile: tmp.FileResult; + + beforeEach(() => (tmpFile = tmp.fileSync())); + + it('reads the file if it exists', () => { + const contents = 'test'; + + fs.writeFileSync(tmpFile.name, contents); + + expect(file.readFileIfExists(tmpFile.name)).toBe(contents); + }); + + it('reads the file if it exists and is empty', () => { + fs.writeFileSync(tmpFile.name, ''); + + expect(file.readFileIfExists(tmpFile.name)).toBe(''); + }); + + it("returns null if file doesn't exist", () => + expect(file.readFileIfExists(tmp.tmpNameSync())).toBe(null)); + }); + + describe('atomicWriteFileSync', () => { + let tmpFile: tmp.FileResult; + + beforeEach(() => (tmpFile = tmp.fileSync())); + + it('writes to the file', () => { + const contents = 'test'; + + file.atomicWriteFileSync(tmpFile.name, contents); + + expect(fs.readFileSync(tmpFile.name, {encoding: 'utf8'})).toEqual(contents); + }); + + it('supports multiple simultaneous writes to the same file', async () => { + const writeCount = 100; + + const writer = (_, id) => + new Promise((resolve, reject) => { + try { + file.atomicWriteFileSync( + tmpFile.name, + `${fs.readFileSync(tmpFile.name, {encoding: 'utf-8'})}${id}\n` + ); + resolve(); + } catch (e) { + reject(e); + } + }); + + await Promise.all(Array.from({length: writeCount}, writer)); + + expect(fs.readFileSync(tmpFile.name, {encoding: 'utf8'}).trimEnd().split('\n').length).toBe( + writeCount + ); + }); + }); +}); diff --git a/src/shadowbox/infrastructure/file.ts b/src/shadowbox/infrastructure/file.ts new file mode 100644 index 000000000..665bf8906 --- /dev/null +++ b/src/shadowbox/infrastructure/file.ts @@ -0,0 +1,40 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as fs from 'fs'; + +// Reads a text file if it exists, or null if the file is not found. +// Throws any other error except file not found. +export function readFileIfExists(filename: string): string { + try { + return fs.readFileSync(filename, {encoding: 'utf8'}) ?? null; + } catch (err) { + // err.code will be 'ENOENT' if the file is not found, this is expected. + if (err.code === 'ENOENT') { + return null; + } else { + throw err; + } + } +} + +// Write to temporary file, then move that temporary file to the +// persistent location, to avoid accidentally breaking the metrics file. +// Use *Sync calls for atomic operations, to guard against corrupting +// these files. +export function atomicWriteFileSync(filename: string, filebody: string) { + const tempFilename = `${filename}.${Date.now()}`; + fs.writeFileSync(tempFilename, filebody, {encoding: 'utf8'}); + fs.renameSync(tempFilename, filename); +} diff --git a/src/shadowbox/infrastructure/filesystem_text_file.ts b/src/shadowbox/infrastructure/filesystem_text_file.ts new file mode 100644 index 000000000..5bea70166 --- /dev/null +++ b/src/shadowbox/infrastructure/filesystem_text_file.ts @@ -0,0 +1,30 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as fs from 'fs'; +import {TextFile} from './text_file'; + +// Reads a text file if it exists, or null if the file is not found. +// Throws any other error except file not found. +export class FilesystemTextFile implements TextFile { + constructor(private readonly filename: string) {} + + readFileSync(): string { + return fs.readFileSync(this.filename, {encoding: 'utf8'}); + } + + writeFileSync(text: string): void { + fs.writeFileSync(this.filename, text, {encoding: 'utf8'}); + } +} diff --git a/src/shadowbox/infrastructure/follow_redirects.ts b/src/shadowbox/infrastructure/follow_redirects.ts new file mode 100644 index 000000000..af344f9d9 --- /dev/null +++ b/src/shadowbox/infrastructure/follow_redirects.ts @@ -0,0 +1,41 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import fetch, {RequestInit, Response} from 'node-fetch'; + +// Makes an http(s) request, and follows any redirect with the same request +// without changing the request method or body. This is used because typical +// http(s) clients follow redirects for POST/PUT/DELETE requests by changing the +// method to GET and removing the request body. The options parameter matches the +// fetch() function. +export async function requestFollowRedirectsWithSameMethodAndBody( + url: string, + options: RequestInit +): Promise { + // Make a copy of options to modify parameters. + const manualRedirectOptions = { + ...options, + redirect: 'manual' as RequestRedirect, + }; + let response: Response; + for (let i = 0; i < 10; i++) { + response = await fetch(url, manualRedirectOptions); + if (response.status >= 300 && response.status < 400) { + url = response.headers.get('location'); + } else { + break; + } + } + return response; +} diff --git a/src/shadowbox/infrastructure/get_port.spec.ts b/src/shadowbox/infrastructure/get_port.spec.ts new file mode 100644 index 000000000..725262504 --- /dev/null +++ b/src/shadowbox/infrastructure/get_port.spec.ts @@ -0,0 +1,103 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as net from 'net'; + +import * as get_port from './get_port'; + +describe('PortProvider', () => { + describe('addReservedPort', () => { + it('gets port over 1023', async () => { + expect(await new get_port.PortProvider().reserveNewPort()).toBeGreaterThan(1023); + }); + + it('fails on double reservation', () => { + const ports = new get_port.PortProvider(); + ports.addReservedPort(8080); + expect(() => ports.addReservedPort(8080)).toThrowError(); + }); + }); + + describe('reserveFirstFreePort', () => { + it('returns free port', async () => { + const ports = new get_port.PortProvider(); + const server = await listen(); + const initialPort = (server.address() as net.AddressInfo).port; + const reservedPort = await ports.reserveFirstFreePort(initialPort); + await closeServer(server); + expect(reservedPort).toBeGreaterThan(initialPort); + }); + + it('respects reserved ports', async () => { + const ports = new get_port.PortProvider(); + ports.addReservedPort(9090); + ports.addReservedPort(9091); + expect(await ports.reserveFirstFreePort(9090)).toBeGreaterThan(9091); + }); + }); + + describe('reserveNewPort', () => { + it('Returns a port not in use', async () => { + // We run 100 times to try to trigger possible race conditions. + for (let i = 0; i < 100; ++i) { + const port = await new get_port.PortProvider().reserveNewPort(); + expect(await get_port.isPortUsed(port)).toBeFalsy(); + } + }); + }); +}); + +describe('isPortUsed', () => { + it('Identifies a port in use on IPV4', async () => { + const port = 12345; + const server = new net.Server(); + const isPortUsed = await new Promise((resolve) => { + server.listen(port, '127.0.0.1', () => { + resolve(get_port.isPortUsed(port)); + }); + }); + await closeServer(server); + expect(isPortUsed).toBeTruthy(); + }); + it('Identifies a port in use on IPV6', async () => { + const port = 12345; + const server = new net.Server(); + const isPortUsed = await new Promise((resolve) => { + server.listen(port, '::1', () => { + resolve(get_port.isPortUsed(port)); + }); + }); + await closeServer(server); + expect(isPortUsed).toBeTruthy(); + }); + it('Identifies a port not in use', async () => { + const port = await new get_port.PortProvider().reserveNewPort(); + expect(await get_port.isPortUsed(port)).toBeFalsy(); + }); +}); + +function listen(): Promise { + const server = net.createServer(); + return new Promise((resolve, _reject) => { + server.listen({host: 'localhost', port: 0, exclusive: true}, () => { + resolve(server); + }); + }); +} + +function closeServer(server: net.Server): Promise { + return new Promise((resolve, reject) => { + server.close(err => err ? reject(err) : resolve()); + }); +} diff --git a/src/shadowbox/infrastructure/get_port.ts b/src/shadowbox/infrastructure/get_port.ts new file mode 100644 index 000000000..4b94c05c3 --- /dev/null +++ b/src/shadowbox/infrastructure/get_port.ts @@ -0,0 +1,86 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as net from 'net'; + +const MAX_PORT = 65535; +const MIN_PORT = 1024; + +export class PortProvider { + private reservedPorts = new Set(); + + addReservedPort(port: number) { + if (this.reservedPorts.has(port)) { + throw new Error(`Port ${port} is already reserved`); + } + this.reservedPorts.add(port); + } + + // Returns the first free port equal or after initialPort + async reserveFirstFreePort(initialPort: number): Promise { + for (let port = initialPort; port < 65536; port++) { + if (!this.reservedPorts.has(port) && !(await isPortUsed(port))) { + this.reservedPorts.add(port); + return port; + } + } + throw new Error('port not found'); + } + + async reserveNewPort(): Promise { + // TODO: consider using a set of available ports, so we don't randomly + // try the same port multiple times. + for (;;) { + const port = getRandomPortOver1023(); + if (this.reservedPorts.has(port)) { + continue; + } + if (await isPortUsed(port)) { + continue; + } + this.reservedPorts.add(port); + return port; + } + } +} + +function getRandomPortOver1023() { + return Math.floor(Math.random() * (MAX_PORT + 1 - MIN_PORT) + MIN_PORT); +} + +interface ServerError extends Error { + code: string; +} + +export function isPortUsed(port: number): Promise { + return new Promise((resolve, reject) => { + let isUsed = false; + const server = new net.Server(); + server.on('error', (error: ServerError) => { + if (error.code === 'EADDRINUSE') { + isUsed = true; + } else { + reject(error); + } + server.close(); + }); + server.listen({port, exclusive: true}, () => { + isUsed = false; + server.close(); + }); + server.on('close', () => { + resolve(isUsed); + }); + }); +} diff --git a/src/shadowbox/infrastructure/json_config.ts b/src/shadowbox/infrastructure/json_config.ts new file mode 100644 index 000000000..922e1a507 --- /dev/null +++ b/src/shadowbox/infrastructure/json_config.ts @@ -0,0 +1,104 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as file from './file'; +import * as logging from './logging'; + +export interface JsonConfig { + // Returns a reference (*not* a copy) to the json object backing the config. + data(): T; + // Writes the config to the backing storage. + write(); +} + +export function loadFileConfig(filename: string): JsonConfig { + const text = file.readFileIfExists(filename); + let dataJson = {} as T; + if (text) { + dataJson = JSON.parse(text) as T; + } + return new FileConfig(filename, dataJson); +} + +// FileConfig is a JsonConfig backed by a filesystem file. +export class FileConfig implements JsonConfig { + constructor(private filename: string, private dataJson: T) {} + + data(): T { + return this.dataJson; + } + + write() { + try { + file.atomicWriteFileSync(this.filename, JSON.stringify(this.dataJson)); + } catch (error) { + // TODO: Stop swallowing the exception and handle it in the callers. + logging.error(`Error writing config ${this.filename} ${error}`); + } + } +} + +// ChildConfig is a JsonConfig backed by another config. +export class ChildConfig implements JsonConfig { + constructor(private parentConfig: JsonConfig<{}>, private dataJson: T) {} + + data(): T { + return this.dataJson; + } + + write() { + this.parentConfig.write(); + } +} + +// DelayedConfig is a JsonConfig that only writes the data in a periodic time interval. +// Calls to write() will mark the data as "dirty" for the next inverval. +export class DelayedConfig implements JsonConfig { + private dirty = false; + constructor(private config: JsonConfig, writePeriodMs: number) { + // This repeated call will never be cancelled until the execution is terminated. + setInterval(() => { + if (!this.dirty) { + return; + } + this.config.write(); + this.dirty = false; + }, writePeriodMs); + } + + data(): T { + return this.config.data(); + } + + write() { + this.dirty = true; + } +} + +// InMemoryConfig is a JsonConfig backed by an internal member variable. Useful for testing. +export class InMemoryConfig implements JsonConfig { + // Holds the data JSON as it was when `write()` was called. + public mostRecentWrite: T; + constructor(private dataJson: T) { + this.mostRecentWrite = this.dataJson; + } + + data(): T { + return this.dataJson; + } + + write() { + this.mostRecentWrite = JSON.parse(JSON.stringify(this.dataJson)); + } +} diff --git a/src/shadowbox/infrastructure/logging.ts b/src/shadowbox/infrastructure/logging.ts new file mode 100644 index 000000000..a07aafe18 --- /dev/null +++ b/src/shadowbox/infrastructure/logging.ts @@ -0,0 +1,109 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as path from 'path'; + +interface Callsite { + getLineNumber(): number; + getFileName(): string; +} + +// Returns the Callsite object of the caller. +// This relies on the V8 stack trace API: https://github.com/v8/v8/wiki/Stack-Trace-API +function getCallsite(): Callsite { + const originalPrepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = (_, stack) => { + return stack; + }; + const error = new Error(); + Error.captureStackTrace(error, getCallsite); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stack = error.stack as any as Callsite[]; + Error.prepareStackTrace = originalPrepareStackTrace; + return stack[1]; +} + +// Possible values for the level prefix. +type LevelPrefix = 'E' | 'W' | 'I' | 'D'; + +// Formats the log message. Example: +// I2018-08-16T16:46:21.577Z 167288 main.js:86] ... +function makeLogMessage(level: LevelPrefix, callsite: Callsite, message: string): string { + // This creates a string in the UTC timezone + // See + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + const timeStr = new Date().toISOString(); + // TODO(alalama): preserve the source file structure in the webpack build so we can use + // `callsite.getFileName()`. + return `${level}${timeStr} ${process.pid} ${path.basename( + callsite.getFileName() || __filename + )}:${callsite.getLineNumber()}] ${message}`; +} + +export enum LogLevel { + // The order here is important, from less to more verbose. + ERROR, + WARNING, + INFO, + DEBUG, +} + +const maxMsgLevel = logLevelFromEnvironment(); + +function logLevelFromEnvironment(): LogLevel { + if (process.env.LOG_LEVEL) { + return parseLogLevel(process.env.LOG_LEVEL); + } + return LogLevel.INFO; +} + +function parseLogLevel(levelStr: string) { + switch(levelStr.toLowerCase()) { + case "error": + return LogLevel.ERROR; + case "warning": + case "warn": + return LogLevel.WARNING; + case "info": + return LogLevel.INFO; + case "debug": + return LogLevel.DEBUG; + default: + throw new Error(`Invalid log level "${levelStr}"`); + } +} + +export function error(message: string) { + if (LogLevel.ERROR <= maxMsgLevel) { + console.error(makeLogMessage('E', getCallsite(), message)); + } +} + +export function warn(message: string) { + if (LogLevel.WARNING <= maxMsgLevel) { + console.warn(makeLogMessage('W', getCallsite(), message)); + } +} + +export function info(message: string) { + if (LogLevel.INFO <= maxMsgLevel) { + console.info(makeLogMessage('I', getCallsite(), message)); + } +} + +export function debug(message: string) { + if (LogLevel.DEBUG <= maxMsgLevel) { + console.debug(makeLogMessage('D', getCallsite(), message)); + } +} diff --git a/src/shadowbox/infrastructure/prometheus_scraper.ts b/src/shadowbox/infrastructure/prometheus_scraper.ts new file mode 100644 index 000000000..aee0239a6 --- /dev/null +++ b/src/shadowbox/infrastructure/prometheus_scraper.ts @@ -0,0 +1,139 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as http from 'http'; +import * as jsyaml from 'js-yaml'; +import * as mkdirp from 'mkdirp'; +import * as path from 'path'; + +import * as logging from '../infrastructure/logging'; + +export interface QueryResultData { + resultType: 'matrix' | 'vector' | 'scalar' | 'string'; + result: Array<{ + metric: {[labelValue: string]: string}; + value: [number, string]; + }>; +} + +// From https://prometheus.io/docs/prometheus/latest/querying/api/ +interface QueryResult { + status: 'success' | 'error'; + data: QueryResultData; + errorType: string; + error: string; +} + +export class PrometheusClient { + constructor(private address: string) {} + + query(query: string): Promise { + return new Promise((fulfill, reject) => { + const url = `${this.address}/api/v1/query?query=${encodeURIComponent(query)}`; + http + .get(url, (response) => { + if (response.statusCode < 200 || response.statusCode > 299) { + reject(new Error(`Got error ${response.statusCode}`)); + response.resume(); + return; + } + let body = ''; + response.on('data', (data) => { + body += data; + }); + response.on('end', () => { + const result = JSON.parse(body) as QueryResult; + if (result.status !== 'success') { + return reject(new Error(`Error ${result.errorType}: ${result.error}`)); + } + fulfill(result.data); + }); + }) + .on('error', (e) => { + reject(new Error(`Failed to query prometheus API: ${e}`)); + }); + }); + } +} + +export async function startPrometheus( + binaryFilename: string, + configFilename: string, + configJson: {}, + processArgs: string[], + endpoint: string +) { + await writePrometheusConfigToDisk(configFilename, configJson); + await spawnPrometheusSubprocess(binaryFilename, processArgs, endpoint); +} + +async function writePrometheusConfigToDisk(configFilename: string, configJson: {}) { + await mkdirp.sync(path.dirname(configFilename)); + const ymlTxt = jsyaml.safeDump(configJson, {sortKeys: true}); + // Write the file asynchronously to prevent blocking the node thread. + await new Promise((resolve, reject) => { + fs.writeFile(configFilename, ymlTxt, 'utf-8', (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +async function spawnPrometheusSubprocess( + binaryFilename: string, + processArgs: string[], + prometheusEndpoint: string +): Promise { + logging.info('======== Starting Prometheus ========'); + logging.info(`${binaryFilename} ${processArgs.map(a => `"${a}"`).join(' ')}`); + const runProcess = child_process.spawn(binaryFilename, processArgs); + runProcess.on('error', (error) => { + logging.error(`Error spawning Prometheus: ${error}`); + }); + runProcess.on('exit', (code, signal) => { + logging.error(`Prometheus has exited with error. Code: ${code}, Signal: ${signal}`); + logging.error('Restarting Prometheus...'); + spawnPrometheusSubprocess(binaryFilename, processArgs, prometheusEndpoint); + }); + // TODO(fortuna): Consider saving the output and expose it through the manager service. + runProcess.stdout.pipe(process.stdout); + runProcess.stderr.pipe(process.stderr); + await waitForPrometheusReady(`${prometheusEndpoint}/api/v1/status/flags`); + logging.info('Prometheus is ready!'); + return runProcess; +} + +async function waitForPrometheusReady(prometheusEndpoint: string) { + while (!(await isHttpEndpointHealthy(prometheusEndpoint))) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } +} + +function isHttpEndpointHealthy(endpoint: string): Promise { + return new Promise((resolve, _) => { + http + .get(endpoint, (response) => { + resolve(response.statusCode >= 200 && response.statusCode < 300); + }) + .on('error', () => { + // Prometheus is not ready yet. + resolve(false); + }); + }); +} diff --git a/src/shadowbox/infrastructure/rollout.spec.ts b/src/shadowbox/infrastructure/rollout.spec.ts new file mode 100644 index 000000000..ab163d8ae --- /dev/null +++ b/src/shadowbox/infrastructure/rollout.spec.ts @@ -0,0 +1,52 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import {RolloutTracker} from './rollout'; + +describe('RolloutTracker', () => { + describe('isRolloutEnabled', () => { + it('throws on out of range percentages', () => { + const tracker = new RolloutTracker('instance-id'); + expect(() => tracker.isRolloutEnabled('rollout-id', -1)).toThrowError(); + expect(() => tracker.isRolloutEnabled('rollout-id', 101)).toThrowError(); + }); + it('throws on fractional percentage', () => { + const tracker = new RolloutTracker('instance-id'); + expect(() => tracker.isRolloutEnabled('rollout-id', 0.1)).toThrowError(); + expect(() => tracker.isRolloutEnabled('rollout-id', 50.1)).toThrowError(); + }); + it('returns false on 0%', () => { + const tracker = new RolloutTracker('instance-id'); + expect(tracker.isRolloutEnabled('rollout-id', 0)).toBeFalsy(); + }); + it('returns true on 100%', () => { + const tracker = new RolloutTracker('instance-id'); + expect(tracker.isRolloutEnabled('rollout-id', 100)).toBeTruthy(); + }); + it('returns true depending on percentage', () => { + const tracker = new RolloutTracker('instance-id'); + expect(tracker.isRolloutEnabled('rollout-id', 9)).toBeFalsy(); + expect(tracker.isRolloutEnabled('rollout-id', 10)).toBeTruthy(); + }); + }); + describe('forceRollout', () => { + it('forces rollout', () => { + const tracker = new RolloutTracker('instance-id'); + tracker.forceRollout('rollout-id', true); + expect(tracker.isRolloutEnabled('rollout-id', 0)).toBeTruthy(); + tracker.forceRollout('rollout-id', false); + expect(tracker.isRolloutEnabled('rollout-id', 100)).toBeFalsy(); + }); + }); +}); diff --git a/src/shadowbox/infrastructure/rollout.ts b/src/shadowbox/infrastructure/rollout.ts new file mode 100644 index 000000000..f5ac98f6d --- /dev/null +++ b/src/shadowbox/infrastructure/rollout.ts @@ -0,0 +1,47 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as crypto from 'crypto'; + +// Utility to help with partial rollouts of new features. +export class RolloutTracker { + private forcedRollouts = new Map(); + + constructor(private instanceId: string) {} + + // Forces a rollout to be enabled or disabled. + forceRollout(rolloutId: string, enabled: boolean) { + this.forcedRollouts.set(rolloutId, enabled); + } + + // Returns true if the given feature is rolled out for this instance. + // `percentage` is between 0 and 100 and represents the percentage of + // instances that should have the feature active. + isRolloutEnabled(rolloutId: string, percentage: number) { + if (this.forcedRollouts.has(rolloutId)) { + return this.forcedRollouts.get(rolloutId); + } + if (percentage < 0 || percentage > 100) { + throw new Error(`Expected 0 <= percentage <= 100. Found ${percentage}`); + } + if (Math.floor(percentage) !== percentage) { + throw new Error(`Expected percentage to be an integer. Found ${percentage}`); + } + const hash = crypto.createHash('md5'); + hash.update(this.instanceId); + hash.update(rolloutId); + const buffer = hash.digest(); + return 100 * buffer[0] < percentage * 256; + } +} diff --git a/src/shadowbox/infrastructure/text_file.ts b/src/shadowbox/infrastructure/text_file.ts new file mode 100644 index 000000000..67b3a85f8 --- /dev/null +++ b/src/shadowbox/infrastructure/text_file.ts @@ -0,0 +1,18 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +export interface TextFile { + readFileSync(): string; + writeFileSync(text: string): void; +} diff --git a/src/shadowbox/integration_test/client/Dockerfile b/src/shadowbox/integration_test/client/Dockerfile new file mode 100644 index 000000000..f4b89cdd9 --- /dev/null +++ b/src/shadowbox/integration_test/client/Dockerfile @@ -0,0 +1,21 @@ +# Copyright 2018 The Outline Authors +# +# 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. + +FROM golang:1-alpine + +# curl for fetching pages using the local proxy +RUN apk add --no-cache curl git +RUN go install github.com/shadowsocks/go-shadowsocks2@v0.1.5 + +ENTRYPOINT [ "sh" ] diff --git a/src/shadowbox/integration_test/docker-compose.yml b/src/shadowbox/integration_test/docker-compose.yml new file mode 100644 index 000000000..2b8f87fad --- /dev/null +++ b/src/shadowbox/integration_test/docker-compose.yml @@ -0,0 +1,60 @@ +version: "2.1" + +networks: + open: + censored: + +services: + target: + build: + context: ./target + ports: + - "10080:80" + networks: + - open + # The python SimpleHTTPServer doesn't quit with SIGTERM. + stop_signal: SIGKILL + + shadowbox: + image: ${SB_IMAGE:-outline/shadowbox} + environment: + - SB_API_PORT=443 + - SB_API_PREFIX=${SB_API_PREFIX} + - LOG_LEVEL=debug + - SB_CERTIFICATE_FILE=/root/shadowbox/test.crt + - SB_PRIVATE_KEY_FILE=/root/shadowbox/test.key + ports: + - "20443:443" + links: + - target + networks: + - open + - censored + volumes: + - ${SB_CERTIFICATE_FILE}:/root/shadowbox/test.crt + - ${SB_PRIVATE_KEY_FILE}:/root/shadowbox/test.key + - ${TMP_STATE_DIR}:/root/shadowbox/persisted-state + # The user management service doesn't quit with SIGTERM + stop_signal: SIGKILL + + client: + build: + context: ./client + ports: + - "30555:555" + # Keep the container running + stdin_open: true + tty: true + links: + - shadowbox + networks: + - censored + + util: + build: + context: ./util + networks: + - open + # Keep the container running + stdin_open: true + tty: true diff --git a/src/shadowbox/integration_test/run.action.sh b/src/shadowbox/integration_test/run.action.sh new file mode 100755 index 000000000..953fc178a --- /dev/null +++ b/src/shadowbox/integration_test/run.action.sh @@ -0,0 +1,38 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +run_action shadowbox/docker/build + +LOGFILE="$(mktemp)" +readonly LOGFILE +echo "Running Shadowbox integration test. Logs at ${LOGFILE}" + +cd src/shadowbox/integration_test + +declare -i result=0 + +if ./test.sh > "${LOGFILE}" 2>&1 ; then + echo "Test Passed!" + # Removing the log file sometimes fails on Travis. There's no point in us cleaning it up + # on a CI build anyways. + rm -f "${LOGFILE}" +else + result=$? + echo "Test Failed! Logs:" + cat "${LOGFILE}" +fi + +exit "${result}" diff --git a/src/shadowbox/integration_test/target/Dockerfile b/src/shadowbox/integration_test/target/Dockerfile new file mode 100644 index 000000000..44f7ced25 --- /dev/null +++ b/src/shadowbox/integration_test/target/Dockerfile @@ -0,0 +1,18 @@ +# Copyright 2018 The Outline Authors +# +# 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. + +# Pin to a known good signed image to avoid failures from the Docker notary service +FROM gcr.io/distroless/python3@sha256:58087520b3c929fe77e1ef3fc95062dbe80bbda265e0e7966c4997c71a9636ea +COPY index.html . +ENTRYPOINT ["python", "-m", "http.server", "80"] diff --git a/src/shadowbox/integration_test/target/index.html b/src/shadowbox/integration_test/target/index.html new file mode 100644 index 000000000..583442013 --- /dev/null +++ b/src/shadowbox/integration_test/target/index.html @@ -0,0 +1,3 @@ + + TARGET PAGE CONTENT + diff --git a/src/shadowbox/integration_test/test.sh b/src/shadowbox/integration_test/test.sh new file mode 100755 index 000000000..d374560d7 --- /dev/null +++ b/src/shadowbox/integration_test/test.sh @@ -0,0 +1,261 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +# Shadowbox Integration Test +# +# This test verifies that a client can access a target website via a shadowbox node. +# Sets up a target in the LAN to validate that it cannot be accessed through shadowbox. +# +# Architecture: +# +# +--------+ +-----------+ +# | Client | --> | Shadowbox | --> Internet +# +--------+ +-----------+ +# | +--------+ +# ----//----> | Target | +# +--------+ +# +# Each node runs on a different Docker container. + +set -x + +export DOCKER_CONTENT_TRUST="${DOCKER_CONTENT_TRUST:-1}" + +OUTPUT_DIR="$(mktemp -d)" +readonly OUTPUT_DIR +# TODO(fortuna): Make it possible to run multiple tests in parallel by adding a +# run id to the container names. +readonly TARGET_CONTAINER='integrationtest_target_1' +readonly SHADOWBOX_CONTAINER='integrationtest_shadowbox_1' +readonly CLIENT_CONTAINER='integrationtest_client_1' +readonly UTIL_CONTAINER='integrationtest_util_1' +readonly INTERNET_TARGET_URL="http://www.gstatic.com/generate_204" +echo "Test output at ${OUTPUT_DIR}" +# Set DEBUG=1 to not kill the stack when the test is finished so you can query +# the containers. +declare -ir DEBUG=${DEBUG:-0} + +# Waits for the input URL to return success. +function wait_for_resource() { + local -r URL="$1" + until curl --silent --insecure "${URL}" > /dev/null; do sleep 1; done +} + +function util_jq() { + docker exec -i "${UTIL_CONTAINER}" jq "$@" +} + +# Takes the JSON from a /access-keys POST request and returns the appropriate +# ss-local arguments to connect to that user/instance. +function ss_arguments_for_user() { + local SS_INSTANCE_CIPHER SS_INSTANCE_PASSWORD + SS_INSTANCE_CIPHER="$(echo "$1" | util_jq -r .method)" + SS_INSTANCE_PASSWORD="$(echo "$1" | util_jq -r .password)" + local -i SS_INSTANCE_PORT + SS_INSTANCE_PORT=$(echo "$1" | util_jq .port) + echo -cipher "${SS_INSTANCE_CIPHER}" -password "${SS_INSTANCE_PASSWORD}" -c "shadowbox:${SS_INSTANCE_PORT}" +} + +# Runs curl on the client container. +function client_curl() { + docker exec "${CLIENT_CONTAINER}" curl --silent --show-error --connect-timeout 5 --retry 5 "$@" +} + +function fail() { + echo FAILED: "$@" + exit 1 +} + +function cleanup() { + local -i status=$? + if ((DEBUG != 1)); then + docker-compose --project-name=integrationtest down + rm -rf "${TMP_STATE_DIR}" || echo "Failed to cleanup files at ${TMP_STATE_DIR}" + fi + return "${status}" +} + +# Start a subprocess for trap +( + set -eu + ((DEBUG == 1)) && set -x + + # Ensure proper shut down on exit if not in debug mode + trap "cleanup" EXIT + + # Make the certificate + source ../scripts/make_test_certificate.sh /tmp + + # Sets everything up + export SB_API_PREFIX='TestApiPrefix' + readonly SB_API_URL="https://shadowbox/${SB_API_PREFIX}" + TMP_STATE_DIR="$(mktemp -d)" + export TMP_STATE_DIR + echo '{"hostname": "shadowbox"}' > "${TMP_STATE_DIR}/shadowbox_server_config.json" + docker-compose --project-name=integrationtest up --build -d + + # Wait for target to come up. + wait_for_resource localhost:10080 + TARGET_IP="$(docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "${TARGET_CONTAINER}")" + readonly TARGET_IP + + # Verify that the client cannot access or even resolve the target + # Exit code 28 for "Connection timed out". + (docker exec "${CLIENT_CONTAINER}" curl --silent --connect-timeout 5 "${TARGET_IP}" > /dev/null && \ + fail "Client should not have access to target IP") || (($? == 28)) + + # Exit code 6 for "Could not resolve host". In some environments, curl reports a timeout + # error (28) instead, which is surprising. TODO: Investigate and fix. + (docker exec "${CLIENT_CONTAINER}" curl --connect-timeout 5 http://target > /dev/null && \ + fail "Client should not have access to target host") || (($? == 6 || $? == 28)) + + # Wait for shadowbox to come up. + wait_for_resource https://localhost:20443/access-keys + # Verify that the shadowbox can access the target + docker exec "${SHADOWBOX_CONTAINER}" wget --spider http://target + + # Create new shadowbox user. + # TODO(bemasc): Verify that the server is using the right certificate + NEW_USER_JSON="$(client_curl --insecure -X POST "${SB_API_URL}/access-keys")" + readonly NEW_USER_JSON + [[ "${NEW_USER_JSON}" == '{"id":"0"'* ]] || fail "Fail to create user" + read -r -a SS_USER_ARGUMENTS < <(ss_arguments_for_user "${NEW_USER_JSON}") + readonly SS_USER_ARGUMENTS + + # Verify that we handle deletions well + client_curl --insecure -X POST "${SB_API_URL}/access-keys" > /dev/null + client_curl --insecure -X DELETE "${SB_API_URL}/access-keys/1" > /dev/null + + # Start Shadowsocks client and wait for it to be ready + declare -ir LOCAL_SOCKS_PORT=5555 + docker exec -d "${CLIENT_CONTAINER}" \ + /go/bin/go-shadowsocks2 "${SS_USER_ARGUMENTS[@]}" -socks "localhost:${LOCAL_SOCKS_PORT}" -verbose \ + || fail "Could not start shadowsocks client" + while ! docker exec "${CLIENT_CONTAINER}" nc -z localhost "${LOCAL_SOCKS_PORT}"; do + sleep 0.1 + done + + function test_networking() { + # Verify the server blocks requests to hosts on private addresses. + # Exit code 52 is "Empty server response". + (client_curl -x "socks5h://localhost:${LOCAL_SOCKS_PORT}" "${TARGET_IP}" &> /dev/null \ + && fail "Target host in a private network accessible through shadowbox") || (($? == 52)) + + # Verify we can retrieve the internet target URL. + client_curl -x "socks5h://localhost:${LOCAL_SOCKS_PORT}" "${INTERNET_TARGET_URL}" \ + || fail "Could not fetch ${INTERNET_TARGET_URL} through shadowbox." + + # Verify we can't access the URL anymore after the key is deleted + client_curl --insecure -X DELETE "${SB_API_URL}/access-keys/0" > /dev/null + # Exit code 56 is "Connection reset by peer". + (client_curl -x "socks5h://localhost:${LOCAL_SOCKS_PORT}" "${INTERNET_TARGET_URL}" &> /dev/null \ + && fail "Deleted access key is still active") || (($? == 56)) + } + + function test_port_for_new_keys() { + # Verify that we can change the port for new access keys + client_curl --insecure -X PUT -H "Content-Type: application/json" -d '{"port": 12345}' "${SB_API_URL}/server/port-for-new-access-keys" \ + || fail "Couldn't change the port for new access keys" + + local ACCESS_KEY_JSON + ACCESS_KEY_JSON="$(client_curl --insecure -X POST "${SB_API_URL}/access-keys" \ + || fail "Couldn't get a new access key after changing port")" + + if [[ "${ACCESS_KEY_JSON}" != *'"port":12345'* ]]; then + fail "Port for new access keys wasn't changed. Newly created access key: ${ACCESS_KEY_JSON}" + fi + } + + function test_hostname_for_new_keys() { + # Verify that we can change the hostname for new access keys + local -r NEW_HOSTNAME="newhostname" + client_curl --insecure -X PUT -H 'Content-Type: application/json' -d '{"hostname": "'"${NEW_HOSTNAME}"'"}' "${SB_API_URL}/server/hostname-for-access-keys" \ + || fail "Couldn't change hostname for new access keys" + + local ACCESS_KEY_JSON + ACCESS_KEY_JSON="$(client_curl --insecure -X POST "${SB_API_URL}/access-keys" \ + || fail "Couldn't get a new access key after changing hostname")" + + if [[ "${ACCESS_KEY_JSON}" != *"@${NEW_HOSTNAME}:"* ]]; then + fail "Hostname for new access keys wasn't changed. Newly created access key: ${ACCESS_KEY_JSON}" + fi + } + + function test_encryption_for_new_keys() { + # Verify that we can create news keys with custom encryption. + client_curl --insecure -X POST -H "Content-Type: application/json" -d '{"method":"aes-256-gcm"}' "${SB_API_URL}/access-keys" \ + || fail "Couldn't create a new access key with a custom method" + + local ACCESS_KEY_JSON + ACCESS_KEY_JSON="$(client_curl --insecure -X GET "${SB_API_URL}/access-keys" \ + || fail "Couldn't get a new access key after changing hostname")" + + if [[ "${ACCESS_KEY_JSON}" != *'"method":"aes-256-gcm"'* ]]; then + fail "Custom encryption key not taken by new access key: ${ACCESS_KEY_JSON}" + fi + } + + function test_default_data_limit() { + # Verify that we can create default data limits + client_curl --insecure -X PUT -H 'Content-Type: application/json' -d '{"limit": {"bytes": 1000}}' \ + "${SB_API_URL}/server/access-key-data-limit" \ + || fail "Couldn't create default data limit" + client_curl --insecure "${SB_API_URL}/server" | grep -q 'accessKeyDataLimit' || fail 'Default data limit not set' + + # Verify that we can remove default data limits + client_curl --insecure -X DELETE "${SB_API_URL}/server/access-key-data-limit" \ + || fail "Couldn't remove default data limit" + client_curl --insecure "${SB_API_URL}/server" | grep -vq 'accessKeyDataLimit' || fail 'Default data limit not removed' + } + + function test_per_key_data_limits() { + # Verify that we can create per-key data limits + local ACCESS_KEY_ID + ACCESS_KEY_ID="$(client_curl --insecure -X POST "${SB_API_URL}/access-keys" | util_jq -re .id \ + || fail "Couldn't get a key to test custom data limits")" + + client_curl --insecure -X PUT -H 'Content-Type: application/json' -d '{"limit": {"bytes": 1000}}' \ + "${SB_API_URL}/access-keys/${ACCESS_KEY_ID}/data-limit" \ + || fail "Couldn't create per-key data limit" + client_curl --insecure "${SB_API_URL}/access-keys" \ + | util_jq -e ".accessKeys[] | select(.id == \"${ACCESS_KEY_ID}\") | .dataLimit.bytes" \ + || fail 'Per-key data limit not set' + + # Verify that we can remove per-key data limits + client_curl --insecure -X DELETE "${SB_API_URL}/access-keys/${ACCESS_KEY_ID}/data-limit" \ + || fail "Couldn't remove per-key data limit" + ! client_curl --insecure "${SB_API_URL}/access-keys" \ + | util_jq -e ".accessKeys[] | select(.id == \"${ACCESS_KEY_ID}\") | .dataLimit.bytes" \ + || fail 'Per-key data limit not removed' + } + + test_networking + test_port_for_new_keys + test_hostname_for_new_keys + test_encryption_for_new_keys + test_default_data_limit + test_per_key_data_limits + + # Verify no errors occurred. + readonly SHADOWBOX_LOG="${OUTPUT_DIR}/shadowbox-log.txt" + if docker logs "${SHADOWBOX_CONTAINER}" 2>&1 | tee "${SHADOWBOX_LOG}" | grep -Eq "^E|level=error|ERROR:"; then + cat "${SHADOWBOX_LOG}" + fail "Found errors in Shadowbox logs (see above, also saved to ${SHADOWBOX_LOG})" + fi + + # TODO(fortuna): Test metrics. + # TODO(fortuna): Verify UDP requests. +) diff --git a/src/shadowbox/integration_test/util/Dockerfile b/src/shadowbox/integration_test/util/Dockerfile new file mode 100644 index 000000000..29ca1758b --- /dev/null +++ b/src/shadowbox/integration_test/util/Dockerfile @@ -0,0 +1,17 @@ +# Copyright 2018 The Outline Authors +# +# 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. + +FROM alpine:3.5 +RUN apk add --no-cache jq +ENTRYPOINT [ "sh" ] diff --git a/src/shadowbox/model/access_key.ts b/src/shadowbox/model/access_key.ts new file mode 100644 index 000000000..d2935fa40 --- /dev/null +++ b/src/shadowbox/model/access_key.ts @@ -0,0 +1,89 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +export type AccessKeyId = string; +export type AccessKeyMetricsId = string; + +// Parameters needed to access a Shadowsocks proxy. +export interface ProxyParams { + // Hostname of the proxy + readonly hostname: string; + // Number of the port where the Shadowsocks service is running. + readonly portNumber: number; + // The Shadowsocks encryption method being used. + readonly encryptionMethod: string; + // The password for the encryption. + readonly password: string; +} + +// Data transfer allowance, measured in bytes. Must be a serializable JSON object. +export interface DataLimit { + readonly bytes: number; +} + +// AccessKey is what admins work with. It gives ProxyParams a name and identity. +export interface AccessKey { + // The unique identifier for this access key. + readonly id: AccessKeyId; + // Admin-controlled, editable name for this access key. + readonly name: string; + // Used in metrics reporting to decouple from the real id. Can change. + readonly metricsId: AccessKeyMetricsId; + // Parameters to access the proxy + readonly proxyParams: ProxyParams; + // Whether the access key has exceeded the data transfer limit. + readonly isOverDataLimit: boolean; + // The key's current data limit. If it exists, it overrides the server default data limit. + readonly dataLimit?: DataLimit; +} + +export interface AccessKeyCreateParams { + // The unique identifier to give the access key. Throws if it exists. + readonly id?: AccessKeyId; + // The encryption method to use for the access key. + readonly encryptionMethod?: string; + // The name to give the access key. + readonly name?: string; + // The password to use for the access key. + readonly password?: string; + // The data transfer limit to apply to the access key. + readonly dataLimit?: DataLimit; +} + +export interface AccessKeyRepository { + // Creates a new access key. Parameters are chosen automatically if not provided. + createNewAccessKey(params?: AccessKeyCreateParams): Promise; + // Removes the access key given its id. Throws on failure. + removeAccessKey(id: AccessKeyId); + // Returns the access key with the given id. Throws on failure. + getAccessKey(id: AccessKeyId): AccessKey; + // Lists all existing access keys + listAccessKeys(): AccessKey[]; + // Changes the port for new access keys. + setPortForNewAccessKeys(port: number): Promise; + // Changes the hostname for access keys. + setHostname(hostname: string): void; + // Apply the specified update to the specified access key. Throws on failure. + renameAccessKey(id: AccessKeyId, name: string): void; + // Gets the metrics id for a given Access Key. + getMetricsId(id: AccessKeyId): AccessKeyMetricsId | undefined; + // Sets a data transfer limit for all access keys. + setDefaultDataLimit(limit: DataLimit): void; + // Removes the access key data transfer limit. + removeDefaultDataLimit(): void; + // Sets access key `id` to use the given custom data limit. + setAccessKeyDataLimit(id: AccessKeyId, limit: DataLimit): void; + // Removes the custom data limit from access key `id`. + removeAccessKeyDataLimit(id: AccessKeyId): void; +} diff --git a/src/shadowbox/model/errors.ts b/src/shadowbox/model/errors.ts new file mode 100644 index 000000000..4124e2bff --- /dev/null +++ b/src/shadowbox/model/errors.ts @@ -0,0 +1,55 @@ +// Copyright 2019 The Outline Authors +// +// 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. + +import {AccessKeyId} from './access_key'; + +// TODO(fortuna): Reuse CustomError from server_manager. +class OutlineError extends Error { + constructor(message: string) { + super(message); + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class InvalidPortNumber extends OutlineError { + // Since this is the error when a non-numeric value is passed to `port`, it takes type `string`. + constructor(public port: string) { + super(port); + } +} + +export class PortUnavailable extends OutlineError { + constructor(public port: number) { + super(port.toString()); + } +} + +export class AccessKeyNotFound extends OutlineError { + constructor(accessKeyId?: AccessKeyId) { + super(`Access key "${accessKeyId}" not found`); + } +} + +export class InvalidCipher extends OutlineError { + constructor(public cipher: string) { + super(`cipher "${cipher}" is not valid`); + } +} + +export class AccessKeyConflict extends OutlineError { + constructor(accessKeyId?: AccessKeyId) { + super(`Access key "${accessKeyId}" conflict`); + } +} diff --git a/src/shadowbox/model/metrics.ts b/src/shadowbox/model/metrics.ts new file mode 100644 index 000000000..9a9a901b8 --- /dev/null +++ b/src/shadowbox/model/metrics.ts @@ -0,0 +1,30 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +// Byte transfer metrics for a sliding timeframe, including both inbound and outbound. +// TODO: this is copied at src/model/server.ts. Both copies should +// be kept in sync, until we can find a way to share code between the web_app +// and shadowbox. +export interface DataUsageByUser { + // The userId key should be of type AccessKeyId, however that results in the tsc + // error TS1023: An index signature parameter type must be 'string' or 'number'. + // See https://github.com/Microsoft/TypeScript/issues/2491 + // TODO: rename this to AccessKeyId in a backwards compatible way. + bytesTransferredByUserId: {[userId: string]: number}; +} + +// Sliding time frame for measuring data utilization. +export interface DataUsageTimeframe { + hours: number; +} diff --git a/src/shadowbox/model/shadowsocks_server.ts b/src/shadowbox/model/shadowsocks_server.ts new file mode 100644 index 000000000..a3416ef95 --- /dev/null +++ b/src/shadowbox/model/shadowsocks_server.ts @@ -0,0 +1,26 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +// Parameters required to identify and authenticate connections to a Shadowsocks server. +export interface ShadowsocksAccessKey { + id: string; + port: number; + cipher: string; + secret: string; +} + +export interface ShadowsocksServer { + // Updates the server to accept only the given access keys. + update(keys: ShadowsocksAccessKey[]): Promise; +} diff --git a/src/shadowbox/package.json b/src/shadowbox/package.json new file mode 100644 index 000000000..a2c952cbc --- /dev/null +++ b/src/shadowbox/package.json @@ -0,0 +1,35 @@ +{ + "name": "outline-server", + "private": true, + "version": "1.7.2", + "description": "Outline server", + "main": "build/server/main.js", + "author": "Outline", + "license": "Apache", + "__COMMENTS__": [ + "Using https:// for ShadowsocksConfig to avoid adding git in the Docker image" + ], + "dependencies": { + "ip-regex": "^4.1.0", + "js-yaml": "^3.12.0", + "outline-shadowsocksconfig": "github:Jigsaw-Code/outline-shadowsocksconfig#v0.2.0", + "prom-client": "^11.1.3", + "randomstring": "^1.1.5", + "restify": "^11.1.0", + "restify-cors-middleware2": "^2.2.1", + "restify-errors": "^8.0.2", + "uuid": "^3.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^3.11.2", + "@types/node": "^12", + "@types/randomstring": "^1.1.6", + "@types/restify": "^8.4.2", + "@types/restify-cors-middleware": "^1.0.1", + "@types/tmp": "^0.2.1", + "tmp": "^0.2.1", + "ts-loader": "^9.5.0", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + } +} diff --git a/src/shadowbox/scripts/make_test_certificate.sh b/src/shadowbox/scripts/make_test_certificate.sh new file mode 100755 index 000000000..35d62c9b4 --- /dev/null +++ b/src/shadowbox/scripts/make_test_certificate.sh @@ -0,0 +1,32 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +# Make a certificate for development purposes, and populate the +# corresponding environment variables. + +readonly CERTIFICATE_NAME="$1/shadowbox-selfsigned-dev" +export SB_CERTIFICATE_FILE="${CERTIFICATE_NAME}.crt" +export SB_PRIVATE_KEY_FILE="${CERTIFICATE_NAME}.key" +declare -a openssl_req_flags=( + -x509 + -nodes + -days 36500 + -newkey rsa:2048 + -subj '/CN=localhost' + -keyout "${SB_PRIVATE_KEY_FILE}" + -out "${SB_CERTIFICATE_FILE}" +) +openssl req "${openssl_req_flags[@]}" diff --git a/src/shadowbox/scripts/update_mmdb.sh b/src/shadowbox/scripts/update_mmdb.sh new file mode 100755 index 000000000..b283321cb --- /dev/null +++ b/src/shadowbox/scripts/update_mmdb.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# Download the IP-to-country MMDB database into the same location +# used by Alpine's libmaxminddb package. + +# IP Geolocation by DB-IP (https://db-ip.com) + +# Note that this runs on BusyBox sh, which lacks bash features. + +TMPDIR="$(mktemp -d)" +readonly TMPDIR +readonly FILENAME="ip-country.mmdb" + +# We need to make sure that we grab an existing database at install-time +for monthdelta in $(seq 10); do + newdate="$(date --date="-${monthdelta} months" +%Y-%m)" + ADDRESS="https://download.db-ip.com/free/dbip-country-lite-${newdate}.mmdb.gz" + curl --fail --silent "${ADDRESS}" -o "${TMPDIR}/${FILENAME}.gz" > /dev/null && break + if [ "${monthdelta}" -eq '10' ]; then + # A weird exit code on purpose -- we should catch this long before it triggers + exit 2 + fi +done + +gunzip "${TMPDIR}/${FILENAME}.gz" +readonly LIBDIR="/var/lib/libmaxminddb" +mkdir -p "${LIBDIR}" +mv -f "${TMPDIR}/${FILENAME}" "${LIBDIR}" +rmdir "${TMPDIR}" diff --git a/src/shadowbox/server/api.yml b/src/shadowbox/server/api.yml new file mode 100644 index 000000000..48e68b5d8 --- /dev/null +++ b/src/shadowbox/server/api.yml @@ -0,0 +1,506 @@ +openapi: 3.0.1 +info: + title: Outline Server Management + description: API to manage an Outline server. See [getoutline.org](https://getoutline.org). + version: '1.0' +tags: + - name: Server + description: Server-level functions + - name: Access Key + description: Access key functions +servers: + - url: https://myserver/SecretPath + description: Example URL. Change to your own server. +paths: + /server: + get: + tags: + - Server + description: Returns information about the server + responses: + '200': + description: Server information + content: + application/json: + schema: + $ref: "#/components/schemas/Server" + examples: + 'No data limit': + value: >- + {"name":"My Server","serverId":"40f1b4a3-5c82-45f4-80a6-a25cf36734d3","metricsEnabled":true,"createdTimestampMs":1536613192052,"version":"1.0.0","portForNewAccessKeys":1234,"hostnameForAccessKeys":"example.com"} + 'Per-key data limit': + value: >- + {"name":"My Server","serverId":"7fda0079-5317-4e5a-bb41-5a431dddae21","metricsEnabled":true,"createdTimestampMs":1536613192052,"version":"1.0.0","accessKeyDataLimit":{"bytes":8589934592},"portForNewAccessKeys":1234,"hostnameForAccessKeys":"example.com"} + /server/hostname-for-access-keys: + put: + tags: + - Server + description: Changes the hostname for access keys. Must be a valid hostname or IP address. If it's a hostname, DNS must be set up independently of this API. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + hostname: + type: string + examples: + 'hostname': + value: '{"hostname": "www.example.org"}' + 'IP address': + value: '{"hostname": "127.0.0.1"}' + responses: + '204': + description: The hostname was successfully changed. + '400': + description: An invalid hostname or IP address was provided. + '500': + description: An internal error occurred. This could be thrown if there were network errors while validating the hostname + + /server/port-for-new-access-keys: + put: + description: Changes the default port for newly created access keys. This can be a port already used for access keys. + tags: + - Access Key + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + port: + type: number + examples: + '0': + value: '{"port": 12345}' + responses: + '204': + description: The default port was successfully changed. + '400': + description: The requested port wasn't an integer from 1 through 65535, or the request had no port parameter. + '409': + description: The requested port was already in use by another service. + + /server/access-key-data-limit: + put: + description: Sets a data transfer limit for all access keys + tags: + - Access Key + - Limit + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DataLimit" + examples: + '0': + value: '{"limit": {"bytes": 10000}}' + responses: + '204': + description: Access key data limit set successfully + '400': + description: Invalid data limit + delete: + description: Removes the access key data limit, lifting data transfer restrictions on all access keys. + tags: + - Access Key + - Limit + responses: + '204': + description: Access key limit deleted successfully. + + /name: + put: + description: Renames the server + tags: + - Server + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + examples: + '0': + value: '{"name":"My Server"}' + responses: + '204': + description: Server renamed successfully + '400': + description: Invalid name + /access-keys: + post: + description: Creates a new access key + tags: + - Access Key + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + name: + type: string + method: + type: string + password: + type: string + port: + type: integer + dataLimit: + $ref: "#/components/schemas/DataLimit" + examples: + 'No params specified': + value: '{"method":"aes-192-gcm"}' + 'Provide params': + value: '{"method":"aes-192-gcm","name":"First","password":"8iu8V8EeoFVpwQvQeS9wiD","limit":{"bytes":10000}}' + responses: + '201': + description: The newly created access key + content: + application/json: + schema: + $ref: "#/components/schemas/AccessKey" + examples: + '0': + value: >- + {"id":"0","name":"First","password":"XxXxXx","port":9795,"method":"chacha20-ietf-poly1305","accessUrl":"ss://SADFJSKADFJAKSD@0.0.0.0:9795/?outline=1"} + '1': + value: >- + {"id":"1","name":"Second","password":"xXxXxX","port":9795,"method":"chacha20-ietf-poly1305","accessUrl":"ss://ASDFHAKSDFSDAKFJ@0.0.0.0:9795/?outline=1"} + put: + description: Creates a new access key with a specific identifer + tags: + - Access Key + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: + id: + type: string + name: + type: string + method: + type: string + password: + type: string + port: + type: integer + dataLimit: + $ref: "#/components/schemas/DataLimit" + examples: + '0': + value: '{"id":"123","method":"aes-192-gcm","name":"First","password":"8iu8V8EeoFVpwQvQeS9wiD","limit":{"bytes":10000}}' + responses: + '201': + description: The newly created access key + content: + application/json: + schema: + $ref: "#/components/schemas/AccessKey" + examples: + '0': + value: >- + {"id":"my-identifier","name":"First","password":"XxXxXx","port":9795,"method":"chacha20-ietf-poly1305","accessUrl":"ss://SADFJSKADFJAKSD@0.0.0.0:9795/?outline=1"} + get: + description: Lists the access keys + tags: + - Access Key + responses: + '200': + description: List of access keys + content: + application/json: + schema: + type: object + properties: + accessKeys: + type: array + items: + $ref: "#/components/schemas/AccessKey" + examples: + '0': + value: >- + {"accessKeys":[ + {"id":"0","name":"Admin","password":"XxXxXx","port":18162,"method":"chacha20-ietf-poly1305","accessUrl":"ss://SADFJSKADFJAKSD@0.0.0.0:18162/?outline=1"}, + {"id":"1","name":"First","password":"xXxXxX","port":4410,"method":"chacha20-ietf-poly1305","accessUrl":"ss://ASDFSADJFKAS=@0.0.0.0:4410/?outline=1"}, + {"id":"2","name":"SecondWithCustomDataLimit","password":"XxXxXx","port":25424,"method":"chacha20-ietf-poly1305","dataLimit":{"bytes":8589934592},"accessUrl":"ss://ASDFHAKSDFSDAKFJ@0.0.0.0:25424/?outline=1"}]} + /access-keys/{id}: + get: + description: Get an access key + tags: + - Access Key + parameters: + - name: id + in: path + required: true + description: The id to get the access key + schema: + type: string + responses: + '200': + description: The access key + content: + application/json: + schema: + $ref: "#/components/schemas/AccessKey" + examples: + '0': + value: '{"id":"0","name":"Admin","password":"XxXxXx","port":18162,"method":"chacha20-ietf-poly1305","accessUrl":"ss://SADFJSKADFJAKSD@0.0.0.0:18162/?outline=1"}' + '404': + description: Access key inexistent + content: + application/json: + schema: + type: object + properties: + code: + type: string + message: + type: string + examples: + '0': + value: >- + {"code":"NotFoundError","message":"No access key found"} + delete: + description: Deletes an access key + tags: + - Access Key + parameters: + - name: id + in: path + required: true + description: The id of the access key to delete + schema: + type: string + responses: + '204': + description: Access key deleted successfully + '404': + description: Access key inexistent + content: + application/json: + schema: + type: object + properties: + code: + type: string + message: + type: string + examples: + '0': + value: >- + {"code":"NotFoundError","message":"No access key found with + id 9"} + /access-keys/{id}/name: + put: + description: Renames an access key + tags: + - Access Key + parameters: + - name: id + in: path + required: true + description: The id of the access key to rename + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + examples: + '0': + value: '{"name": "New Key Name"}' + responses: + '204': + description: Access key renamed successfully + '404': + description: Access key inexistent + /access-keys/{id}/data-limit: + put: + description: Sets a data limit for the given access key + tags: + - Access Key + - Limit + parameters: + - name: id + in: path + required: true + description: The id of the access key + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DataLimit" + examples: + '0': + value: '{"limit": {"bytes": 10000}}' + responses: + '204': + description: Access key limit set successfully + '400': + description: Invalid data limit + '404': + description: Access key inexistent + delete: + description: Removes the data limit on the given access key. + tags: + - Access Key + - Limit + parameters: + - name: id + in: path + required: true + description: The id of the access key + schema: + type: string + responses: + '204': + description: Access key limit deleted successfully. + '404': + description: Access key inexistent + /metrics/transfer: + get: + description: Returns the data transferred per access key + tags: + - Access Key + responses: + '200': + description: The data transferred by each access key + content: + application/json: + schema: + type: object + properties: + bytesTransferredByUserId: + type: object + additionalProperties: + type: integer + examples: + '0': + value: '{"bytesTransferredByUserId":{"1":1008040941,"2":5958113497,"3":752221577}}' + /metrics/enabled: + get: + description: Returns whether metrics is being shared + tags: + - Server + responses: + '200': + description: The metrics enabled setting + content: + application/json: + schema: + type: object + properties: + metricsEnabled: + type: boolean + examples: + '0': + value: '{"metricsEnabled":true}' + put: + description: Enables or disables sharing of metrics + tags: + - Server + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + metricsEnabled: + type: boolean + examples: + '0': + value: '{"metricsEnabled": true}' + responses: + '204': + description: Setting successful + '400': + description: Invalid request + /experimental/access-key-data-limit: + put: + deprecated: true + description: (Deprecated) Sets a data transfer limit for all access keys + tags: + - Access Key + - Limit + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/DataLimit" + examples: + '0': + value: '{"limit": {"bytes": 10000}}' + responses: + '204': + description: Access key data limit set successfully + '400': + description: Invalid data limit + delete: + deprecated: true + description: (Deprecated) Removes the access key data limit, lifting data transfer restrictions on all access keys. + tags: + - Access Key + - Limit + responses: + '204': + description: Access key limit deleted successfully. + +components: + schemas: + Server: + properties: + name: + type: string + serverId: + type: string + metricsEnabled: + type: boolean + createdTimestampMs: + type: number + portForNewAccessKeys: + type: integer + + DataLimit: + properties: + bytes: + type: integer + minimum: 0 + + AccessKey: + required: + - id + properties: + id: + type: string + name: + type: string + password: + type: string + port: + type: integer + method: + type: string + accessUrl: + type: string diff --git a/src/shadowbox/server/build.action.sh b/src/shadowbox/server/build.action.sh new file mode 100755 index 000000000..bcc05f600 --- /dev/null +++ b/src/shadowbox/server/build.action.sh @@ -0,0 +1,40 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +readonly OUT_DIR="${BUILD_DIR}/shadowbox" +rm -rf "${OUT_DIR}" +mkdir -p "${OUT_DIR}" + +webpack --config=src/shadowbox/webpack.config.js ${BUILD_ENV:+--mode="${BUILD_ENV}"} + +# Install third_party dependencies +readonly OS="$([[ "$(uname)" == "Darwin" ]] && echo "macos" || echo "linux")" +export ARCH=${ARCH:-x86_64} +readonly BIN_DIR="${OUT_DIR}/bin" +mkdir -p "${BIN_DIR}" +{ + cd "${ROOT_DIR}/third_party/prometheus" + make "bin/${OS}-${ARCH}/prometheus" + cp "bin/${OS}-${ARCH}/prometheus" "${BIN_DIR}/" +} +{ + cd "${ROOT_DIR}/third_party/outline-ss-server" + make "bin/${OS}-${ARCH}/outline-ss-server" + cp "bin/${OS}-${ARCH}/outline-ss-server" "${BIN_DIR}/" +} + +# Copy shadowbox package.json +cp "${ROOT_DIR}/src/shadowbox/package.json" "${OUT_DIR}/" diff --git a/src/shadowbox/server/main.ts b/src/shadowbox/server/main.ts new file mode 100644 index 000000000..98c92440b --- /dev/null +++ b/src/shadowbox/server/main.ts @@ -0,0 +1,285 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as fs from 'fs'; +import * as http from 'http'; +import * as path from 'path'; +import * as process from 'process'; +import * as prometheus from 'prom-client'; +import * as restify from 'restify'; +import * as corsMiddleware from 'restify-cors-middleware2'; + +import {RealClock} from '../infrastructure/clock'; +import {PortProvider} from '../infrastructure/get_port'; +import * as json_config from '../infrastructure/json_config'; +import * as logging from '../infrastructure/logging'; +import {PrometheusClient, startPrometheus} from '../infrastructure/prometheus_scraper'; +import {RolloutTracker} from '../infrastructure/rollout'; +import {AccessKeyId} from '../model/access_key'; +import {version} from '../package.json'; + +import {PrometheusManagerMetrics} from './manager_metrics'; +import {bindService, ShadowsocksManagerService} from './manager_service'; +import {OutlineShadowsocksServer} from './outline_shadowsocks_server'; +import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key'; +import * as server_config from './server_config'; +import { + OutlineSharedMetricsPublisher, + PrometheusUsageMetrics, + RestMetricsCollectorClient, + SharedMetricsPublisher, +} from './shared_metrics'; + +const APP_BASE_DIR = path.join(__dirname, '..'); +const DEFAULT_STATE_DIR = '/root/shadowbox/persisted-state'; +const MMDB_LOCATION = '/var/lib/libmaxminddb/ip-country.mmdb'; + +async function exportPrometheusMetrics(registry: prometheus.Registry, port): Promise { + return new Promise((resolve, _) => { + const server = http.createServer((_, res) => { + res.write(registry.metrics()); + res.end(); + }); + server.on('listening', () => { + resolve(server); + }); + server.listen({port, host: 'localhost', exclusive: true}); + }); +} + +function reserveExistingAccessKeyPorts( + keyConfig: json_config.JsonConfig, + portProvider: PortProvider +) { + const accessKeys = keyConfig.data().accessKeys || []; + const dedupedPorts = new Set(accessKeys.map((ak) => ak.port)); + dedupedPorts.forEach((p) => portProvider.addReservedPort(p)); +} + +function createRolloutTracker( + serverConfig: json_config.JsonConfig +): RolloutTracker { + const rollouts = new RolloutTracker(serverConfig.data().serverId); + if (serverConfig.data().rollouts) { + for (const rollout of serverConfig.data().rollouts) { + rollouts.forceRollout(rollout.id, rollout.enabled); + } + } + return rollouts; +} + +async function main() { + const verbose = process.env.LOG_LEVEL === 'debug'; + logging.info('======== Outline Server main() ========'); + logging.info(`Version is ${version}`); + + const portProvider = new PortProvider(); + const accessKeyConfig = json_config.loadFileConfig( + getPersistentFilename('shadowbox_config.json') + ); + reserveExistingAccessKeyPorts(accessKeyConfig, portProvider); + + prometheus.collectDefaultMetrics({register: prometheus.register}); + + // Default to production metrics, as some old Docker images may not have + // SB_METRICS_URL properly set. + const metricsCollectorUrl = process.env.SB_METRICS_URL || 'https://prod.metrics.getoutline.org'; + if (!process.env.SB_METRICS_URL) { + logging.warn('process.env.SB_METRICS_URL not set, using default'); + } + + const DEFAULT_PORT = 8081; + const apiPortNumber = Number(process.env.SB_API_PORT || DEFAULT_PORT); + if (isNaN(apiPortNumber)) { + logging.error(`Invalid SB_API_PORT: ${process.env.SB_API_PORT}`); + process.exit(1); + } + portProvider.addReservedPort(apiPortNumber); + + const serverConfig = server_config.readServerConfig( + getPersistentFilename('shadowbox_server_config.json') + ); + + const proxyHostname = serverConfig.data().hostname; + if (!proxyHostname) { + logging.error('Need to specify hostname in shadowbox_server_config.json'); + process.exit(1); + } + + logging.info(`Hostname: ${proxyHostname}`); + logging.info(`SB_METRICS_URL: ${metricsCollectorUrl}`); + + const prometheusPort = await portProvider.reserveFirstFreePort(9090); + // Use 127.0.0.1 instead of localhost for Prometheus because it's resolving incorrectly for some users. + // See https://github.com/Jigsaw-Code/outline-server/issues/341 + const prometheusLocation = `127.0.0.1:${prometheusPort}`; + + const nodeMetricsPort = await portProvider.reserveFirstFreePort(prometheusPort + 1); + exportPrometheusMetrics(prometheus.register, nodeMetricsPort); + const nodeMetricsLocation = `127.0.0.1:${nodeMetricsPort}`; + + const ssMetricsPort = await portProvider.reserveFirstFreePort(nodeMetricsPort + 1); + logging.info(`Prometheus is at ${prometheusLocation}`); + logging.info(`Node metrics is at ${nodeMetricsLocation}`); + + const prometheusConfigJson = { + global: { + scrape_interval: '1m', + }, + scrape_configs: [ + {job_name: 'prometheus', static_configs: [{targets: [prometheusLocation]}]}, + {job_name: 'outline-server-main', static_configs: [{targets: [nodeMetricsLocation]}]}, + ], + }; + + const ssMetricsLocation = `127.0.0.1:${ssMetricsPort}`; + logging.info(`outline-ss-server metrics is at ${ssMetricsLocation}`); + prometheusConfigJson.scrape_configs.push({ + job_name: 'outline-server-ss', + static_configs: [{targets: [ssMetricsLocation]}], + }); + const shadowsocksServer = new OutlineShadowsocksServer( + getBinaryFilename('outline-ss-server'), + getPersistentFilename('outline-ss-server/config.yml'), + verbose, + ssMetricsLocation + ); + if (fs.existsSync(MMDB_LOCATION)) { + shadowsocksServer.enableCountryMetrics(MMDB_LOCATION); + } + + const isReplayProtectionEnabled = createRolloutTracker(serverConfig).isRolloutEnabled( + 'replay-protection', + 100 + ); + logging.info(`Replay protection enabled: ${isReplayProtectionEnabled}`); + if (isReplayProtectionEnabled) { + shadowsocksServer.enableReplayProtection(); + } + + // Start Prometheus subprocess and wait for it to be up and running. + const prometheusConfigFilename = getPersistentFilename('prometheus/config.yml'); + const prometheusTsdbFilename = getPersistentFilename('prometheus/data'); + const prometheusEndpoint = `http://${prometheusLocation}`; + const prometheusBinary = getBinaryFilename('prometheus'); + const prometheusArgs = [ + '--config.file', + prometheusConfigFilename, + '--web.enable-admin-api', + '--storage.tsdb.retention.time', + '31d', + '--storage.tsdb.path', + prometheusTsdbFilename, + '--web.listen-address', + prometheusLocation, + '--log.level', + verbose ? 'debug' : 'info', + ]; + await startPrometheus( + prometheusBinary, + prometheusConfigFilename, + prometheusConfigJson, + prometheusArgs, + prometheusEndpoint + ); + + const prometheusClient = new PrometheusClient(prometheusEndpoint); + if (!serverConfig.data().portForNewAccessKeys) { + serverConfig.data().portForNewAccessKeys = await portProvider.reserveNewPort(); + serverConfig.write(); + } + const accessKeyRepository = new ServerAccessKeyRepository( + serverConfig.data().portForNewAccessKeys, + proxyHostname, + accessKeyConfig, + shadowsocksServer, + prometheusClient, + serverConfig.data().accessKeyDataLimit + ); + + const metricsReader = new PrometheusUsageMetrics(prometheusClient); + const toMetricsId = (id: AccessKeyId) => { + try { + return accessKeyRepository.getMetricsId(id); + } catch (e) { + logging.warn(`Failed to get metrics id for access key ${id}: ${e}`); + } + }; + const managerMetrics = new PrometheusManagerMetrics(prometheusClient); + const metricsCollector = new RestMetricsCollectorClient(metricsCollectorUrl); + const metricsPublisher: SharedMetricsPublisher = new OutlineSharedMetricsPublisher( + new RealClock(), + serverConfig, + accessKeyConfig, + metricsReader, + toMetricsId, + metricsCollector + ); + const managerService = new ShadowsocksManagerService( + process.env.SB_DEFAULT_SERVER_NAME || 'Outline Server', + serverConfig, + accessKeyRepository, + managerMetrics, + metricsPublisher + ); + + const certificateFilename = process.env.SB_CERTIFICATE_FILE; + const privateKeyFilename = process.env.SB_PRIVATE_KEY_FILE; + const apiServer = restify.createServer({ + certificate: fs.readFileSync(certificateFilename), + key: fs.readFileSync(privateKeyFilename), + }); + + // Pre-routing handlers + const cors = corsMiddleware({ + origins: ['*'], + allowHeaders: [], + exposeHeaders: [], + credentials: false, + }); + apiServer.pre(cors.preflight); + apiServer.pre(restify.pre.sanitizePath()); + + // All routes handlers + const apiPrefix = process.env.SB_API_PREFIX ? `/${process.env.SB_API_PREFIX}` : ''; + apiServer.use(restify.plugins.jsonp()); + apiServer.use(restify.plugins.bodyParser({mapParams: true})); + apiServer.use(cors.actual); + bindService(apiServer, apiPrefix, managerService); + + apiServer.listen(apiPortNumber, () => { + logging.info(`Manager listening at ${apiServer.url}${apiPrefix}`); + }); + + await accessKeyRepository.start(new RealClock()); +} + +function getPersistentFilename(file: string): string { + const stateDir = process.env.SB_STATE_DIR || DEFAULT_STATE_DIR; + return path.join(stateDir, file); +} + +function getBinaryFilename(file: string): string { + const binDir = path.join(APP_BASE_DIR, 'bin'); + return path.join(binDir, file); +} + +process.on('unhandledRejection', (error: Error) => { + logging.error(`unhandledRejection: ${error.stack}`); +}); + +main().catch((error) => { + logging.error(error.stack); + process.exit(1); +}); diff --git a/src/shadowbox/server/manager_metrics.spec.ts b/src/shadowbox/server/manager_metrics.spec.ts new file mode 100644 index 000000000..d13a6c1ee --- /dev/null +++ b/src/shadowbox/server/manager_metrics.spec.ts @@ -0,0 +1,30 @@ +// Copyright 2019 The Outline Authors +// +// 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. + +import {PrometheusManagerMetrics} from './manager_metrics'; +import {FakePrometheusClient} from './mocks/mocks'; + +describe('PrometheusManagerMetrics', () => { + it('getOutboundByteTransfer', async (done) => { + const managerMetrics = new PrometheusManagerMetrics( + new FakePrometheusClient({'access-key-1': 1000, 'access-key-2': 10000}) + ); + const dataUsage = await managerMetrics.getOutboundByteTransfer({hours: 0}); + const bytesTransferredByUserId = dataUsage.bytesTransferredByUserId; + expect(Object.keys(bytesTransferredByUserId).length).toEqual(2); + expect(bytesTransferredByUserId['access-key-1']).toEqual(1000); + expect(bytesTransferredByUserId['access-key-2']).toEqual(10000); + done(); + }); +}); diff --git a/src/shadowbox/server/manager_metrics.ts b/src/shadowbox/server/manager_metrics.ts new file mode 100644 index 000000000..cb8275f5b --- /dev/null +++ b/src/shadowbox/server/manager_metrics.ts @@ -0,0 +1,43 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import {PrometheusClient} from '../infrastructure/prometheus_scraper'; +import {DataUsageByUser, DataUsageTimeframe} from '../model/metrics'; + +export interface ManagerMetrics { + getOutboundByteTransfer(timeframe: DataUsageTimeframe): Promise; +} + +// Reads manager metrics from a Prometheus instance. +export class PrometheusManagerMetrics implements ManagerMetrics { + constructor(private prometheusClient: PrometheusClient) {} + + async getOutboundByteTransfer(timeframe: DataUsageTimeframe): Promise { + // TODO(fortuna): Consider pre-computing this to save server's CPU. + // We measure only traffic leaving the server, since that's what DigitalOcean charges. + // TODO: Display all directions to admin + const result = await this.prometheusClient.query( + `sum(increase(shadowsocks_data_bytes{dir=~"ct"}[${timeframe.hours}h])) by (access_key)` + ); + const usage = {} as {[userId: string]: number}; + for (const entry of result.result) { + const bytes = Math.round(parseFloat(entry.value[1])); + if (bytes === 0) { + continue; + } + usage[entry.metric['access_key'] || ''] = bytes; + } + return {bytesTransferredByUserId: usage}; + } +} diff --git a/src/shadowbox/server/manager_service.spec.ts b/src/shadowbox/server/manager_service.spec.ts new file mode 100644 index 000000000..4e6a8dcd3 --- /dev/null +++ b/src/shadowbox/server/manager_service.spec.ts @@ -0,0 +1,1218 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import fetch from 'node-fetch'; +import * as net from 'net'; +import * as restify from 'restify'; + +import {InMemoryConfig, JsonConfig} from '../infrastructure/json_config'; +import {AccessKey, AccessKeyRepository, DataLimit} from '../model/access_key'; +import {ManagerMetrics} from './manager_metrics'; +import {bindService, ShadowsocksManagerService} from './manager_service'; +import {FakePrometheusClient, FakeShadowsocksServer} from './mocks/mocks'; +import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key'; +import {ServerConfigJson} from './server_config'; +import {SharedMetricsPublisher} from './shared_metrics'; + +interface ServerInfo { + name: string; + accessKeyDataLimit?: DataLimit; +} + +const NEW_PORT = 12345; +const OLD_PORT = 54321; +const EXPECTED_ACCESS_KEY_PROPERTIES = [ + 'id', + 'name', + 'password', + 'port', + 'method', + 'accessUrl', + 'dataLimit', +].sort(); + +const SEND_NOTHING = (_httpCode, _data) => {}; + +describe('ShadowsocksManagerService', () => { + // After processing the response callback, we should set + // responseProcessed=true. This is so we can detect that first the response + // callback is invoked, followed by the next (done) callback. + let responseProcessed = false; + beforeEach(() => { + responseProcessed = false; + }); + afterEach(() => { + expect(responseProcessed).toEqual(true); + }); + + describe('getServer', () => { + it('Return default name if name is absent', (done) => { + const repo = getAccessKeyRepository(); + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + service.getServer( + {params: {}}, + { + send: (httpCode, data: ServerInfo) => { + expect(httpCode).toEqual(200); + expect(data.name).toEqual('default name'); + responseProcessed = true; + }, + }, + done + ); + }); + it('Returns persisted properties', (done) => { + const repo = getAccessKeyRepository(); + const defaultDataLimit = {bytes: 999}; + const serverConfig = new InMemoryConfig({ + name: 'Server', + accessKeyDataLimit: defaultDataLimit, + } as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + service.getServer( + {params: {}}, + { + send: (httpCode, data: ServerInfo) => { + expect(httpCode).toEqual(200); + expect(data.name).toEqual('Server'); + expect(data.accessKeyDataLimit).toEqual(defaultDataLimit); + responseProcessed = true; + }, + }, + done + ); + }); + }); + + describe('renameServer', () => { + it('Rename changes the server name', (done) => { + const repo = getAccessKeyRepository(); + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + service.renameServer( + {params: {name: 'new name'}}, + { + send: (httpCode, _) => { + expect(httpCode).toEqual(204); + expect(serverConfig.mostRecentWrite.name).toEqual('new name'); + responseProcessed = true; + }, + }, + done + ); + }); + }); + + describe('setHostnameForAccessKeys', () => { + it(`accepts valid hostnames`, (done) => { + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(getAccessKeyRepository()) + .build(); + + const res = { + send: (httpCode) => { + expect(httpCode).toEqual(204); + }, + }; + + const goodHostnames = [ + '-bad', + 'localhost', + 'example.com', + 'www.example.org', + 'www.exa-mple.tw', + '123abc.co.uk', + '93.184.216.34', + '::0', + '2606:2800:220:1:248:1893:25c8:1946', + ]; + for (const hostname of goodHostnames) { + service.setHostnameForAccessKeys({params: {hostname}}, res, () => {}); + } + + responseProcessed = true; + done(); + }); + it(`rejects invalid hostnames`, (done) => { + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(getAccessKeyRepository()) + .build(); + + const res = {send: SEND_NOTHING}; + const next = (error) => { + expect(error.statusCode).toEqual(400); + }; + + const badHostnames = [ + null, + '', + '-abc.com', + 'abc-.com', + 'abc.com/def', + 'i_have_underscores.net', + 'gggg:ggg:220:1:248:1893:25c8:1946', + ]; + for (const hostname of badHostnames) { + service.setHostnameForAccessKeys({params: {hostname}}, res, next); + } + + responseProcessed = true; + done(); + }); + it("Changes the server's hostname", (done) => { + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(getAccessKeyRepository()) + .build(); + const hostname = 'www.example.org'; + const res = { + send: (httpCode) => { + expect(httpCode).toEqual(204); + expect(serverConfig.data().hostname).toEqual(hostname); + responseProcessed = true; + }, + }; + service.setHostnameForAccessKeys({params: {hostname}}, res, done); + }); + it('Rejects missing hostname', (done) => { + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(getAccessKeyRepository()) + .build(); + const res = {send: SEND_NOTHING}; + const next = (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; + done(); + }; + const missingHostname = {params: {}} as {params: {hostname: string}}; + service.setHostnameForAccessKeys(missingHostname, res, next); + }); + it('Rejects non-string hostname', (done) => { + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(getAccessKeyRepository()) + .build(); + const res = {send: SEND_NOTHING}; + const next = (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; + done(); + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const badHostname = {params: {hostname: 123}} as any as {params: {hostname: string}}; + service.setHostnameForAccessKeys(badHostname, res, next); + }); + }); + + describe('getAccessKey', () => { + it('Returns an access key', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const key1 = await createNewAccessKeyWithName(repo, 'keyName1'); + service.getAccessKey( + {params: {id: key1.id}}, + { + send: (httpCode, data: AccessKey) => { + expect(httpCode).toEqual(200); + expect(data.id).toEqual('0'); + responseProcessed = true; + }, + }, + done + ); + }); + + it('Returns 404 if the access key does not exist', (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + service.getAccessKey({params: {id: '1'}}, {send: () => {}}, (error) => { + expect(error.statusCode).toEqual(404); + responseProcessed = true; + done(); + }); + }); + }); + + describe('listAccessKeys', () => { + it('lists access keys in order', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + // Create 2 access keys with names. + const key1 = await createNewAccessKeyWithName(repo, 'keyName1'); + const key2 = await createNewAccessKeyWithName(repo, 'keyName2'); + // Verify that response returns keys in correct order with correct names. + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(200); + expect(data.accessKeys.length).toEqual(2); + expect(data.accessKeys[0].name).toEqual(key1.name); + expect(data.accessKeys[0].id).toEqual(key1.id); + expect(data.accessKeys[1].name).toEqual(key2.name); + expect(data.accessKeys[1].id).toEqual(key2.id); + responseProcessed = true; // required for afterEach to pass. + }, + }; + service.listAccessKeys({params: {}}, res, done); + }); + it('lists access keys with expected properties', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const accessKey = await repo.createNewAccessKey(); + await repo.createNewAccessKey(); + const accessKeyName = 'new name'; + await repo.renameAccessKey(accessKey.id, accessKeyName); + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(200); + expect(data.accessKeys.length).toEqual(2); + const serviceAccessKey1 = data.accessKeys[0]; + const serviceAccessKey2 = data.accessKeys[1]; + expect(Object.keys(serviceAccessKey1).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); + expect(Object.keys(serviceAccessKey2).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); + expect(serviceAccessKey1.name).toEqual(accessKeyName); + responseProcessed = true; // required for afterEach to pass. + }, + }; + service.listAccessKeys({params: {}}, res, done); + }); + }); + + describe('creating new access key', () => { + let repo: ServerAccessKeyRepository; + let service: ShadowsocksManagerService; + + beforeEach(() => { + repo = getAccessKeyRepository(); + service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + }); + + describe('handling the access key identifier', () => { + describe("with 'createNewAccessKey'", () => { + it('generates a unique ID', (done) => { + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(201); + expect(data.id).toEqual('0'); + responseProcessed = true; // required for afterEach to pass. + }, + }; + service.createNewAccessKey({params: {}}, res, done); + }); + it('rejects requests with ID parameter set', (done) => { + const res = {send: (_httpCode, _data) => {}}; + service.createNewAccessKey({params: {id: 'foobar'}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + }); + + describe("with 'createAccessKey'", () => { + it('rejects requests without ID parameter set', (done) => { + const res = {send: (_httpCode, _data) => {}}; + service.createAccessKey({params: {}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + it('rejects non-string ID', (done) => { + const res = {send: (_httpCode, _data) => {}}; + service.createAccessKey({params: {id: Number('9876')}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + it('rejects if key exists', async (done) => { + const accessKey = await repo.createNewAccessKey(); + const res = {send: (_httpCode, _data) => {}}; + service.createAccessKey({params: {id: accessKey.id}}, res, (error) => { + expect(error.statusCode).toEqual(409); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + it('creates key with provided ID', (done) => { + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(201); + expect(data.id).toEqual('myKeyId'); + responseProcessed = true; // required for afterEach to pass. + }, + }; + service.createAccessKey({params: {id: 'myKeyId'}}, res, done); + }); + }); + }); + + const conditions = [ + {methodName: 'createNewAccessKey', accessKeyId: undefined}, + {methodName: 'createAccessKey', accessKeyId: 'myKeyId'}, + ]; + + for (const {methodName, accessKeyId} of conditions) { + describe(`with '${methodName}'`, () => { + let serviceMethod: (req, res, next) => Promise; + + beforeEach(() => { + serviceMethod = service[methodName].bind(service); + }); + + it('verify default method', (done) => { + // Verify that response returns a key with the expected properties. + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(201); + expect(Object.keys(data).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); + expect(data.method).toEqual('chacha20-ietf-poly1305'); + responseProcessed = true; // required for afterEach to pass. + }, + }; + serviceMethod({params: {id: accessKeyId}}, res, done); + }); + it('non-default method gets set', (done) => { + // Verify that response returns a key with the expected properties. + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(201); + expect(Object.keys(data).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); + expect(data.method).toEqual('aes-256-gcm'); + responseProcessed = true; // required for afterEach to pass. + }, + }; + serviceMethod({params: {id: accessKeyId, method: 'aes-256-gcm'}}, res, done); + }); + it('use default name is params is not defined', (done) => { + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(201); + expect(data.name).toEqual(''); + responseProcessed = true; // required for afterEach to pass. + }, + }; + serviceMethod({params: {id: accessKeyId}}, res, done); + }); + it('rejects non-string name', (done) => { + const res = {send: (_httpCode, _data) => {}}; + serviceMethod({params: {id: accessKeyId, name: Number('9876')}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + it('defined name is equal to stored', (done) => { + const ACCESSKEY_NAME = 'accesskeyname'; + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(201); + expect(data.name).toEqual(ACCESSKEY_NAME); + responseProcessed = true; // required for afterEach to pass. + }, + }; + serviceMethod({params: {id: accessKeyId, name: ACCESSKEY_NAME}}, res, done); + }); + it('limit can be undefined', (done) => { + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(201); + expect(data.limit).toBeUndefined(); + responseProcessed = true; // required for afterEach to pass. + }, + }; + serviceMethod({params: {id: accessKeyId}}, res, done); + }); + it('rejects non-numeric limits', (done) => { + const ACCESSKEY_LIMIT = {bytes: '9876'}; + + const res = {send: (_httpCode, _data) => {}}; + serviceMethod({params: {id: accessKeyId, limit: ACCESSKEY_LIMIT}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + it('defined limit is equal to stored', (done) => { + const ACCESSKEY_LIMIT = {bytes: 9876}; + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(201); + expect(data.dataLimit).toEqual(ACCESSKEY_LIMIT); + responseProcessed = true; // required for afterEach to pass. + }, + }; + serviceMethod({params: {id: accessKeyId, limit: ACCESSKEY_LIMIT}}, res, done); + }); + it('method must be of type string', (done) => { + const res = {send: (_httpCode, _data) => {}}; + serviceMethod({params: {id: accessKeyId, method: Number('9876')}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + it('method must be valid', (done) => { + const res = {send: (_httpCode, _data) => {}}; + serviceMethod({params: {id: accessKeyId, method: 'abcdef'}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + it('Create returns a 500 when the repository throws an exception', (done) => { + spyOn(repo, 'createNewAccessKey').and.throwError('cannot write to disk'); + const res = {send: (_httpCode, _data) => {}}; + serviceMethod({params: {id: accessKeyId, method: 'aes-192-gcm'}}, res, (error) => { + expect(error.statusCode).toEqual(500); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + + it('generates a new password when no password is provided', async (done) => { + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(201); + expect(data.password).toBeDefined(); + responseProcessed = true; // required for afterEach to pass. + }, + }; + await serviceMethod({params: {id: accessKeyId}}, res, done); + }); + + it('uses the provided password when one is provided', async (done) => { + const PASSWORD = '8iu8V8EeoFVpwQvQeS9wiD'; + const res = { + send: (httpCode, data) => { + expect(httpCode).toEqual(201); + expect(data.password).toEqual(PASSWORD); + responseProcessed = true; // required for afterEach to pass. + }, + }; + await serviceMethod({params: {id: accessKeyId, password: PASSWORD}}, res, done); + }); + + it('rejects a password that is not a string', async (done) => { + const PASSWORD = Number.MAX_SAFE_INTEGER; + const res = {send: SEND_NOTHING}; + await serviceMethod({params: {id: accessKeyId, password: PASSWORD}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + }); + } + }); + describe('setPortForNewAccessKeys', () => { + it('changes ports for new access keys', async (done) => { + const repo = getAccessKeyRepository(); + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + + const oldKey = await repo.createNewAccessKey(); + const res = { + send: (httpCode) => { + expect(httpCode).toEqual(204); + }, + }; + await service.setPortForNewAccessKeys({params: {port: NEW_PORT}}, res, () => {}); + const newKey = await repo.createNewAccessKey(); + expect(newKey.proxyParams.portNumber).toEqual(NEW_PORT); + expect(oldKey.proxyParams.portNumber).not.toEqual(NEW_PORT); + responseProcessed = true; + done(); + }); + + it('changes the server config', async (done) => { + const repo = getAccessKeyRepository(); + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + + const res = { + send: (httpCode) => { + expect(httpCode).toEqual(204); + expect(serverConfig.data().portForNewAccessKeys).toEqual(NEW_PORT); + responseProcessed = true; + }, + }; + await service.setPortForNewAccessKeys({params: {port: NEW_PORT}}, res, done); + }); + + it('rejects invalid port numbers', async (done) => { + const repo = getAccessKeyRepository(); + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + + const res = { + send: (httpCode) => { + fail( + `setPortForNewAccessKeys should have failed with 400 Bad Request, instead succeeded with code ${httpCode}` + ); + }, + }; + const next = (error) => { + // Bad Request + expect(error.statusCode).toEqual(400); + }; + + await service.setPortForNewAccessKeys({params: {port: -1}}, res, next); + await service.setPortForNewAccessKeys({params: {port: 0}}, res, next); + await service.setPortForNewAccessKeys({params: {port: 100.1}}, res, next); + await service.setPortForNewAccessKeys({params: {port: 65536}}, res, next); + + responseProcessed = true; + done(); + }); + + it('rejects port numbers already in use', async (done) => { + const repo = getAccessKeyRepository(); + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + + const res = { + send: (httpCode) => { + fail( + `setPortForNewAccessKeys should have failed with 409 Conflict, instead succeeded with code ${httpCode}` + ); + }, + }; + const next = (error) => { + // Conflict + expect(error.statusCode).toEqual(409); + responseProcessed = true; + done(); + }; + + const server = new net.Server(); + server.listen(NEW_PORT, async () => { + await service.setPortForNewAccessKeys({params: {port: NEW_PORT}}, res, next); + }); + }); + + it('accepts port numbers already in use by access keys', async (done) => { + const repo = getAccessKeyRepository(); + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + + await service.createNewAccessKey({params: {}}, {send: () => {}}, () => {}); + await service.setPortForNewAccessKeys({params: {port: NEW_PORT}}, {send: () => {}}, () => {}); + const res = { + send: (httpCode) => { + expect(httpCode).toEqual(204); + responseProcessed = true; + }, + }; + + const firstKeyConnection = new net.Server(); + firstKeyConnection.listen(OLD_PORT, async () => { + await service.setPortForNewAccessKeys({params: {port: OLD_PORT}}, res, () => {}); + firstKeyConnection.close(); + done(); + }); + }); + + it('rejects malformed requests', async (done) => { + const repo = getAccessKeyRepository(); + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + + const noPort = {params: {}}; + const res = { + send: (httpCode) => { + fail( + `setPortForNewAccessKeys should have failed with 400 BadRequest, instead succeeded with code ${httpCode}` + ); + }, + }; + const next = (error) => { + expect(error.statusCode).toEqual(400); + }; + + await service.setPortForNewAccessKeys(noPort, res, next); + + const nonNumericPort = {params: {port: 'abc'}}; + await service.setPortForNewAccessKeys( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + nonNumericPort as any as {params: {port: number}}, + res, + next + ); + + responseProcessed = true; + done(); + }); + }); + + describe('removeAccessKey', () => { + it('removes keys', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const key1 = await repo.createNewAccessKey(); + const key2 = await repo.createNewAccessKey(); + const res = { + send: (httpCode, _data) => { + expect(httpCode).toEqual(204); + // expect that the only remaining key is the 2nd key we created. + const keys = repo.listAccessKeys(); + expect(keys.length).toEqual(1); + expect(keys[0].id === key2.id); + responseProcessed = true; // required for afterEach to pass. + }, + }; + // remove the 1st key. + service.removeAccessKey({params: {id: key1.id}}, res, done); + }); + it('Remove returns a 500 when the repository throws an exception', async (done) => { + const repo = getAccessKeyRepository(); + spyOn(repo, 'removeAccessKey').and.throwError('cannot write to disk'); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const key = await createNewAccessKeyWithName(repo, 'keyName1'); + const res = {send: (_httpCode, _data) => {}}; + service.removeAccessKey({params: {id: key.id}}, res, (error) => { + expect(error.statusCode).toEqual(500); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + }); + + describe('renameAccessKey', () => { + it('renames keys', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const OLD_NAME = 'oldName'; + const NEW_NAME = 'newName'; + + const key = await createNewAccessKeyWithName(repo, OLD_NAME); + expect(key.name === OLD_NAME); + const res = { + send: (httpCode, _) => { + expect(httpCode).toEqual(204); + expect(key.name === NEW_NAME); + responseProcessed = true; // required for afterEach to pass. + }, + }; + service.renameAccessKey({params: {id: key.id, name: NEW_NAME}}, res, done); + }); + it('Rename returns a 400 when the access key id is not a string', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + + await repo.createNewAccessKey(); + const res = {send: SEND_NOTHING}; + service.renameAccessKey({params: {id: 123}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + it('Rename returns a 500 when the repository throws an exception', async (done) => { + const repo = getAccessKeyRepository(); + spyOn(repo, 'renameAccessKey').and.throwError('cannot write to disk'); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + + const key = await createNewAccessKeyWithName(repo, 'oldName'); + const res = {send: SEND_NOTHING}; + service.renameAccessKey({params: {id: key.id, name: 'newName'}}, res, (error) => { + expect(error.statusCode).toEqual(500); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + }); + + describe('setAccessKeyDataLimit', () => { + it('sets access key data limit', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const key = await repo.createNewAccessKey(); + const limit = {bytes: 1000}; + const res = { + send: (httpCode) => { + expect(httpCode).toEqual(204); + expect(key.dataLimit.bytes).toEqual(1000); + responseProcessed = true; + done(); + }, + }; + service.setAccessKeyDataLimit({params: {id: key.id, limit}}, res, () => {}); + }); + + it('rejects negative numbers', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const keyId = (await repo.createNewAccessKey()).id; + const limit = {bytes: -1}; + service.setAccessKeyDataLimit({params: {id: keyId, limit}}, {send: () => {}}, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; + done(); + }); + }); + + it('rejects non-numeric limits', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const keyId = (await repo.createNewAccessKey()).id; + const limit = {bytes: '1'}; + service.setAccessKeyDataLimit({params: {id: keyId, limit}}, {send: () => {}}, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; + done(); + }); + }); + + it('rejects an empty request', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const keyId = (await repo.createNewAccessKey()).id; + const limit = {} as DataLimit; + service.setAccessKeyDataLimit({params: {id: keyId, limit}}, {send: () => {}}, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; + done(); + }); + }); + + it('rejects requests for nonexistent keys', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + await repo.createNewAccessKey(); + const limit: DataLimit = {bytes: 1000}; + service.setAccessKeyDataLimit( + {params: {id: 'not an id', limit}}, + {send: () => {}}, + (error) => { + expect(error.statusCode).toEqual(404); + responseProcessed = true; + done(); + } + ); + }); + }); + + describe('removeAccessKeyDataLimit', () => { + it('removes an access key data limit', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const key = await repo.createNewAccessKey(); + repo.setAccessKeyDataLimit(key.id, {bytes: 1000}); + await repo.enforceAccessKeyDataLimits(); + const res = { + send: (httpCode) => { + expect(httpCode).toEqual(204); + expect(key.dataLimit).toBeFalsy(); + responseProcessed = true; + done(); + }, + }; + service.removeAccessKeyDataLimit({params: {id: key.id}}, res, () => {}); + }); + it('returns 404 for a nonexistent key', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + await repo.createNewAccessKey(); + service.removeAccessKeyDataLimit({params: {id: 'not an id'}}, {send: () => {}}, (error) => { + expect(error.statusCode).toEqual(404); + responseProcessed = true; + done(); + }); + }); + }); + + describe('setDefaultDataLimit', () => { + it('sets default data limit', async (done) => { + const serverConfig = new InMemoryConfig({} as ServerConfigJson); + const repo = getAccessKeyRepository(); + spyOn(repo, 'setDefaultDataLimit'); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + const limit = {bytes: 10000}; + const res = { + send: (httpCode, _data) => { + expect(httpCode).toEqual(204); + expect(serverConfig.data().accessKeyDataLimit).toEqual(limit); + expect(repo.setDefaultDataLimit).toHaveBeenCalledWith(limit); + service.getServer( + {params: {}}, + { + send: (httpCode, data: ServerInfo) => { + expect(httpCode).toEqual(200); + expect(data.accessKeyDataLimit).toEqual(limit); + responseProcessed = true; // required for afterEach to pass. + }, + }, + done + ); + }, + }; + service.setDefaultDataLimit({params: {limit}}, res, done); + }); + it('returns 400 when limit is missing values', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + await repo.createNewAccessKey(); + const limit = {} as DataLimit; + const res = {send: SEND_NOTHING}; + service.setDefaultDataLimit({params: {limit}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + it('returns 400 when limit has negative values', async (done) => { + const repo = getAccessKeyRepository(); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + await repo.createNewAccessKey(); + const limit = {bytes: -1}; + const res = {send: SEND_NOTHING}; + service.setDefaultDataLimit({params: {limit}}, res, (error) => { + expect(error.statusCode).toEqual(400); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + it('returns 500 when the repository throws an exception', async (done) => { + const repo = getAccessKeyRepository(); + spyOn(repo, 'setDefaultDataLimit').and.throwError('cannot write to disk'); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + await repo.createNewAccessKey(); + const limit = {bytes: 10000}; + const res = {send: SEND_NOTHING}; + service.setDefaultDataLimit({params: {limit}}, res, (error) => { + expect(error.statusCode).toEqual(500); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + }); + + describe('removeDefaultDataLimit', () => { + it('clears default data limit', async (done) => { + const limit = {bytes: 10000}; + const serverConfig = new InMemoryConfig({accessKeyDataLimit: limit} as ServerConfigJson); + const repo = getAccessKeyRepository(); + spyOn(repo, 'removeDefaultDataLimit').and.callThrough(); + const service = new ShadowsocksManagerServiceBuilder() + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); + await repo.setDefaultDataLimit(limit); + const res = { + send: (httpCode, _data) => { + expect(httpCode).toEqual(204); + expect(serverConfig.data().accessKeyDataLimit).toBeUndefined(); + expect(repo.removeDefaultDataLimit).toHaveBeenCalled(); + responseProcessed = true; // required for afterEach to pass. + }, + }; + service.removeDefaultDataLimit({params: {}}, res, done); + }); + it('returns 500 when the repository throws an exception', async (done) => { + const repo = getAccessKeyRepository(); + spyOn(repo, 'removeDefaultDataLimit').and.throwError('cannot write to disk'); + const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); + const accessKey = await repo.createNewAccessKey(); + const res = {send: SEND_NOTHING}; + service.removeDefaultDataLimit({params: {id: accessKey.id}}, res, (error) => { + expect(error.statusCode).toEqual(500); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + }); + + describe('getShareMetrics', () => { + it('Returns value from sharedMetrics', (done) => { + const sharedMetrics = fakeSharedMetricsReporter(); + sharedMetrics.startSharing(); + const service = new ShadowsocksManagerServiceBuilder() + .metricsPublisher(sharedMetrics) + .build(); + service.getShareMetrics( + {params: {}}, + { + send: (httpCode, data: {metricsEnabled: boolean}) => { + expect(httpCode).toEqual(200); + expect(data.metricsEnabled).toEqual(true); + responseProcessed = true; + }, + }, + done + ); + }); + }); + describe('setShareMetrics', () => { + it('Sets value in the config', (done) => { + const sharedMetrics = fakeSharedMetricsReporter(); + sharedMetrics.stopSharing(); + const service = new ShadowsocksManagerServiceBuilder() + .metricsPublisher(sharedMetrics) + .build(); + service.setShareMetrics( + {params: {metricsEnabled: true}}, + { + send: (httpCode, _) => { + expect(httpCode).toEqual(204); + expect(sharedMetrics.isSharingEnabled()).toEqual(true); + responseProcessed = true; + }, + }, + done + ); + }); + }); +}); + +describe('bindService', () => { + let server: restify.Server; + let service: ShadowsocksManagerService; + let url: URL; + const PREFIX = '/TestApiPrefix'; + + const fakeResponse = {foo: 'bar'}; + const fakeHandler = async (req, res, next) => { + res.send(200, fakeResponse); + next(); + }; + + beforeEach(() => { + server = restify.createServer(); + service = new ShadowsocksManagerServiceBuilder().build(); + server.listen(0); + url = new URL(server.url); + }); + + afterEach(() => { + server.close(); + }); + + it('basic routing', async () => { + spyOn(service, 'renameServer').and.callFake(fakeHandler); + bindService(server, PREFIX, service); + + url.pathname = `${PREFIX}/name`; + const response = await fetch(url, {method: 'put'}); + const body = await response.json(); + + expect(body).toEqual(fakeResponse); + expect(service.renameServer).toHaveBeenCalled(); + }); + + it('parameterized routing', async () => { + spyOn(service, 'removeAccessKeyDataLimit').and.callFake(fakeHandler); + bindService(server, PREFIX, service); + + url.pathname = `${PREFIX}/access-keys/fake-access-key-id/data-limit`; + const response = await fetch(url, {method: 'delete'}); + const body = await response.json(); + + expect(body).toEqual(fakeResponse); + expect(service.removeAccessKeyDataLimit).toHaveBeenCalled(); + }); + + // Verify that we have consistent 404 behavior for all inputs. + [ + '/', + '/TestApiPre', + '/foo', + '/TestApiPrefix123', + '/123TestApiPrefix', + '/very-long-path-that-does-not-exist', + `${PREFIX}/does-not-exist`, + ].forEach((path) => { + it(`404 (${path})`, async () => { + // Ensure no methods are called on the Service. + spyOnAllFunctions(service); + jasmine.setDefaultSpyStrategy(fail); + bindService(server, PREFIX, service); + + url.pathname = path; + const response = await fetch(url); + const body = await response.json(); + + expect(response.status).toEqual(404); + expect(body).toEqual({ + code: 'ResourceNotFound', + message: `${path} does not exist`, + }); + }); + }); + + // This is primarily a reverse testcase for the unauthorized case. + it(`standard routing for authorized queries`, async () => { + bindService(server, PREFIX, service); + // Verify that ordinary routing goes through the Router. + spyOn(server.router, 'lookup').and.callThrough(); + + // This is an authorized request, so it will pass the prefix filter + // and reach the Router. + url.pathname = `${PREFIX}`; + const response = await fetch(url); + expect(response.status).toEqual(404); + await response.json(); + + expect(server.router.lookup).toHaveBeenCalled(); + }); + + // Check that unauthorized queries are rejected without ever reaching + // the routing stage. + ['/', '/T', '/TestApiPre', '/TestApi123456', '/TestApi123456789'].forEach((path) => { + it(`no routing for unauthorized queries (${path})`, async () => { + bindService(server, PREFIX, service); + // Ensure no methods are called on the Router. + spyOnAllFunctions(server.router); + jasmine.setDefaultSpyStrategy(fail); + + // Try bare pathname. + url.pathname = path; + const response1 = await fetch(url); + expect(response1.status).toEqual(404); + await response1.json(); + + // Try a subpath that would exist if this were a valid prefix + url.pathname = `${path}/server`; + const response2 = await fetch(url); + expect(response2.status).toEqual(404); + await response2.json(); + + // Try an arbitrary subpath + url.pathname = `${path}/does-not-exist`; + const response3 = await fetch(url); + expect(response3.status).toEqual(404); + await response3.json(); + }); + }); +}); + +class ShadowsocksManagerServiceBuilder { + private defaultServerName_ = 'default name'; + private serverConfig_: JsonConfig = null; + private accessKeys_: AccessKeyRepository = null; + private managerMetrics_: ManagerMetrics = null; + private metricsPublisher_: SharedMetricsPublisher = null; + + defaultServerName(name: string): ShadowsocksManagerServiceBuilder { + this.defaultServerName_ = name; + return this; + } + + serverConfig(config: JsonConfig): ShadowsocksManagerServiceBuilder { + this.serverConfig_ = config; + return this; + } + + accessKeys(keys: AccessKeyRepository): ShadowsocksManagerServiceBuilder { + this.accessKeys_ = keys; + return this; + } + + managerMetrics(metrics: ManagerMetrics): ShadowsocksManagerServiceBuilder { + this.managerMetrics_ = metrics; + return this; + } + + metricsPublisher(publisher: SharedMetricsPublisher): ShadowsocksManagerServiceBuilder { + this.metricsPublisher_ = publisher; + return this; + } + + build(): ShadowsocksManagerService { + return new ShadowsocksManagerService( + this.defaultServerName_, + this.serverConfig_, + this.accessKeys_, + this.managerMetrics_, + this.metricsPublisher_ + ); + } +} + +async function createNewAccessKeyWithName( + repo: AccessKeyRepository, + name: string +): Promise { + const accessKey = await repo.createNewAccessKey(); + try { + repo.renameAccessKey(accessKey.id, name); + } catch (e) { + // Ignore; writing to disk is expected to fail in some of the tests. + } + return accessKey; +} + +function fakeSharedMetricsReporter(): SharedMetricsPublisher { + let sharing = false; + return { + startSharing() { + sharing = true; + }, + stopSharing() { + sharing = false; + }, + isSharingEnabled(): boolean { + return sharing; + }, + }; +} + +function getAccessKeyRepository(): ServerAccessKeyRepository { + return new ServerAccessKeyRepository( + OLD_PORT, + 'hostname', + new InMemoryConfig({accessKeys: [], nextId: 0}), + new FakeShadowsocksServer(), + new FakePrometheusClient({}) + ); +} diff --git a/src/shadowbox/server/manager_service.ts b/src/shadowbox/server/manager_service.ts new file mode 100644 index 000000000..45fa905bd --- /dev/null +++ b/src/shadowbox/server/manager_service.ts @@ -0,0 +1,632 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as crypto from 'crypto'; +import * as ipRegex from 'ip-regex'; +import * as restify from 'restify'; +import * as restifyErrors from 'restify-errors'; +import {makeConfig, SIP002_URI} from 'outline-shadowsocksconfig'; + +import {JsonConfig} from '../infrastructure/json_config'; +import * as logging from '../infrastructure/logging'; +import {AccessKey, AccessKeyRepository, DataLimit} from '../model/access_key'; +import * as errors from '../model/errors'; +import {version} from '../package.json'; + +import {ManagerMetrics} from './manager_metrics'; +import {ServerConfigJson} from './server_config'; +import {SharedMetricsPublisher} from './shared_metrics'; + +interface AccessKeyJson { + // The unique identifier of this access key. + id: string; + // Admin-controlled, editable name for this access key. + name: string; + // Shadowsocks-specific details and credentials. + password: string; + port: number; + method: string; + dataLimit: DataLimit; + accessUrl: string; +} + +// Creates a AccessKey response. +function accessKeyToApiJson(accessKey: AccessKey): AccessKeyJson { + return { + id: accessKey.id, + name: accessKey.name, + password: accessKey.proxyParams.password, + port: accessKey.proxyParams.portNumber, + method: accessKey.proxyParams.encryptionMethod, + dataLimit: accessKey.dataLimit, + accessUrl: SIP002_URI.stringify( + makeConfig({ + host: accessKey.proxyParams.hostname, + port: accessKey.proxyParams.portNumber, + method: accessKey.proxyParams.encryptionMethod, + password: accessKey.proxyParams.password, + outline: 1, + }) + ), + }; +} + +// Type to reflect that we receive untyped JSON request parameters. +interface RequestParams { + // Supported parameters: + // id: string + // name: string + // metricsEnabled: boolean + // limit: DataLimit + // port: number + // hours: number + // method: string + [param: string]: unknown; +} +// Simplified request and response type interfaces containing only the +// properties we actually use, to make testing easier. +interface RequestType { + params: RequestParams; +} +interface ResponseType { + send(code: number, data?: {}): void; +} + +enum HttpSuccess { + OK = 200, + NO_CONTENT = 204, +} + +// Similar to String.startsWith(), but constant-time. +function timingSafeStartsWith(input: string, prefix: string): boolean { + const prefixBuf = Buffer.from(prefix); + const inputBuf = Buffer.from(input); + const L = Math.min(inputBuf.length, prefixBuf.length); + const inputOverlap = inputBuf.slice(0, L); + const prefixOverlap = prefixBuf.slice(0, L); + const match = crypto.timingSafeEqual(inputOverlap, prefixOverlap); + return inputBuf.length >= prefixBuf.length && match; +} + +// Returns a pre-routing hook that injects a 404 if the request does not +// start with `apiPrefix`. This filter runs in constant time. +function prefixFilter(apiPrefix: string): restify.RequestHandler { + return (req: restify.Request, res: restify.Response, next: restify.Next) => { + if (timingSafeStartsWith(req.path(), apiPrefix)) { + return next(); + } + // This error matches the router's default 404 response. + next(new restifyErrors.ResourceNotFoundError('%s does not exist', req.path())); + }; +} + +export function bindService( + apiServer: restify.Server, + apiPrefix: string, + service: ShadowsocksManagerService +) { + // Reject unauthorized requests in constant time before they reach the routing step. + apiServer.pre(prefixFilter(apiPrefix)); + + apiServer.put(`${apiPrefix}/name`, service.renameServer.bind(service)); + apiServer.get(`${apiPrefix}/server`, service.getServer.bind(service)); + apiServer.put( + `${apiPrefix}/server/access-key-data-limit`, + service.setDefaultDataLimit.bind(service) + ); + apiServer.del( + `${apiPrefix}/server/access-key-data-limit`, + service.removeDefaultDataLimit.bind(service) + ); + apiServer.put( + `${apiPrefix}/server/hostname-for-access-keys`, + service.setHostnameForAccessKeys.bind(service) + ); + apiServer.put( + `${apiPrefix}/server/port-for-new-access-keys`, + service.setPortForNewAccessKeys.bind(service) + ); + + apiServer.post(`${apiPrefix}/access-keys`, service.createNewAccessKey.bind(service)); + apiServer.put(`${apiPrefix}/access-keys/:id`, service.createAccessKey.bind(service)); + apiServer.get(`${apiPrefix}/access-keys`, service.listAccessKeys.bind(service)); + + apiServer.get(`${apiPrefix}/access-keys/:id`, service.getAccessKey.bind(service)); + apiServer.del(`${apiPrefix}/access-keys/:id`, service.removeAccessKey.bind(service)); + apiServer.put(`${apiPrefix}/access-keys/:id/name`, service.renameAccessKey.bind(service)); + apiServer.put( + `${apiPrefix}/access-keys/:id/data-limit`, + service.setAccessKeyDataLimit.bind(service) + ); + apiServer.del( + `${apiPrefix}/access-keys/:id/data-limit`, + service.removeAccessKeyDataLimit.bind(service) + ); + + apiServer.get(`${apiPrefix}/metrics/transfer`, service.getDataUsage.bind(service)); + apiServer.get(`${apiPrefix}/metrics/enabled`, service.getShareMetrics.bind(service)); + apiServer.put(`${apiPrefix}/metrics/enabled`, service.setShareMetrics.bind(service)); + + // Redirect former experimental APIs + apiServer.put( + `${apiPrefix}/experimental/access-key-data-limit`, + redirect(`${apiPrefix}/server/access-key-data-limit`) + ); + apiServer.del( + `${apiPrefix}/experimental/access-key-data-limit`, + redirect(`${apiPrefix}/server/access-key-data-limit`) + ); +} + +// Returns a request handler that redirects a bound request path to `url` with HTTP status code 308. +function redirect(url: string): restify.RequestHandlerType { + return (req: restify.Request, res: restify.Response, next: restify.Next) => { + logging.debug(`Redirecting ${req.url} => ${url}`); + res.redirect(308, url, next); + }; +} + +function validateAccessKeyId(accessKeyId: unknown): string { + if (!accessKeyId) { + throw new restifyErrors.MissingParameterError({statusCode: 400}, 'Parameter `id` is missing'); + } else if (typeof accessKeyId !== 'string') { + throw new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + 'Parameter `id` must be of type string' + ); + } + return accessKeyId; +} + +function validateDataLimit(limit: unknown): DataLimit { + if (typeof limit === 'undefined') { + return undefined; + } + + const bytes = (limit as DataLimit).bytes; + if (!(Number.isInteger(bytes) && bytes >= 0)) { + throw new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + '`limit.bytes` must be an non-negative integer' + ); + } + return limit as DataLimit; +} + +function validateStringParam(param: unknown, paramName: string): string { + if (typeof param === 'undefined') { + return undefined; + } + + if (typeof param !== 'string') { + throw new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + `Expected a string for ${paramName}, instead got ${param} of type ${typeof param}` + ); + } + return param; +} + +// The ShadowsocksManagerService manages the access keys that can use the server +// as a proxy using Shadowsocks. It runs an instance of the Shadowsocks server +// for each existing access key, with the port and password assigned for that access key. +export class ShadowsocksManagerService { + constructor( + private defaultServerName: string, + private serverConfig: JsonConfig, + private accessKeys: AccessKeyRepository, + private managerMetrics: ManagerMetrics, + private metricsPublisher: SharedMetricsPublisher + ) {} + + public renameServer(req: RequestType, res: ResponseType, next: restify.Next): void { + logging.debug(`renameServer request ${JSON.stringify(req.params)}`); + const name = req.params.name; + if (!name) { + return next( + new restifyErrors.MissingParameterError({statusCode: 400}, 'Parameter `name` is missing') + ); + } + if (typeof name !== 'string' || name.length > 100) { + next( + new restifyErrors.InvalidArgumentError( + `Requested server name should be a string <= 100 characters long. Got ${name}` + ) + ); + return; + } + this.serverConfig.data().name = name; + this.serverConfig.write(); + res.send(HttpSuccess.NO_CONTENT); + next(); + } + + public getServer(req: RequestType, res: ResponseType, next: restify.Next): void { + res.send(HttpSuccess.OK, { + name: this.serverConfig.data().name || this.defaultServerName, + serverId: this.serverConfig.data().serverId, + metricsEnabled: this.serverConfig.data().metricsEnabled || false, + createdTimestampMs: this.serverConfig.data().createdTimestampMs, + version, + accessKeyDataLimit: this.serverConfig.data().accessKeyDataLimit, + portForNewAccessKeys: this.serverConfig.data().portForNewAccessKeys, + hostnameForAccessKeys: this.serverConfig.data().hostname, + }); + next(); + } + + // Changes the server's hostname. Hostname must be a valid domain or IP address + public setHostnameForAccessKeys(req: RequestType, res: ResponseType, next: restify.Next): void { + logging.debug(`changeHostname request: ${JSON.stringify(req.params)}`); + + const hostname = req.params.hostname; + if (typeof hostname === 'undefined') { + return next( + new restifyErrors.MissingParameterError({statusCode: 400}, 'hostname must be provided') + ); + } + if (typeof hostname !== 'string') { + return next( + new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + `Expected hostname to be a string, instead got ${hostname} of type ${typeof hostname}` + ) + ); + } + // Hostnames can have any number of segments of alphanumeric characters and hyphens, separated + // by periods. No segment may start or end with a hyphen. + const hostnameRegex = + /^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)*[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$/; + if (!hostnameRegex.test(hostname) && !ipRegex({includeBoundaries: true}).test(hostname)) { + return next( + new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + `Hostname ${hostname} isn't a valid hostname or IP address` + ) + ); + } + + this.serverConfig.data().hostname = hostname; + this.serverConfig.write(); + this.accessKeys.setHostname(hostname); + res.send(HttpSuccess.NO_CONTENT); + next(); + } + + // Get a access key + public getAccessKey(req: RequestType, res: ResponseType, next: restify.Next): void { + try { + logging.debug(`getAccessKey request ${JSON.stringify(req.params)}`); + const accessKeyId = validateAccessKeyId(req.params.id); + const accessKey = this.accessKeys.getAccessKey(accessKeyId); + const accessKeyJson = accessKeyToApiJson(accessKey); + + logging.debug(`getAccessKey response ${JSON.stringify(accessKeyJson)}`); + res.send(HttpSuccess.OK, accessKeyJson); + return next(); + } catch (error) { + logging.error(error); + if (error instanceof errors.AccessKeyNotFound) { + return next(new restifyErrors.NotFoundError(error.message)); + } + return next(error); + } + } + + // Lists all access keys + public listAccessKeys(req: RequestType, res: ResponseType, next: restify.Next): void { + logging.debug(`listAccessKeys request ${JSON.stringify(req.params)}`); + const response = {accessKeys: []}; + for (const accessKey of this.accessKeys.listAccessKeys()) { + response.accessKeys.push(accessKeyToApiJson(accessKey)); + } + logging.debug(`listAccessKeys response ${JSON.stringify(response)}`); + res.send(HttpSuccess.OK, response); + return next(); + } + + private async createAccessKeyFromRequest(req: RequestType, id?: string): Promise { + try { + const encryptionMethod = validateStringParam(req.params.method || '', 'encryptionMethod'); + const name = validateStringParam(req.params.name || '', 'name'); + const dataLimit = validateDataLimit(req.params.limit); + const password = validateStringParam(req.params.password, 'password'); + + const accessKeyJson = accessKeyToApiJson( + await this.accessKeys.createNewAccessKey({ + encryptionMethod, + id, + name, + dataLimit, + password, + }) + ); + return accessKeyJson; + } catch (error) { + logging.error(error); + if (error instanceof errors.InvalidCipher) { + throw new restifyErrors.InvalidArgumentError({statusCode: 400}, error.message); + } + throw error; + } + } + + // Creates a new access key + public async createNewAccessKey( + req: RequestType, + res: ResponseType, + next: restify.Next + ): Promise { + try { + logging.debug(`createNewAccessKey request ${JSON.stringify(req.params)}`); + if (req.params.id) { + return next( + new restifyErrors.InvalidArgumentError({statusCode: 400}, 'Parameter `id` is not allowed') + ); + } + const accessKeyJson = await this.createAccessKeyFromRequest(req); + res.send(201, accessKeyJson); + logging.debug(`createNewAccessKey response ${JSON.stringify(accessKeyJson)}`); + return next(); + } catch (error) { + logging.error(error); + if ( + error instanceof restifyErrors.InvalidArgumentError || + error instanceof restifyErrors.MissingParameterError + ) { + return next(error); + } + return next(new restifyErrors.InternalServerError()); + } + } + + // Creates an access key with a specific identifier + public async createAccessKey( + req: RequestType, + res: ResponseType, + next: restify.Next + ): Promise { + try { + logging.debug(`createAccessKey request ${JSON.stringify(req.params)}`); + const accessKeyId = validateAccessKeyId(req.params.id); + const accessKeyJson = await this.createAccessKeyFromRequest(req, accessKeyId); + res.send(201, accessKeyJson); + logging.debug(`createAccessKey response ${JSON.stringify(accessKeyJson)}`); + return next(); + } catch (error) { + logging.error(error); + if (error instanceof errors.AccessKeyConflict) { + return next(new restifyErrors.ConflictError(error.message)); + } + if ( + error instanceof restifyErrors.InvalidArgumentError || + error instanceof restifyErrors.MissingParameterError + ) { + return next(error); + } + return next(new restifyErrors.InternalServerError()); + } + } + + // Sets the default ports for new access keys + public async setPortForNewAccessKeys( + req: RequestType, + res: ResponseType, + next: restify.Next + ): Promise { + try { + logging.debug(`setPortForNewAccessKeys request ${JSON.stringify(req.params)}`); + const port = req.params.port; + if (!port) { + return next( + new restifyErrors.MissingParameterError({statusCode: 400}, 'Parameter `port` is missing') + ); + } else if (typeof port !== 'number') { + return next( + new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + `Expected a numeric port, instead got ${port} of type ${typeof port}` + ) + ); + } + await this.accessKeys.setPortForNewAccessKeys(port); + this.serverConfig.data().portForNewAccessKeys = port; + this.serverConfig.write(); + res.send(HttpSuccess.NO_CONTENT); + next(); + } catch (error) { + logging.error(error); + if (error instanceof errors.InvalidPortNumber) { + return next(new restifyErrors.InvalidArgumentError({statusCode: 400}, error.message)); + } else if (error instanceof errors.PortUnavailable) { + return next(new restifyErrors.ConflictError(error.message)); + } + return next(new restifyErrors.InternalServerError(error)); + } + } + + // Removes an existing access key + public removeAccessKey(req: RequestType, res: ResponseType, next: restify.Next): void { + try { + logging.debug(`removeAccessKey request ${JSON.stringify(req.params)}`); + const accessKeyId = validateAccessKeyId(req.params.id); + this.accessKeys.removeAccessKey(accessKeyId); + res.send(HttpSuccess.NO_CONTENT); + return next(); + } catch (error) { + logging.error(error); + if (error instanceof errors.AccessKeyNotFound) { + return next(new restifyErrors.NotFoundError(error.message)); + } else if (error instanceof restifyErrors.HttpError) { + return next(error); + } + return next(new restifyErrors.InternalServerError()); + } + } + + public renameAccessKey(req: RequestType, res: ResponseType, next: restify.Next): void { + try { + logging.debug(`renameAccessKey request ${JSON.stringify(req.params)}`); + const accessKeyId = validateAccessKeyId(req.params.id); + const name = req.params.name; + if (!name) { + return next( + new restifyErrors.MissingParameterError({statusCode: 400}, 'Parameter `name` is missing') + ); + } else if (typeof name !== 'string') { + return next( + new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + 'Parameter `name` must be of type string' + ) + ); + } + this.accessKeys.renameAccessKey(accessKeyId, name); + res.send(HttpSuccess.NO_CONTENT); + return next(); + } catch (error) { + logging.error(error); + if (error instanceof errors.AccessKeyNotFound) { + return next(new restifyErrors.NotFoundError(error.message)); + } else if (error instanceof restifyErrors.HttpError) { + return next(error); + } + return next(new restifyErrors.InternalServerError()); + } + } + + public async setAccessKeyDataLimit(req: RequestType, res: ResponseType, next: restify.Next) { + try { + logging.debug(`setAccessKeyDataLimit request ${JSON.stringify(req.params)}`); + const accessKeyId = validateAccessKeyId(req.params.id); + const limit = validateDataLimit(req.params.limit); + // Enforcement is done asynchronously in the proxy server. This is transparent to the manager + // so this doesn't introduce any race conditions between the server and UI. + this.accessKeys.setAccessKeyDataLimit(accessKeyId, limit); + res.send(HttpSuccess.NO_CONTENT); + return next(); + } catch (error) { + logging.error(error); + if (error instanceof errors.AccessKeyNotFound) { + return next(new restifyErrors.NotFoundError(error.message)); + } + return next(error); + } + } + + public async removeAccessKeyDataLimit(req: RequestType, res: ResponseType, next: restify.Next) { + try { + logging.debug(`removeAccessKeyDataLimit request ${JSON.stringify(req.params)}`); + const accessKeyId = validateAccessKeyId(req.params.id); + // Enforcement is done asynchronously in the proxy server. This is transparent to the manager + // so this doesn't introduce any race conditions between the server and UI. + this.accessKeys.removeAccessKeyDataLimit(accessKeyId); + res.send(HttpSuccess.NO_CONTENT); + return next(); + } catch (error) { + logging.error(error); + if (error instanceof errors.AccessKeyNotFound) { + return next(new restifyErrors.NotFoundError(error.message)); + } + return next(error); + } + } + + public async setDefaultDataLimit(req: RequestType, res: ResponseType, next: restify.Next) { + try { + logging.debug(`setDefaultDataLimit request ${JSON.stringify(req.params)}`); + const limit = validateDataLimit(req.params.limit); + // Enforcement is done asynchronously in the proxy server. This is transparent to the manager + // so this doesn't introduce any race conditions between the server and UI. + this.accessKeys.setDefaultDataLimit(limit); + this.serverConfig.data().accessKeyDataLimit = limit; + this.serverConfig.write(); + res.send(HttpSuccess.NO_CONTENT); + return next(); + } catch (error) { + logging.error(error); + if ( + error instanceof restifyErrors.InvalidArgumentError || + error instanceof restifyErrors.MissingParameterError + ) { + return next(error); + } + return next(new restifyErrors.InternalServerError()); + } + } + + public async removeDefaultDataLimit(req: RequestType, res: ResponseType, next: restify.Next) { + try { + logging.debug(`removeDefaultDataLimit request ${JSON.stringify(req.params)}`); + // Enforcement is done asynchronously in the proxy server. This is transparent to the manager + // so this doesn't introduce any race conditions between the server and UI. + this.accessKeys.removeDefaultDataLimit(); + delete this.serverConfig.data().accessKeyDataLimit; + this.serverConfig.write(); + res.send(HttpSuccess.NO_CONTENT); + return next(); + } catch (error) { + logging.error(error); + return next(new restifyErrors.InternalServerError()); + } + } + + public async getDataUsage(req: RequestType, res: ResponseType, next: restify.Next) { + try { + logging.debug(`getDataUsage request ${JSON.stringify(req.params)}`); + const response = await this.managerMetrics.getOutboundByteTransfer({hours: 30 * 24}); + res.send(HttpSuccess.OK, response); + logging.debug(`getDataUsage response ${JSON.stringify(response)}`); + return next(); + } catch (error) { + logging.error(error); + return next(new restifyErrors.InternalServerError()); + } + } + + public getShareMetrics(req: RequestType, res: ResponseType, next: restify.Next): void { + logging.debug(`getShareMetrics request ${JSON.stringify(req.params)}`); + const response = {metricsEnabled: this.metricsPublisher.isSharingEnabled()}; + res.send(HttpSuccess.OK, response); + logging.debug(`getShareMetrics response: ${JSON.stringify(response)}`); + next(); + } + + public setShareMetrics(req: RequestType, res: ResponseType, next: restify.Next): void { + logging.debug(`setShareMetrics request ${JSON.stringify(req.params)}`); + const metricsEnabled = req.params.metricsEnabled; + if (metricsEnabled === undefined || metricsEnabled === null) { + return next( + new restifyErrors.MissingParameterError( + {statusCode: 400}, + 'Parameter `metricsEnabled` is missing' + ) + ); + } else if (typeof metricsEnabled !== 'boolean') { + return next( + new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + 'Parameter `hours` must be an integer' + ) + ); + } + if (metricsEnabled) { + this.metricsPublisher.startSharing(); + } else { + this.metricsPublisher.stopSharing(); + } + res.send(HttpSuccess.NO_CONTENT); + next(); + } +} diff --git a/src/shadowbox/server/mocks/mocks.ts b/src/shadowbox/server/mocks/mocks.ts new file mode 100644 index 000000000..248cd55b0 --- /dev/null +++ b/src/shadowbox/server/mocks/mocks.ts @@ -0,0 +1,67 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import {PrometheusClient, QueryResultData} from '../../infrastructure/prometheus_scraper'; +import {ShadowsocksAccessKey, ShadowsocksServer} from '../../model/shadowsocks_server'; +import {TextFile} from '../../infrastructure/text_file'; + +export class InMemoryFile implements TextFile { + private savedText: string; + constructor(private exists: boolean) {} + readFileSync() { + if (this.exists) { + return this.savedText; + } else { + const err = new Error('no such file or directory'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err as any).code = 'ENOENT'; + throw err; + } + } + writeFileSync(text: string) { + this.savedText = text; + this.exists = true; + } +} + +export class FakeShadowsocksServer implements ShadowsocksServer { + private accessKeys: ShadowsocksAccessKey[] = []; + + update(keys: ShadowsocksAccessKey[]) { + this.accessKeys = keys; + return Promise.resolve(); + } + + getAccessKeys() { + return this.accessKeys; + } +} + +export class FakePrometheusClient extends PrometheusClient { + constructor(public bytesTransferredById: {[accessKeyId: string]: number}) { + super(''); + } + + async query(_query: string): Promise { + const queryResultData = {result: []} as QueryResultData; + for (const accessKeyId of Object.keys(this.bytesTransferredById)) { + const bytesTransferred = this.bytesTransferredById[accessKeyId] || 0; + queryResultData.result.push({ + metric: {access_key: accessKeyId}, + value: [bytesTransferred, `${bytesTransferred}`], + }); + } + return queryResultData; + } +} diff --git a/src/shadowbox/server/outline_shadowsocks_server.ts b/src/shadowbox/server/outline_shadowsocks_server.ts new file mode 100644 index 000000000..da27c874f --- /dev/null +++ b/src/shadowbox/server/outline_shadowsocks_server.ts @@ -0,0 +1,123 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as child_process from 'child_process'; +import * as jsyaml from 'js-yaml'; +import * as mkdirp from 'mkdirp'; +import * as path from 'path'; + +import * as file from '../infrastructure/file'; +import * as logging from '../infrastructure/logging'; +import {ShadowsocksAccessKey, ShadowsocksServer} from '../model/shadowsocks_server'; + +// Runs outline-ss-server. +export class OutlineShadowsocksServer implements ShadowsocksServer { + private ssProcess: child_process.ChildProcess; + private ipCountryFilename = ''; + private isReplayProtectionEnabled = false; + + // binaryFilename is the location for the outline-ss-server binary. + // configFilename is the location for the outline-ss-server config. + constructor( + private readonly binaryFilename: string, + private readonly configFilename: string, + private readonly verbose: boolean, + private readonly metricsLocation: string + ) {} + + // Annotates the Prometheus data metrics with countries. + // ipCountryFilename is the location of the ip-country.mmdb IP-to-country database file. + enableCountryMetrics(ipCountryFilename: string): OutlineShadowsocksServer { + this.ipCountryFilename = ipCountryFilename; + return this; + } + + enableReplayProtection(): OutlineShadowsocksServer { + this.isReplayProtectionEnabled = true; + return this; + } + + // Promise is resolved after the outline-ss-config config is updated and the SIGHUP sent. + // Keys may not be active yet. + // TODO(fortuna): Make promise resolve when keys are ready. + update(keys: ShadowsocksAccessKey[]): Promise { + return this.writeConfigFile(keys).then(() => { + if (!this.ssProcess) { + this.start(); + return Promise.resolve(); + } else { + this.ssProcess.kill('SIGHUP'); + } + }); + } + + private writeConfigFile(keys: ShadowsocksAccessKey[]): Promise { + return new Promise((resolve, reject) => { + const keysJson = {keys: [] as ShadowsocksAccessKey[]}; + for (const key of keys) { + if (!isAeadCipher(key.cipher)) { + logging.error( + `Cipher ${key.cipher} for access key ${key.id} is not supported: use an AEAD cipher instead.` + ); + continue; + } + + keysJson.keys.push(key); + } + + mkdirp.sync(path.dirname(this.configFilename)); + + try { + file.atomicWriteFileSync(this.configFilename, jsyaml.safeDump(keysJson, {sortKeys: true})); + resolve(); + } catch (error) { + reject(error); + } + }); + } + + private start() { + const commandArguments = ['-config', this.configFilename, '-metrics', this.metricsLocation]; + if (this.ipCountryFilename) { + commandArguments.push('-ip_country_db', this.ipCountryFilename); + } + if (this.verbose) { + commandArguments.push('-verbose'); + } + if (this.isReplayProtectionEnabled) { + commandArguments.push('--replay_history=10000'); + } + logging.info('======== Starting Outline Shadowsocks Service ========'); + logging.info(`${this.binaryFilename} ${commandArguments.map(a => `"${a}"`).join(' ')}`); + this.ssProcess = child_process.spawn(this.binaryFilename, commandArguments); + this.ssProcess.on('error', (error) => { + logging.error(`Error spawning outline-ss-server: ${error}`); + }); + this.ssProcess.on('exit', (code, signal) => { + logging.info(`outline-ss-server has exited with error. Code: ${code}, Signal: ${signal}`); + logging.info(`Restarting`); + this.start(); + }); + // This exposes the outline-ss-server output on the docker logs. + // TODO(fortuna): Consider saving the output and expose it through the manager service. + this.ssProcess.stdout.pipe(process.stdout); + this.ssProcess.stderr.pipe(process.stderr); + } +} + +// List of AEAD ciphers can be found at https://shadowsocks.org/en/spec/AEAD-Ciphers.html +function isAeadCipher(cipherAlias: string) { + cipherAlias = cipherAlias.toLowerCase(); + return cipherAlias.endsWith('gcm') || cipherAlias.endsWith('poly1305'); +} diff --git a/src/shadowbox/server/server_access_key.spec.ts b/src/shadowbox/server/server_access_key.spec.ts new file mode 100644 index 000000000..166ef2d49 --- /dev/null +++ b/src/shadowbox/server/server_access_key.spec.ts @@ -0,0 +1,759 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as net from 'net'; + +import {ManualClock} from '../infrastructure/clock'; +import {PortProvider} from '../infrastructure/get_port'; +import {InMemoryConfig} from '../infrastructure/json_config'; +import {AccessKeyId, AccessKeyRepository, DataLimit} from '../model/access_key'; +import * as errors from '../model/errors'; + +import {FakePrometheusClient, FakeShadowsocksServer} from './mocks/mocks'; +import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key'; + +describe('ServerAccessKeyRepository', () => { + it('Repos with non-existent files are created with no access keys', (done) => { + const repo = new RepoBuilder().build(); + expect(countAccessKeys(repo)).toEqual(0); + done(); + }); + + it('Can get a access keys', (done) => { + const repo = new RepoBuilder().build(); + repo.createNewAccessKey().then((accessKey) => { + const accessKey2 = repo.getAccessKey(accessKey.id); + expect(accessKey2).toBeDefined(); + expect(accessKey2.id).toEqual(accessKey.id); + expect(repo.removeAccessKey.bind(repo, accessKey.id)).not.toThrow(); + done(); + }); + }); + + it('Can create new access keys', (done) => { + const repo = new RepoBuilder().build(); + repo.createNewAccessKey().then((accessKey) => { + expect(accessKey).toBeDefined(); + done(); + }); + }); + + it('Generates unique access key IDs by default', async (done) => { + const repo = new RepoBuilder().build(); + const accessKey1 = await repo.createNewAccessKey(); + const accessKey2 = await repo.createNewAccessKey(); + const accessKey3 = await repo.createNewAccessKey(); + expect(accessKey1.id).not.toEqual(accessKey2.id); + expect(accessKey2.id).not.toEqual(accessKey3.id); + expect(accessKey3.id).not.toEqual(accessKey1.id); + done(); + }); + + it('Can create new access keys with a given ID', async (done) => { + const repo = new RepoBuilder().build(); + const accessKey1 = await repo.createNewAccessKey(); + const accessKey2 = await repo.createNewAccessKey({id: 'myKeyId'}); + const accessKey3 = await repo.createNewAccessKey(); + expect(accessKey1.id).toEqual('0'); + expect(accessKey2.id).toEqual('myKeyId'); + expect(accessKey3.id).toEqual('1'); + done(); + }); + + it('createNewAccessKey throws on creating keys with existing IDs', async (done) => { + const repo = new RepoBuilder().build(); + await repo.createNewAccessKey({id: 'myKeyId'}); + await expectAsyncThrow( + repo.createNewAccessKey.bind(repo, {id: 'myKeyId'}), + errors.AccessKeyConflict + ); + done(); + }); + + it('New access keys have the correct default encryption method', (done) => { + const repo = new RepoBuilder().build(); + repo.createNewAccessKey().then((accessKey) => { + expect(accessKey).toBeDefined(); + expect(accessKey.proxyParams.encryptionMethod).toEqual('chacha20-ietf-poly1305'); + done(); + }); + }); + + it('New access keys sees the encryption method correctly', (done) => { + const repo = new RepoBuilder().build(); + repo.createNewAccessKey({encryptionMethod: 'aes-256-gcm'}).then((accessKey) => { + expect(accessKey).toBeDefined(); + expect(accessKey.proxyParams.encryptionMethod).toEqual('aes-256-gcm'); + done(); + }); + }); + + it('Creates access keys under limit', async (done) => { + const repo = new RepoBuilder().build(); + const accessKey = await repo.createNewAccessKey(); + expect(accessKey.isOverDataLimit).toBeFalsy(); + done(); + }); + + it('Can remove access keys', (done) => { + const repo = new RepoBuilder().build(); + repo.createNewAccessKey().then((accessKey) => { + expect(countAccessKeys(repo)).toEqual(1); + expect(repo.removeAccessKey.bind(repo, accessKey.id)).not.toThrow(); + expect(countAccessKeys(repo)).toEqual(0); + done(); + }); + }); + + it('removeAccessKey throws for missing keys', (done) => { + const repo = new RepoBuilder().build(); + repo.createNewAccessKey().then((_accessKey) => { + expect(countAccessKeys(repo)).toEqual(1); + expect(repo.removeAccessKey.bind(repo, 'badId')).toThrowError(errors.AccessKeyNotFound); + expect(countAccessKeys(repo)).toEqual(1); + done(); + }); + }); + + it('Can rename access keys', (done) => { + const repo = new RepoBuilder().build(); + repo.createNewAccessKey().then((accessKey) => { + const NEW_NAME = 'newName'; + expect(repo.renameAccessKey.bind(repo, accessKey.id, NEW_NAME)).not.toThrow(); + // List keys again and expect to see the NEW_NAME. + const accessKeys = repo.listAccessKeys(); + expect(accessKeys[0].name).toEqual(NEW_NAME); + done(); + }); + }); + + it('renameAccessKey throws for missing keys', (done) => { + const repo = new RepoBuilder().build(); + repo.createNewAccessKey().then((_accessKey) => { + const NEW_NAME = 'newName'; + expect(repo.renameAccessKey.bind(repo, 'badId', NEW_NAME)).toThrowError( + errors.AccessKeyNotFound + ); + // List keys again and expect to NOT see the NEW_NAME. + const accessKeys = repo.listAccessKeys(); + expect(accessKeys[0].name).not.toEqual(NEW_NAME); + done(); + }); + }); + + it('Creates keys at the right port by construction', async (done) => { + const portProvider = new PortProvider(); + const port = await portProvider.reserveNewPort(); + const repo = new RepoBuilder().port(port).build(); + const key = await repo.createNewAccessKey(); + expect(key.proxyParams.portNumber).toEqual(port); + done(); + }); + + it('setPortForNewAccessKeys changes default port for new keys', async (done) => { + const portProvider = new PortProvider(); + const port = await portProvider.reserveNewPort(); + const repo = new RepoBuilder().build(); + await repo.setPortForNewAccessKeys(port); + const key = await repo.createNewAccessKey(); + expect(key.proxyParams.portNumber).toEqual(port); + done(); + }); + + it('setPortForNewAccessKeys maintains ports on existing keys', async (done) => { + const portProvider = new PortProvider(); + const oldPort = await portProvider.reserveNewPort(); + const repo = new RepoBuilder().port(oldPort).build(); + const oldKey = await repo.createNewAccessKey(); + + const newPort = await portProvider.reserveNewPort(); + await repo.setPortForNewAccessKeys(newPort); + expect(oldKey.proxyParams.portNumber).toEqual(oldPort); + done(); + }); + + it('setPortForNewAccessKeys rejects invalid port numbers', async (done) => { + const repo = new RepoBuilder().build(); + await expectAsyncThrow(repo.setPortForNewAccessKeys.bind(repo, 0), errors.InvalidPortNumber); + await expectAsyncThrow(repo.setPortForNewAccessKeys.bind(repo, -1), errors.InvalidPortNumber); + await expectAsyncThrow( + repo.setPortForNewAccessKeys.bind(repo, 100.1), + errors.InvalidPortNumber + ); + await expectAsyncThrow( + repo.setPortForNewAccessKeys.bind(repo, 65536), + errors.InvalidPortNumber + ); + done(); + }); + + it('setPortForNewAccessKeys rejects ports in use', async (done) => { + const portProvider = new PortProvider(); + const port = await portProvider.reserveNewPort(); + const repo = new RepoBuilder().build(); + const server = new net.Server(); + server.listen(port, async () => { + try { + await repo.setPortForNewAccessKeys(port); + fail(`setPortForNewAccessKeys should reject already used port ${port}.`); + } catch (error) { + expect(error instanceof errors.PortUnavailable); + } + server.close(); + done(); + }); + }); + + it('setPortForNewAccessKeys accepts ports already used by access keys', async (done) => { + const portProvider = new PortProvider(); + const oldPort = await portProvider.reserveNewPort(); + const repo = new RepoBuilder().port(oldPort).build(); + await repo.createNewAccessKey(); + + await expectNoAsyncThrow(portProvider.reserveNewPort.bind(portProvider)); + // simulate the first key's connection on its port + const server = new net.Server(); + server.listen(oldPort, async () => { + await expectNoAsyncThrow(repo.setPortForNewAccessKeys.bind(repo, oldPort)); + server.close(); + done(); + }); + }); + + it('setAccessKeyDataLimit can set a custom data limit', async (done) => { + const server = new FakeShadowsocksServer(); + const config = new InMemoryConfig({accessKeys: [], nextId: 0}); + const repo = new RepoBuilder().shadowsocksServer(server).keyConfig(config).build(); + const key = await repo.createNewAccessKey(); + const limit = {bytes: 5000}; + await expectNoAsyncThrow(repo.setAccessKeyDataLimit.bind(repo, key.id, {bytes: 5000})); + expect(key.dataLimit).toEqual(limit); + expect(config.mostRecentWrite.accessKeys[0].dataLimit).toEqual(limit); + done(); + }); + + async function setKeyLimitAndEnforce( + repo: ServerAccessKeyRepository, + id: AccessKeyId, + limit: DataLimit + ) { + repo.setAccessKeyDataLimit(id, limit); + // We enforce asynchronously, in setAccessKeyDataLimit, so explicitly call it here to make sure + // enforcement is done before we make assertions. + return repo.enforceAccessKeyDataLimits(); + } + + it("setAccessKeyDataLimit can change a key's limit status", async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 500}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); + await repo.start(new ManualClock()); + const key = await repo.createNewAccessKey(); + await setKeyLimitAndEnforce(repo, key.id, {bytes: 0}); + + expect(key.isOverDataLimit).toBeTruthy(); + let serverKeys = server.getAccessKeys(); + expect(serverKeys.length).toEqual(0); + + await setKeyLimitAndEnforce(repo, key.id, {bytes: 1000}); + + expect(key.isOverDataLimit).toBeFalsy(); + serverKeys = server.getAccessKeys(); + expect(serverKeys.length).toEqual(1); + expect(serverKeys[0].id).toEqual(key.id); + done(); + }); + + it('setAccessKeyDataLimit overrides default data limit', async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 750, '1': 1250}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); + await repo.start(new ManualClock()); + const lowerLimitThanDefault = await repo.createNewAccessKey(); + const higherLimitThanDefault = await repo.createNewAccessKey(); + await repo.setDefaultDataLimit({bytes: 1000}); + + expect(lowerLimitThanDefault.isOverDataLimit).toBeFalsy(); + await setKeyLimitAndEnforce(repo, lowerLimitThanDefault.id, {bytes: 500}); + expect(lowerLimitThanDefault.isOverDataLimit).toBeTruthy(); + + expect(higherLimitThanDefault.isOverDataLimit).toBeTruthy(); + await setKeyLimitAndEnforce(repo, higherLimitThanDefault.id, {bytes: 1500}); + expect(higherLimitThanDefault.isOverDataLimit).toBeFalsy(); + done(); + }); + + it('removeAccessKeyDataLimit can remove a custom data limit', async (done) => { + const server = new FakeShadowsocksServer(); + const config = new InMemoryConfig({accessKeys: [], nextId: 0}); + const repo = new RepoBuilder().shadowsocksServer(server).keyConfig(config).build(); + const key = await repo.createNewAccessKey(); + await setKeyLimitAndEnforce(repo, key.id, {bytes: 1}); + await expectNoAsyncThrow(repo.removeAccessKeyDataLimit.bind(repo, key.id)); + expect(key.dataLimit).toBeFalsy(); + expect(config.mostRecentWrite.accessKeys[0].dataLimit).not.toBeDefined(); + done(); + }); + + it('removeAccessKeyDataLimit restores a key to the default data limit', async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 500}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); + const key = await repo.createNewAccessKey(); + await repo.start(new ManualClock()); + await repo.setDefaultDataLimit({bytes: 0}); + await setKeyLimitAndEnforce(repo, key.id, {bytes: 1000}); + expect(key.isOverDataLimit).toBeFalsy(); + + await removeKeyLimitAndEnforce(repo, key.id); + expect(key.isOverDataLimit).toBeTruthy(); + done(); + }); + + it("setAccessKeyDataLimit can change a key's limit status", async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 500}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); + await repo.start(new ManualClock()); + const key = await repo.createNewAccessKey(); + await setKeyLimitAndEnforce(repo, key.id, {bytes: 0}); + + expect(key.isOverDataLimit).toBeTruthy(); + let serverKeys = server.getAccessKeys(); + expect(serverKeys.length).toEqual(0); + + await setKeyLimitAndEnforce(repo, key.id, {bytes: 1000}); + + expect(key.isOverDataLimit).toBeFalsy(); + serverKeys = server.getAccessKeys(); + expect(serverKeys.length).toEqual(1); + expect(serverKeys[0].id).toEqual(key.id); + done(); + }); + + it('setAccessKeyDataLimit overrides default data limit', async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 750, '1': 1250}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); + await repo.start(new ManualClock()); + const lowerLimitThanDefault = await repo.createNewAccessKey(); + const higherLimitThanDefault = await repo.createNewAccessKey(); + await repo.setDefaultDataLimit({bytes: 1000}); + + expect(lowerLimitThanDefault.isOverDataLimit).toBeFalsy(); + await setKeyLimitAndEnforce(repo, lowerLimitThanDefault.id, {bytes: 500}); + expect(lowerLimitThanDefault.isOverDataLimit).toBeTruthy(); + + expect(higherLimitThanDefault.isOverDataLimit).toBeTruthy(); + await setKeyLimitAndEnforce(repo, higherLimitThanDefault.id, {bytes: 1500}); + expect(higherLimitThanDefault.isOverDataLimit).toBeFalsy(); + done(); + }); + + async function removeKeyLimitAndEnforce(repo: ServerAccessKeyRepository, id: AccessKeyId) { + repo.removeAccessKeyDataLimit(id); + // We enforce asynchronously, in setAccessKeyDataLimit, so explicitly call it here to make sure + // enforcement is done before we make assertions. + return repo.enforceAccessKeyDataLimits(); + } + + it('removeAccessKeyDataLimit can remove a custom data limit', async (done) => { + const server = new FakeShadowsocksServer(); + const config = new InMemoryConfig({accessKeys: [], nextId: 0}); + const repo = new RepoBuilder().shadowsocksServer(server).keyConfig(config).build(); + const key = await repo.createNewAccessKey(); + await setKeyLimitAndEnforce(repo, key.id, {bytes: 1}); + await expectNoAsyncThrow(repo.removeAccessKeyDataLimit.bind(repo, key.id)); + expect(key.dataLimit).toBeFalsy(); + expect(config.mostRecentWrite.accessKeys[0].dataLimit).not.toBeDefined(); + done(); + }); + + it('removeAccessKeyDataLimit restores a key to the default data limit', async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 500}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); + const key = await repo.createNewAccessKey(); + await repo.start(new ManualClock()); + await repo.setDefaultDataLimit({bytes: 0}); + await setKeyLimitAndEnforce(repo, key.id, {bytes: 1000}); + expect(key.isOverDataLimit).toBeFalsy(); + + await removeKeyLimitAndEnforce(repo, key.id); + expect(key.isOverDataLimit).toBeTruthy(); + done(); + }); + + it('removeAccessKeyDataLimit can restore an over-limit access key', async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 500}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); + const key = await repo.createNewAccessKey(); + await repo.start(new ManualClock()); + + await setKeyLimitAndEnforce(repo, key.id, {bytes: 0}); + expect(key.isOverDataLimit).toBeTruthy(); + expect(server.getAccessKeys().length).toEqual(0); + + await removeKeyLimitAndEnforce(repo, key.id); + expect(key.isOverDataLimit).toBeFalsy(); + expect(server.getAccessKeys().length).toEqual(1); + done(); + }); + + it('can set default data limit', async (done) => { + const repo = new RepoBuilder().build(); + const limit = {bytes: 5000}; + await expectNoAsyncThrow(repo.setDefaultDataLimit.bind(repo, limit)); + expect(repo.defaultDataLimit).toEqual(limit); + done(); + }); + + it('setDefaultDataLimit updates keys limit status', async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 500, '1': 200}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); + const accessKey1 = await repo.createNewAccessKey(); + const accessKey2 = await repo.createNewAccessKey(); + await repo.start(new ManualClock()); + + repo.setDefaultDataLimit({bytes: 250}); + // We enforce asynchronously, in setAccessKeyDataLimit, so explicitly call it here to make sure + // enforcement is done before we make assertions. + await repo.enforceAccessKeyDataLimits(); + expect(accessKey1.isOverDataLimit).toBeTruthy(); + expect(accessKey2.isOverDataLimit).toBeFalsy(); + // We determine which access keys have been enabled/disabled by accessing them from + // the server's perspective, ensuring `server.update` has been called. + let serverAccessKeys = server.getAccessKeys(); + expect(serverAccessKeys.length).toEqual(1); + expect(serverAccessKeys[0].id).toEqual(accessKey2.id); + // The over-limit key should be re-enabled after increasing the data limit, while the other key + // should be disabled after its data usage increased. + prometheusClient.bytesTransferredById = {'0': 500, '1': 1000}; + repo.setDefaultDataLimit({bytes: 700}); + await repo.enforceAccessKeyDataLimits(); + expect(accessKey1.isOverDataLimit).toBeFalsy(); + expect(accessKey2.isOverDataLimit).toBeTruthy(); + serverAccessKeys = server.getAccessKeys(); + expect(serverAccessKeys.length).toEqual(1); + expect(serverAccessKeys[0].id).toEqual(accessKey1.id); + done(); + }); + + it('can remove the default data limit', async (done) => { + const limit = {bytes: 100}; + const repo = new RepoBuilder().defaultDataLimit(limit).build(); + expect(repo.defaultDataLimit).toEqual(limit); + await expectNoAsyncThrow(repo.removeDefaultDataLimit.bind(repo)); + expect(repo.defaultDataLimit).toBeUndefined(); + done(); + }); + + it('removeDefaultDataLimit restores over-limit access keys', async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 500, '1': 100}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .defaultDataLimit({bytes: 200}) + .build(); + + const accessKey1 = await repo.createNewAccessKey(); + const accessKey2 = await repo.createNewAccessKey(); + await repo.start(new ManualClock()); + expect(server.getAccessKeys().length).toEqual(1); + + // Remove the limit; expect the key to be under limit and enabled. + expectNoAsyncThrow(repo.removeDefaultDataLimit.bind(repo)); + // We enforce asynchronously, in setAccessKeyDataLimit, so explicitly call it here to make sure + // enforcement is done before we make assertions. + await repo.enforceAccessKeyDataLimits(); + expect(server.getAccessKeys().length).toEqual(2); + expect(accessKey1.isOverDataLimit).toBeFalsy(); + expect(accessKey2.isOverDataLimit).toBeFalsy(); + done(); + }); + + it('enforceAccessKeyDataLimits updates keys limit status', async (done) => { + const prometheusClient = new FakePrometheusClient({ + '0': 100, + '1': 200, + '2': 300, + '3': 400, + '4': 500, + }); + const limit = {bytes: 250}; + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .defaultDataLimit(limit) + .build(); + for (let i = 0; i < Object.keys(prometheusClient.bytesTransferredById).length; ++i) { + await repo.createNewAccessKey(); + } + await repo.enforceAccessKeyDataLimits(); + for (const key of repo.listAccessKeys()) { + expect(key.isOverDataLimit).toEqual( + prometheusClient.bytesTransferredById[key.id] > limit.bytes + ); + } + // Simulate a change in usage. + prometheusClient.bytesTransferredById = {'0': 500, '1': 400, '2': 300, '3': 200, '4': 100}; + + await repo.enforceAccessKeyDataLimits(); + for (const key of repo.listAccessKeys()) { + expect(key.isOverDataLimit).toEqual( + prometheusClient.bytesTransferredById[key.id] > limit.bytes + ); + } + done(); + }); + + it('enforceAccessKeyDataLimits respects both default and per-key limits', async (done) => { + const prometheusClient = new FakePrometheusClient({'0': 200, '1': 300}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .defaultDataLimit({bytes: 500}) + .build(); + const perKeyLimited = await repo.createNewAccessKey(); + const defaultLimited = await repo.createNewAccessKey(); + await setKeyLimitAndEnforce(repo, perKeyLimited.id, {bytes: 100}); + + await repo.enforceAccessKeyDataLimits(); + expect(perKeyLimited.isOverDataLimit).toBeTruthy(); + expect(defaultLimited.isOverDataLimit).toBeFalsy(); + + prometheusClient.bytesTransferredById[perKeyLimited.id] = 50; + prometheusClient.bytesTransferredById[defaultLimited.id] = 600; + await repo.enforceAccessKeyDataLimits(); + expect(perKeyLimited.isOverDataLimit).toBeFalsy(); + expect(defaultLimited.isOverDataLimit).toBeTruthy(); + + done(); + }); + + it('enforceAccessKeyDataLimits enables and disables keys', async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 500, '1': 100}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .defaultDataLimit({bytes: 200}) + .build(); + + await repo.createNewAccessKey(); + const accessKey2 = await repo.createNewAccessKey(); + + await repo.enforceAccessKeyDataLimits(); + await repo.listAccessKeys(); + let serverAccessKeys = server.getAccessKeys(); + expect(serverAccessKeys.length).toEqual(1); + expect(serverAccessKeys[0].id).toEqual(accessKey2.id); + + prometheusClient.bytesTransferredById = {'0': 100, '1': 100}; + await repo.enforceAccessKeyDataLimits(); + serverAccessKeys = server.getAccessKeys(); + expect(serverAccessKeys.length).toEqual(2); + done(); + }); + + it('Repos created with an existing file restore access keys', async (done) => { + const config = new InMemoryConfig({accessKeys: [], nextId: 0}); + const repo1 = new RepoBuilder().keyConfig(config).build(); + // Create 2 new access keys + await Promise.all([repo1.createNewAccessKey(), repo1.createNewAccessKey()]); + // Modify properties + repo1.renameAccessKey('1', 'name'); + repo1.setAccessKeyDataLimit('0', {bytes: 1}); + + // Create a 2nd repo from the same config file. This simulates what + // might happen after the shadowbox server is restarted. + const repo2 = new RepoBuilder().keyConfig(config).build(); + // Check that repo1 and repo2 have the same access keys + expect(repo1.listAccessKeys()).toEqual(repo2.listAccessKeys()); + done(); + }); + + it('Does not re-use ids when using the same config file', (done) => { + const config = new InMemoryConfig({accessKeys: [], nextId: 0}); + // Create a repo with 1 access key, then delete that access key. + const repo1 = new RepoBuilder().keyConfig(config).build(); + repo1.createNewAccessKey().then((accessKey1) => { + repo1.removeAccessKey(accessKey1.id); + + // Create a 2nd repo with one access key, and verify that + // it hasn't reused the first access key's ID. + const repo2 = new RepoBuilder().keyConfig(config).build(); + repo2.createNewAccessKey().then((accessKey2) => { + expect(accessKey1.id).not.toEqual(accessKey2.id); + done(); + }); + }); + }); + + it('start exposes the access keys to the server', async (done) => { + const config = new InMemoryConfig({accessKeys: [], nextId: 0}); + const repo = new RepoBuilder().keyConfig(config).build(); + + const accessKey1 = await repo.createNewAccessKey(); + const accessKey2 = await repo.createNewAccessKey(); + // Create a new repository with the same configuration. The keys should not be exposed to the + // server until `start` is called. + const server = new FakeShadowsocksServer(); + const repo2 = new RepoBuilder().keyConfig(config).shadowsocksServer(server).build(); + expect(server.getAccessKeys().length).toEqual(0); + await repo2.start(new ManualClock()); + const serverAccessKeys = server.getAccessKeys(); + expect(serverAccessKeys.length).toEqual(2); + expect(serverAccessKeys[0].id).toEqual(accessKey1.id); + expect(serverAccessKeys[1].id).toEqual(accessKey2.id); + done(); + }); + + it('start periodically enforces access key data limits', async (done) => { + const server = new FakeShadowsocksServer(); + const prometheusClient = new FakePrometheusClient({'0': 500, '1': 200, '2': 400}); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); + const accessKey1 = await repo.createNewAccessKey(); + const accessKey2 = await repo.createNewAccessKey(); + const accessKey3 = await repo.createNewAccessKey(); + await repo.setDefaultDataLimit({bytes: 300}); + const clock = new ManualClock(); + await repo.start(clock); + await clock.runCallbacks(); + + expect(accessKey1.isOverDataLimit).toBeTruthy(); + expect(accessKey2.isOverDataLimit).toBeFalsy(); + expect(accessKey3.isOverDataLimit).toBeTruthy(); + let serverAccessKeys = await server.getAccessKeys(); + expect(serverAccessKeys.length).toEqual(1); + expect(serverAccessKeys[0].id).toEqual(accessKey2.id); + + // Simulate a change in usage. + prometheusClient.bytesTransferredById = {'0': 100, '1': 200, '2': 1000}; + await clock.runCallbacks(); + expect(accessKey1.isOverDataLimit).toBeFalsy(); + expect(accessKey2.isOverDataLimit).toBeFalsy(); + expect(accessKey3.isOverDataLimit).toBeTruthy(); + serverAccessKeys = await server.getAccessKeys(); + expect(serverAccessKeys.length).toEqual(2); + expect(serverAccessKeys[0].id).toEqual(accessKey1.id); + expect(serverAccessKeys[1].id).toEqual(accessKey2.id); + done(); + }); + + it('setHostname changes hostname for new keys', async (done) => { + const newHostname = 'host2'; + const repo = new RepoBuilder().build(); + repo.setHostname(newHostname); + const key = await repo.createNewAccessKey(); + expect(key.proxyParams.hostname).toEqual(newHostname); + done(); + }); +}); + +// Convenience function to expect that an asynchronous function does not throw an error. Note that +// jasmine.toThrowError lacks asynchronous support and could lead to false positives. +async function expectNoAsyncThrow(fn: Function) { + try { + await fn(); + } catch (e) { + fail(`Unexpected error thrown: ${e}`); + } +} + +// Convenience function to expect that an asynchronous function throws an error. Fails if the thrown +// error does not match `errorType`, when defined. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function expectAsyncThrow(fn: Function, errorType?: new (...args: any[]) => Error) { + try { + await fn(); + fail(`Expected error to be thrown`); + } catch (e) { + if (!!errorType && !(e instanceof errorType)) { + fail(`Thrown error is not of type ${errorType.name}. Got ${e.name}`); + } + } +} + +function countAccessKeys(repo: AccessKeyRepository) { + return repo.listAccessKeys().length; +} + +class RepoBuilder { + private port_ = 12345; + private keyConfig_ = new InMemoryConfig({accessKeys: [], nextId: 0}); + private shadowsocksServer_ = new FakeShadowsocksServer(); + private prometheusClient_ = new FakePrometheusClient({}); + private defaultDataLimit_; + + public port(port: number): RepoBuilder { + this.port_ = port; + return this; + } + public keyConfig(keyConfig: InMemoryConfig): RepoBuilder { + this.keyConfig_ = keyConfig; + return this; + } + public shadowsocksServer(shadowsocksServer: FakeShadowsocksServer): RepoBuilder { + this.shadowsocksServer_ = shadowsocksServer; + return this; + } + public prometheusClient(prometheusClient: FakePrometheusClient): RepoBuilder { + this.prometheusClient_ = prometheusClient; + return this; + } + public defaultDataLimit(limit: DataLimit): RepoBuilder { + this.defaultDataLimit_ = limit; + return this; + } + + public build(): ServerAccessKeyRepository { + return new ServerAccessKeyRepository( + this.port_, + 'hostname', + this.keyConfig_, + this.shadowsocksServer_, + this.prometheusClient_, + this.defaultDataLimit_ + ); + } +} diff --git a/src/shadowbox/server/server_access_key.ts b/src/shadowbox/server/server_access_key.ts new file mode 100644 index 000000000..d41be524f --- /dev/null +++ b/src/shadowbox/server/server_access_key.ts @@ -0,0 +1,329 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as randomstring from 'randomstring'; +import * as uuidv4 from 'uuid/v4'; + +import {Clock} from '../infrastructure/clock'; +import {isPortUsed} from '../infrastructure/get_port'; +import {JsonConfig} from '../infrastructure/json_config'; +import * as logging from '../infrastructure/logging'; +import {PrometheusClient} from '../infrastructure/prometheus_scraper'; +import { + AccessKey, + AccessKeyCreateParams, + AccessKeyId, + AccessKeyMetricsId, + AccessKeyRepository, + DataLimit, + ProxyParams, +} from '../model/access_key'; +import * as errors from '../model/errors'; +import {ShadowsocksServer} from '../model/shadowsocks_server'; +import {PrometheusManagerMetrics} from './manager_metrics'; + +// The format as json of access keys in the config file. +interface AccessKeyStorageJson { + id: AccessKeyId; + metricsId: AccessKeyId; + name: string; + password: string; + port: number; + encryptionMethod?: string; + dataLimit?: DataLimit; +} + +// The configuration file format as json. +export interface AccessKeyConfigJson { + accessKeys?: AccessKeyStorageJson[]; + // Next AccessKeyId to use. + nextId?: number; +} + +// AccessKey implementation with write access enabled on properties that may change. +class ServerAccessKey implements AccessKey { + public isOverDataLimit = false; + constructor( + readonly id: AccessKeyId, + public name: string, + public metricsId: AccessKeyMetricsId, + readonly proxyParams: ProxyParams, + public dataLimit?: DataLimit + ) {} +} + +// Generates a random password for Shadowsocks access keys. +function generatePassword(): string { + // 22 * log2(62) = 131 bits of entropy. + return randomstring.generate(22); +} + +function makeAccessKey(hostname: string, accessKeyJson: AccessKeyStorageJson): AccessKey { + const proxyParams = { + hostname, + portNumber: accessKeyJson.port, + encryptionMethod: accessKeyJson.encryptionMethod, + password: accessKeyJson.password, + }; + return new ServerAccessKey( + accessKeyJson.id, + accessKeyJson.name, + accessKeyJson.metricsId, + proxyParams, + accessKeyJson.dataLimit + ); +} + +function accessKeyToStorageJson(accessKey: AccessKey): AccessKeyStorageJson { + return { + id: accessKey.id, + metricsId: accessKey.metricsId, + name: accessKey.name, + password: accessKey.proxyParams.password, + port: accessKey.proxyParams.portNumber, + encryptionMethod: accessKey.proxyParams.encryptionMethod, + dataLimit: accessKey.dataLimit, + }; +} + +function isValidCipher(cipher: string): boolean { + if ( + ['aes-256-gcm', 'aes-192-gcm', 'aes-128-gcm', 'chacha20-ietf-poly1305'].indexOf(cipher) === -1 + ) { + return false; + } + return true; +} + +// AccessKeyRepository that keeps its state in a config file and uses ShadowsocksServer +// to start and stop per-access-key Shadowsocks instances. Requires external validation +// that portForNewAccessKeys is valid. +export class ServerAccessKeyRepository implements AccessKeyRepository { + private static DATA_LIMITS_ENFORCEMENT_INTERVAL_MS = 60 * 60 * 1000; // 1h + private NEW_USER_ENCRYPTION_METHOD = 'chacha20-ietf-poly1305'; + private accessKeys: ServerAccessKey[]; + + constructor( + private portForNewAccessKeys: number, + private proxyHostname: string, + private keyConfig: JsonConfig, + private shadowsocksServer: ShadowsocksServer, + private prometheusClient: PrometheusClient, + private _defaultDataLimit?: DataLimit + ) { + if (this.keyConfig.data().accessKeys === undefined) { + this.keyConfig.data().accessKeys = []; + } + if (this.keyConfig.data().nextId === undefined) { + this.keyConfig.data().nextId = 0; + } + this.accessKeys = this.loadAccessKeys(); + } + + // Starts the Shadowsocks server and exposes the access key configuration to the server. + // Periodically enforces access key limits. + async start(clock: Clock): Promise { + const tryEnforceDataLimits = async () => { + try { + await this.enforceAccessKeyDataLimits(); + } catch (e) { + logging.error(`Failed to enforce access key limits: ${e}`); + } + }; + await tryEnforceDataLimits(); + await this.updateServer(); + clock.setInterval( + tryEnforceDataLimits, + ServerAccessKeyRepository.DATA_LIMITS_ENFORCEMENT_INTERVAL_MS + ); + } + + private isExistingAccessKeyId(id: AccessKeyId): boolean { + return this.accessKeys.some((key) => { + return key.id === id; + }); + } + + private isExistingAccessKeyPort(port: number): boolean { + return this.accessKeys.some((key) => { + return key.proxyParams.portNumber === port; + }); + } + + setHostname(hostname: string): void { + this.proxyHostname = hostname; + } + + async setPortForNewAccessKeys(port: number): Promise { + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new errors.InvalidPortNumber(port.toString()); + } + if (!this.isExistingAccessKeyPort(port) && (await isPortUsed(port))) { + throw new errors.PortUnavailable(port); + } + this.portForNewAccessKeys = port; + } + + private generateId(): string { + let id: AccessKeyId = this.keyConfig.data().nextId.toString(); + this.keyConfig.data().nextId += 1; + // Users can supply their own access key IDs. This means we always need to + // verify that any auto-generated key ID hasn't already been used. + while (this.isExistingAccessKeyId(id)) { + id = this.keyConfig.data().nextId.toString(); + this.keyConfig.data().nextId += 1; + } + return id; + } + + async createNewAccessKey(params?: AccessKeyCreateParams): Promise { + let id = params?.id; + if (id) { + if (this.isExistingAccessKeyId(params?.id)) { + throw new errors.AccessKeyConflict(id); + } + } else { + id = this.generateId(); + } + const metricsId = uuidv4(); + const password = params?.password ?? generatePassword(); + const encryptionMethod = params?.encryptionMethod || this.NEW_USER_ENCRYPTION_METHOD; + + // Validate encryption method. + if (!isValidCipher(encryptionMethod)) { + throw new errors.InvalidCipher(encryptionMethod); + } + const proxyParams = { + hostname: this.proxyHostname, + portNumber: this.portForNewAccessKeys, + encryptionMethod, + password, + }; + const name = params?.name ?? ''; + const dataLimit = params?.dataLimit; + const accessKey = new ServerAccessKey(id, name, metricsId, proxyParams, dataLimit); + this.accessKeys.push(accessKey); + this.saveAccessKeys(); + await this.updateServer(); + return accessKey; + } + + removeAccessKey(id: AccessKeyId) { + for (let ai = 0; ai < this.accessKeys.length; ai++) { + const accessKey = this.accessKeys[ai]; + if (accessKey.id === id) { + this.accessKeys.splice(ai, 1); + this.saveAccessKeys(); + this.updateServer(); + return; + } + } + throw new errors.AccessKeyNotFound(id); + } + + getAccessKey(id: AccessKeyId): ServerAccessKey { + for (const accessKey of this.accessKeys) { + if (accessKey.id === id) { + return accessKey; + } + } + throw new errors.AccessKeyNotFound(id); + } + + listAccessKeys(): AccessKey[] { + return [...this.accessKeys]; // Return a copy of the access key array. + } + + renameAccessKey(id: AccessKeyId, name: string) { + const accessKey = this.getAccessKey(id); + accessKey.name = name; + this.saveAccessKeys(); + } + + setAccessKeyDataLimit(id: AccessKeyId, limit: DataLimit): void { + this.getAccessKey(id).dataLimit = limit; + this.saveAccessKeys(); + this.enforceAccessKeyDataLimits(); + } + + removeAccessKeyDataLimit(id: AccessKeyId): void { + delete this.getAccessKey(id).dataLimit; + this.saveAccessKeys(); + this.enforceAccessKeyDataLimits(); + } + + get defaultDataLimit(): DataLimit | undefined { + return this._defaultDataLimit; + } + + setDefaultDataLimit(limit: DataLimit): void { + this._defaultDataLimit = limit; + this.enforceAccessKeyDataLimits(); + } + + removeDefaultDataLimit(): void { + delete this._defaultDataLimit; + this.enforceAccessKeyDataLimits(); + } + + getMetricsId(id: AccessKeyId): AccessKeyMetricsId | undefined { + const accessKey = this.getAccessKey(id); + return accessKey ? accessKey.metricsId : undefined; + } + + // Compares access key usage with collected metrics, marking them as under or over limit. + // Updates access key data usage. + async enforceAccessKeyDataLimits() { + const metrics = new PrometheusManagerMetrics(this.prometheusClient); + const bytesTransferredById = (await metrics.getOutboundByteTransfer({hours: 30 * 24})) + .bytesTransferredByUserId; + let limitStatusChanged = false; + for (const accessKey of this.accessKeys) { + const usageBytes = bytesTransferredById[accessKey.id] ?? 0; + const wasOverDataLimit = accessKey.isOverDataLimit; + let limitBytes = (accessKey.dataLimit ?? this._defaultDataLimit)?.bytes; + if (limitBytes === undefined) { + limitBytes = Number.POSITIVE_INFINITY; + } + accessKey.isOverDataLimit = usageBytes > limitBytes; + limitStatusChanged = accessKey.isOverDataLimit !== wasOverDataLimit || limitStatusChanged; + } + if (limitStatusChanged) { + await this.updateServer(); + } + } + + private updateServer(): Promise { + const serverAccessKeys = this.accessKeys + .filter((key) => !key.isOverDataLimit) + .map((key) => { + return { + id: key.id, + port: key.proxyParams.portNumber, + cipher: key.proxyParams.encryptionMethod, + secret: key.proxyParams.password, + }; + }); + return this.shadowsocksServer.update(serverAccessKeys); + } + + private loadAccessKeys(): AccessKey[] { + return this.keyConfig.data().accessKeys.map((key) => makeAccessKey(this.proxyHostname, key)); + } + + private saveAccessKeys() { + this.keyConfig.data().accessKeys = this.accessKeys.map((key) => accessKeyToStorageJson(key)); + this.keyConfig.write(); + } +} diff --git a/src/shadowbox/server/server_config.ts b/src/shadowbox/server/server_config.ts new file mode 100644 index 000000000..2e757abeb --- /dev/null +++ b/src/shadowbox/server/server_config.ts @@ -0,0 +1,64 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import * as uuidv4 from 'uuid/v4'; + +import * as json_config from '../infrastructure/json_config'; +import {DataLimit} from '../model/access_key'; + +// Serialized format for the server config. +// WARNING: Renaming fields will break backwards-compatibility. +export interface ServerConfigJson { + // The unique random identifier for this server. Used for shared metrics and staged rollouts. + serverId?: string; + // Whether metrics sharing is enabled. + metricsEnabled?: boolean; + // The name of this server, as shown in the Outline Manager. + name?: string; + // When this server was created. Shown in the Outline Manager and to trigger the metrics opt-in. + createdTimestampMs?: number; + // What port number should we use for new access keys? + portForNewAccessKeys?: number; + // Which staged rollouts we should force enabled or disabled. + rollouts?: RolloutConfigJson[]; + // We don't serialize the shadowbox version, this is obtained dynamically from node. + // Public proxy hostname. + hostname?: string; + // Default data transfer limit applied to all access keys. + accessKeyDataLimit?: DataLimit; +} + +// Serialized format for rollouts. +// WARNING: Renaming fields will break backwards-compatibility. +export interface RolloutConfigJson { + // Unique identifier of the rollout. + id: string; + // Whether it's forced enabled or disabled. Omit for automatic behavior based on + // hash(serverId, rolloutId). + enabled: boolean; +} + +export function readServerConfig(filename: string): json_config.JsonConfig { + try { + const config = json_config.loadFileConfig(filename); + config.data().serverId = config.data().serverId || uuidv4(); + config.data().metricsEnabled = config.data().metricsEnabled || false; + config.data().createdTimestampMs = config.data().createdTimestampMs || Date.now(); + config.data().hostname = config.data().hostname || process.env.SB_PUBLIC_IP; + config.write(); + return config; + } catch (error) { + throw new Error(`Failed to read server config at ${filename}: ${error}`); + } +} diff --git a/src/shadowbox/server/shared_metrics.spec.ts b/src/shadowbox/server/shared_metrics.spec.ts new file mode 100644 index 000000000..2038d885e --- /dev/null +++ b/src/shadowbox/server/shared_metrics.spec.ts @@ -0,0 +1,307 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import {ManualClock} from '../infrastructure/clock'; +import {InMemoryConfig} from '../infrastructure/json_config'; +import {AccessKeyId, DataLimit} from '../model/access_key'; +import {version} from '../package.json'; +import {AccessKeyConfigJson} from './server_access_key'; + +import {ServerConfigJson} from './server_config'; +import { + CountryUsage, + DailyFeatureMetricsReportJson, + HourlyServerMetricsReportJson, + KeyUsage, + MetricsCollectorClient, + OutlineSharedMetricsPublisher, + UsageMetrics, +} from './shared_metrics'; + +describe('OutlineSharedMetricsPublisher', () => { + describe('Enable/Disable', () => { + it('Mirrors config', () => { + const serverConfig = new InMemoryConfig({}); + + const publisher = new OutlineSharedMetricsPublisher( + new ManualClock(), + serverConfig, + null, + null, + null, + null + ); + expect(publisher.isSharingEnabled()).toBeFalsy(); + + publisher.startSharing(); + expect(publisher.isSharingEnabled()).toBeTruthy(); + expect(serverConfig.mostRecentWrite.metricsEnabled).toBeTruthy(); + + publisher.stopSharing(); + expect(publisher.isSharingEnabled()).toBeFalsy(); + expect(serverConfig.mostRecentWrite.metricsEnabled).toBeFalsy(); + }); + it('Reads from config', () => { + const serverConfig = new InMemoryConfig({metricsEnabled: true}); + const publisher = new OutlineSharedMetricsPublisher( + new ManualClock(), + serverConfig, + null, + null, + null, + null + ); + expect(publisher.isSharingEnabled()).toBeTruthy(); + }); + }); + describe('Metrics Reporting', () => { + it('reports server usage metrics correctly', async () => { + const clock = new ManualClock(); + let startTime = clock.nowMs; + const serverConfig = new InMemoryConfig({serverId: 'server-id'}); + const usageMetrics = new ManualUsageMetrics(); + const toMetricsId = (id: AccessKeyId) => `M(${id})`; + const metricsCollector = new FakeMetricsCollector(); + const publisher = new OutlineSharedMetricsPublisher( + clock, + serverConfig, + null, + usageMetrics, + toMetricsId, + metricsCollector + ); + + publisher.startSharing(); + usageMetrics.keyUsage = [ + {accessKeyId: 'user-0', inboundBytes: 11}, + {accessKeyId: 'user-1', inboundBytes: 22}, + {accessKeyId: 'user-0', inboundBytes: 33}, + ]; + usageMetrics.countryUsage = [ + {country: 'AA', inboundBytes: 11}, + {country: 'BB', inboundBytes: 11}, + {country: 'CC', inboundBytes: 22}, + {country: 'AA', inboundBytes: 33}, + {country: 'DD', inboundBytes: 33}, + ]; + + clock.nowMs += 60 * 60 * 1000; + await clock.runCallbacks(); + expect(metricsCollector.collectedServerUsageReport).toEqual({ + serverId: 'server-id', + startUtcMs: startTime, + endUtcMs: clock.nowMs, + userReports: [ + {userId: 'M(user-0)', bytesTransferred: 11}, + {userId: 'M(user-1)', bytesTransferred: 22}, + {userId: 'M(user-0)', bytesTransferred: 33}, + {bytesTransferred: 11, countries: ['AA']}, + {bytesTransferred: 11, countries: ['BB']}, + {bytesTransferred: 22, countries: ['CC']}, + {bytesTransferred: 33, countries: ['AA']}, + {bytesTransferred: 33, countries: ['DD']}, + ], + }); + + startTime = clock.nowMs; + usageMetrics.keyUsage = [ + {accessKeyId: 'user-0', inboundBytes: 44}, + {accessKeyId: 'user-2', inboundBytes: 55}, + ]; + usageMetrics.countryUsage = [ + {country: 'EE', inboundBytes: 44}, + {country: 'FF', inboundBytes: 55}, + ]; + + clock.nowMs += 60 * 60 * 1000; + await clock.runCallbacks(); + expect(metricsCollector.collectedServerUsageReport).toEqual({ + serverId: 'server-id', + startUtcMs: startTime, + endUtcMs: clock.nowMs, + userReports: [ + {userId: 'M(user-0)', bytesTransferred: 44}, + {userId: 'M(user-2)', bytesTransferred: 55}, + {bytesTransferred: 44, countries: ['EE']}, + {bytesTransferred: 55, countries: ['FF']}, + ], + }); + + publisher.stopSharing(); + }); + it('ignores sanctioned countries', async () => { + const clock = new ManualClock(); + const startTime = clock.nowMs; + const serverConfig = new InMemoryConfig({serverId: 'server-id'}); + const usageMetrics = new ManualUsageMetrics(); + const toMetricsId = (id: AccessKeyId) => `M(${id})`; + const metricsCollector = new FakeMetricsCollector(); + const publisher = new OutlineSharedMetricsPublisher( + clock, + serverConfig, + null, + usageMetrics, + toMetricsId, + metricsCollector + ); + + publisher.startSharing(); + usageMetrics.keyUsage = [ + {accessKeyId: 'user-0', inboundBytes: 11}, + {accessKeyId: 'user-1', inboundBytes: 22}, + {accessKeyId: 'user-0', inboundBytes: 33}, + ]; + usageMetrics.countryUsage = [ + {country: 'AA', inboundBytes: 11}, + {country: 'SY', inboundBytes: 11}, + {country: 'CC', inboundBytes: 22}, + {country: 'AA', inboundBytes: 33}, + {country: 'DD', inboundBytes: 33}, + ]; + + clock.nowMs += 60 * 60 * 1000; + await clock.runCallbacks(); + expect(metricsCollector.collectedServerUsageReport).toEqual({ + serverId: 'server-id', + startUtcMs: startTime, + endUtcMs: clock.nowMs, + userReports: [ + {userId: 'M(user-0)', bytesTransferred: 11}, + {userId: 'M(user-1)', bytesTransferred: 22}, + {userId: 'M(user-0)', bytesTransferred: 33}, + {bytesTransferred: 11, countries: ['AA']}, + {bytesTransferred: 22, countries: ['CC']}, + {bytesTransferred: 33, countries: ['AA']}, + {bytesTransferred: 33, countries: ['DD']}, + ], + }); + publisher.stopSharing(); + }); + }); + it('reports feature metrics correctly', async () => { + const clock = new ManualClock(); + let timestamp = clock.nowMs; + const serverConfig = new InMemoryConfig({ + serverId: 'server-id', + accessKeyDataLimit: {bytes: 123}, + }); + let keyId = 0; + const makeKeyJson = (dataLimit?: DataLimit) => { + return { + id: (keyId++).toString(), + metricsId: 'id', + name: 'name', + password: 'pass', + port: 12345, + dataLimit, + }; + }; + const keyConfig = new InMemoryConfig({ + accessKeys: [makeKeyJson({bytes: 2}), makeKeyJson()], + }); + const metricsCollector = new FakeMetricsCollector(); + const publisher = new OutlineSharedMetricsPublisher( + clock, + serverConfig, + keyConfig, + new ManualUsageMetrics(), + (_id: AccessKeyId) => '', + metricsCollector + ); + + publisher.startSharing(); + await clock.runCallbacks(); + expect(metricsCollector.collectedFeatureMetricsReport).toEqual({ + serverId: 'server-id', + serverVersion: version, + timestampUtcMs: timestamp, + dataLimit: { + enabled: true, + perKeyLimitCount: 1, + }, + }); + clock.nowMs += 24 * 60 * 60 * 1000; + timestamp = clock.nowMs; + + delete serverConfig.data().accessKeyDataLimit; + await clock.runCallbacks(); + expect(metricsCollector.collectedFeatureMetricsReport).toEqual({ + serverId: 'server-id', + serverVersion: version, + timestampUtcMs: timestamp, + dataLimit: { + enabled: false, + perKeyLimitCount: 1, + }, + }); + + clock.nowMs += 24 * 60 * 60 * 1000; + delete keyConfig.data().accessKeys[0].dataLimit; + await clock.runCallbacks(); + expect(metricsCollector.collectedFeatureMetricsReport.dataLimit.perKeyLimitCount).toEqual(0); + }); + it('does not report metrics when sharing is disabled', async () => { + const clock = new ManualClock(); + const serverConfig = new InMemoryConfig({ + serverId: 'server-id', + metricsEnabled: false, + }); + const metricsCollector = new FakeMetricsCollector(); + spyOn(metricsCollector, 'collectServerUsageMetrics').and.callThrough(); + spyOn(metricsCollector, 'collectFeatureMetrics').and.callThrough(); + new OutlineSharedMetricsPublisher( + clock, + serverConfig, + new InMemoryConfig({}), + new ManualUsageMetrics(), + (_id: AccessKeyId) => '', + metricsCollector + ); + + await clock.runCallbacks(); + expect(metricsCollector.collectServerUsageMetrics).not.toHaveBeenCalled(); + expect(metricsCollector.collectFeatureMetrics).not.toHaveBeenCalled(); + }); +}); + +class FakeMetricsCollector implements MetricsCollectorClient { + public collectedServerUsageReport: HourlyServerMetricsReportJson; + public collectedFeatureMetricsReport: DailyFeatureMetricsReportJson; + + async collectServerUsageMetrics(report) { + this.collectedServerUsageReport = report; + } + + async collectFeatureMetrics(report) { + this.collectedFeatureMetricsReport = report; + } +} + +class ManualUsageMetrics implements UsageMetrics { + public keyUsage = [] as KeyUsage[]; + public countryUsage = [] as CountryUsage[]; + + getKeyUsage(): Promise { + return Promise.resolve(this.keyUsage); + } + + getCountryUsage(): Promise { + return Promise.resolve(this.countryUsage) + } + + reset() { + this.keyUsage = [] as KeyUsage[]; + this.countryUsage = [] as CountryUsage[]; + } +} diff --git a/src/shadowbox/server/shared_metrics.ts b/src/shadowbox/server/shared_metrics.ts new file mode 100644 index 000000000..e96a0792f --- /dev/null +++ b/src/shadowbox/server/shared_metrics.ts @@ -0,0 +1,296 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +import {Clock} from '../infrastructure/clock'; +import * as follow_redirects from '../infrastructure/follow_redirects'; +import {JsonConfig} from '../infrastructure/json_config'; +import * as logging from '../infrastructure/logging'; +import {PrometheusClient} from '../infrastructure/prometheus_scraper'; +import {AccessKeyId, AccessKeyMetricsId} from '../model/access_key'; +import {version} from '../package.json'; +import {AccessKeyConfigJson} from './server_access_key'; + +import {ServerConfigJson} from './server_config'; + +const MS_PER_HOUR = 60 * 60 * 1000; +const MS_PER_DAY = 24 * MS_PER_HOUR; +const SANCTIONED_COUNTRIES = new Set(['CU', 'KP', 'SY']); + +// Used internally to track key usage. +export interface KeyUsage { + accessKeyId: string; + inboundBytes: number; +} + +export interface CountryUsage { + country: string; + inboundBytes: number; +} + +// JSON format for the published report. +// Field renames will break backwards-compatibility. +export interface HourlyServerMetricsReportJson { + serverId: string; + startUtcMs: number; + endUtcMs: number; + userReports: HourlyUserMetricsReportJson[]; +} + +// JSON format for the published report. +// Field renames will break backwards-compatibility. +export interface HourlyUserMetricsReportJson { + userId?: string; + countries?: string[]; + bytesTransferred: number; +} + +// JSON format for the feature metrics report. +// Field renames will break backwards-compatibility. +export interface DailyFeatureMetricsReportJson { + serverId: string; + serverVersion: string; + timestampUtcMs: number; + dataLimit: DailyDataLimitMetricsReportJson; +} + +// JSON format for the data limit feature metrics report. +// Field renames will break backwards-compatibility. +export interface DailyDataLimitMetricsReportJson { + enabled: boolean; + perKeyLimitCount?: number; +} + +export interface SharedMetricsPublisher { + startSharing(); + stopSharing(); + isSharingEnabled(); +} + +export interface UsageMetrics { + getKeyUsage(): Promise; + getCountryUsage(): Promise; + reset(); +} + +// Reads data usage metrics from Prometheus. +export class PrometheusUsageMetrics implements UsageMetrics { + private resetTimeMs: number = Date.now(); + + constructor(private prometheusClient: PrometheusClient) {} + + async getKeyUsage(): Promise { + const timeDeltaSecs = Math.round((Date.now() - this.resetTimeMs) / 1000); + // We measure the traffic to and from the target, since that's what we are protecting. + const result = await this.prometheusClient.query( + `sum(increase(shadowsocks_data_bytes{dir=~"p>t|p 0) { + usage.push({accessKeyId, inboundBytes}); + } + } + return usage; + } + + async getCountryUsage(): Promise { + const timeDeltaSecs = Math.round((Date.now() - this.resetTimeMs) / 1000); + // We measure the traffic to and from the target, since that's what we are protecting. + const result = await this.prometheusClient.query( + `sum(increase(shadowsocks_data_bytes_per_location{dir=~"p>t|p; + collectFeatureMetrics(reportJson: DailyFeatureMetricsReportJson): Promise; +} + +export class RestMetricsCollectorClient { + constructor(private serviceUrl: string) {} + + collectServerUsageMetrics(reportJson: HourlyServerMetricsReportJson): Promise { + return this.postMetrics('/connections', JSON.stringify(reportJson)); + } + + collectFeatureMetrics(reportJson: DailyFeatureMetricsReportJson): Promise { + return this.postMetrics('/features', JSON.stringify(reportJson)); + } + + private async postMetrics(urlPath: string, reportJson: string): Promise { + const options = { + headers: {'Content-Type': 'application/json'}, + method: 'POST', + body: reportJson, + }; + const url = `${this.serviceUrl}${urlPath}`; + logging.debug(`Posting metrics to ${url} with options ${JSON.stringify(options)}`); + try { + const response = await follow_redirects.requestFollowRedirectsWithSameMethodAndBody( + url, + options + ); + if (!response.ok) { + throw new Error(`Got status ${response.status}`); + } + } catch (e) { + throw new Error(`Failed to post to metrics server: ${e}`); + } + } +} + +// Keeps track of the connection metrics per user, since the startDatetime. +// This is reported to the Outline team if the admin opts-in. +export class OutlineSharedMetricsPublisher implements SharedMetricsPublisher { + // Time at which we started recording connection metrics. + private reportStartTimestampMs: number; + + // serverConfig: where the enabled/disable setting is persisted + // keyConfig: where access keys are persisted + // usageMetrics: where we get the metrics from + // toMetricsId: maps Access key ids to metric ids + // metricsUrl: where to post the metrics + constructor( + private clock: Clock, + private serverConfig: JsonConfig, + private keyConfig: JsonConfig, + usageMetrics: UsageMetrics, + private toMetricsId: (accessKeyId: AccessKeyId) => AccessKeyMetricsId, + private metricsCollector: MetricsCollectorClient + ) { + // Start timer + this.reportStartTimestampMs = this.clock.now(); + + this.clock.setInterval(async () => { + if (!this.isSharingEnabled()) { + return; + } + try { + const keyUsagePromise = usageMetrics.getKeyUsage() + const countryUsagePromise = usageMetrics.getCountryUsage() + await this.reportServerUsageMetrics(await keyUsagePromise, await countryUsagePromise); + usageMetrics.reset(); + } catch (err) { + logging.error(`Failed to report server usage metrics: ${err}`); + } + }, MS_PER_HOUR); + // TODO(fortuna): also trigger report on shutdown, so data loss is minimized. + + this.clock.setInterval(async () => { + if (!this.isSharingEnabled()) { + return; + } + try { + await this.reportFeatureMetrics(); + } catch (err) { + logging.error(`Failed to report feature metrics: ${err}`); + } + }, MS_PER_DAY); + } + + startSharing() { + this.serverConfig.data().metricsEnabled = true; + this.serverConfig.write(); + } + + stopSharing() { + this.serverConfig.data().metricsEnabled = false; + this.serverConfig.write(); + } + + isSharingEnabled(): boolean { + return this.serverConfig.data().metricsEnabled || false; + } + + private async reportServerUsageMetrics(keyUsageMetrics: KeyUsage[], countryUsageMetrics: CountryUsage[]): Promise { + const reportEndTimestampMs = this.clock.now(); + + const userReports = [] as HourlyUserMetricsReportJson[]; + // HACK! We use the same backend reporting endpoint for key and country usage. + // A row with empty country is for key usage, a row with empty userId is for country usage. + // Note that this reports usage twice. If you want the total, filter to rows with non empty countries. + for (const keyUsage of keyUsageMetrics) { + if (keyUsage.inboundBytes === 0) { + continue; + } + const userId = this.toMetricsId(keyUsage.accessKeyId); + if (!userId) { + continue; + } + userReports.push({ + userId, + bytesTransferred: keyUsage.inboundBytes, + }); + } + for (const countryUsage of countryUsageMetrics) { + if (countryUsage.inboundBytes === 0) { + continue; + } + if (isSanctionedCountry(countryUsage.country)) { + continue; + } + // Make sure to always set the country to differentiate the row + // from key usage rows. + const country = countryUsage.country || 'ZZ'; + userReports.push({ + bytesTransferred: countryUsage.inboundBytes, + countries: [country], + }); + } + const report = { + serverId: this.serverConfig.data().serverId, + startUtcMs: this.reportStartTimestampMs, + endUtcMs: reportEndTimestampMs, + userReports, + } as HourlyServerMetricsReportJson; + + this.reportStartTimestampMs = reportEndTimestampMs; + if (userReports.length === 0) { + return; + } + await this.metricsCollector.collectServerUsageMetrics(report); + } + + private async reportFeatureMetrics(): Promise { + const keys = this.keyConfig.data().accessKeys; + const featureMetricsReport = { + serverId: this.serverConfig.data().serverId, + serverVersion: version, + timestampUtcMs: this.clock.now(), + dataLimit: { + enabled: !!this.serverConfig.data().accessKeyDataLimit, + perKeyLimitCount: keys.filter((key) => !!key.dataLimit).length, + }, + }; + await this.metricsCollector.collectFeatureMetrics(featureMetricsReport); + } +} + +function isSanctionedCountry(country: string) { + return SANCTIONED_COUNTRIES.has(country); +} diff --git a/src/shadowbox/server/start.action.sh b/src/shadowbox/server/start.action.sh new file mode 100755 index 000000000..b36e2b0e3 --- /dev/null +++ b/src/shadowbox/server/start.action.sh @@ -0,0 +1,38 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +run_action shadowbox/server/build + +RUN_ID="${RUN_ID:-$(date +%Y-%m-%d-%H%M%S)}" +readonly RUN_DIR="/tmp/outline/${RUN_ID}" +echo "Using directory ${RUN_DIR}" + +export LOG_LEVEL="${LOG_LEVEL:-debug}" +SB_PUBLIC_IP="${SB_PUBLIC_IP:-$(curl https://ipinfo.io/ip)}" +export SB_PUBLIC_IP +# WARNING: The SB_API_PREFIX should be kept secret! +export SB_API_PREFIX='TestApiPrefix' +export SB_METRICS_URL='https://dev.metrics.getoutline.org' +export SB_STATE_DIR="${RUN_DIR}/persisted-state" +readonly STATE_CONFIG="${SB_STATE_DIR}/shadowbox_server_config.json" + +[[ -d "${SB_STATE_DIR}" ]] || mkdir -p "${SB_STATE_DIR}" +[[ -e "${STATE_CONFIG}" ]] || echo '{"hostname":"127.0.0.1"}' > "${STATE_CONFIG}" + +# shellcheck source=../scripts/make_test_certificate.sh +source "${ROOT_DIR}/src/shadowbox/scripts/make_test_certificate.sh" "${RUN_DIR}" + +node "${BUILD_DIR}/shadowbox/app/main.js" diff --git a/src/shadowbox/shadowbox_config.json b/src/shadowbox/shadowbox_config.json new file mode 100644 index 000000000..9043b57bc --- /dev/null +++ b/src/shadowbox/shadowbox_config.json @@ -0,0 +1 @@ +{"users": []} diff --git a/src/shadowbox/test.action.sh b/src/shadowbox/test.action.sh new file mode 100755 index 000000000..73b8421d4 --- /dev/null +++ b/src/shadowbox/test.action.sh @@ -0,0 +1,23 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# 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. + +readonly TEST_DIR="${BUILD_DIR}/js/shadowbox/" +rm -rf "${TEST_DIR}" + +tsc -p "${ROOT_DIR}/src/shadowbox" --outDir "${TEST_DIR}" +jasmine --config="${ROOT_DIR}/jasmine.json" + +rm -rf "${TEST_DIR}" diff --git a/src/shadowbox/tsconfig.json b/src/shadowbox/tsconfig.json new file mode 100644 index 000000000..75bfaf300 --- /dev/null +++ b/src/shadowbox/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2015", + "removeComments": false, + "noImplicitAny": false, + "noImplicitThis": true, + "module": "commonjs", + "rootDir": ".", + "resolveJsonModule": true, + "sourceMap": true, + "skipLibCheck": true + }, + "include": ["server/main.ts", "**/*.spec.ts", "types/**/*.d.ts"], + "exclude": ["build", "node_modules"] +} diff --git a/src/shadowbox/types/node.d.ts b/src/shadowbox/types/node.d.ts new file mode 100644 index 000000000..bd742a3c4 --- /dev/null +++ b/src/shadowbox/types/node.d.ts @@ -0,0 +1,31 @@ +// Copyright 2018 The Outline Authors +// +// 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. + +// Definitions missing from @types/node. + +// Reference: https://nodejs.org/api/dns.html +declare module 'dns' { + export function getServers(): string[]; +} + +// https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_child_process_exec_command_options_callback +declare module 'child_process' { + export interface ExecError { + code: number; + } + export function exec( + command: string, + callback?: (error: ExecError | undefined, stdout: string, stderr: string) => void + ): ChildProcess; +} diff --git a/src/shadowbox/webpack.config.js b/src/shadowbox/webpack.config.js new file mode 100644 index 000000000..dd1d9cbb3 --- /dev/null +++ b/src/shadowbox/webpack.config.js @@ -0,0 +1,40 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +// Copyright 2020 The Outline Authors +// +// 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. + +const path = require('path'); +const webpack = require('webpack'); + +const config = { + mode: 'production', + entry: path.resolve(__dirname, './server/main.ts'), + target: 'node', + output: { + filename: 'main.js', + path: path.resolve(__dirname, '../../build/shadowbox/app'), + }, + module: {rules: [{test: /\.ts(x)?$/, use: 'ts-loader'}]}, + node: { + // Use the regular node behavior, the directory name of the output file when run. + __dirname: false, + }, + plugins: [ + // WORKAROUND: some of our (transitive) dependencies use node-gently, which hijacks `require`. + // Setting global.GENTLY to false makes these dependencies use standard require. + new webpack.DefinePlugin({'global.GENTLY': false}), + ], + resolve: {extensions: ['.tsx', '.ts', '.js']}, +}; + +module.exports = config; diff --git a/third_party/jsign/.gitignore b/third_party/jsign/.gitignore new file mode 100644 index 000000000..d392f0e82 --- /dev/null +++ b/third_party/jsign/.gitignore @@ -0,0 +1 @@ +*.jar diff --git a/third_party/jsign/LICENSE.txt b/third_party/jsign/LICENSE.txt new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/third_party/jsign/LICENSE.txt @@ -0,0 +1,201 @@ + 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/third_party/jsign/METADATA b/third_party/jsign/METADATA new file mode 100644 index 000000000..7224c5035 --- /dev/null +++ b/third_party/jsign/METADATA @@ -0,0 +1,17 @@ +name: "Jsign" +description: + "Java implementation of Microsoft Authenticode for signing Windows " + "executable files, installers and scripts." + +third_party { + url { + type: HOMEPAGE + value: "https://ebourg.github.io/jsign/" + } + url { + type: GIT + value: "https://github.com/ebourg/jsign" + } + version: "4.2" + last_upgrade_date { year: 2022 month: 9 day: 28 } +} diff --git a/third_party/jsign/index.mjs b/third_party/jsign/index.mjs new file mode 100644 index 000000000..d67773710 --- /dev/null +++ b/third_party/jsign/index.mjs @@ -0,0 +1,65 @@ +// Copyright 2023 The Outline Authors +// +// 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. + +import {spawn} from 'node:child_process'; +import {resolve} from 'node:path'; + +import {downloadFile} from '../../src/build/download_file.mjs'; +import {getFileChecksum} from '../../src/build/get_file_checksum.mjs'; +import {getRootDir} from '../../src/build/get_root_dir.mjs'; + +/** + * Run jsign.jar to sign `fileToSign` with a list of cli arguments stored in `options`. + * @param {string} fileToSign The path string of a file to be signed. + * @param {string[]} options A list of cli arguments to be passed to jsign. see https://ebourg.github.io/jsign/ + * @returns {Promise} A promise containing the exit code of jsign. + */ +export async function jsign(fileToSign, options) { + if (!fileToSign) { + throw new Error('fileToSign is required by jsign'); + } + if (!options) { + throw new Error('options are required by jsign'); + } + + const jsignJarPath = await ensureJsignJar(); + const jsignProc = spawn('java', ['-jar', jsignJarPath, ...options, fileToSign], { + stdio: 'inherit', + }); + return await new Promise((resolve, reject) => { + jsignProc.on('error', reject); + jsignProc.on('exit', resolve); + }); +} + + +const JSIGN_FILE_NAME = 'jsign-4.2.jar'; +const JSIGN_DOWNLOAD_URL = 'https://github.com/ebourg/jsign/releases/download/4.2/jsign-4.2.jar'; +const JSIGN_SHA256_CHECKSUM = '290377fc4f593256200b3ea4061b7409e8276255f449d4c6de7833faf0850cc1'; + +/** + * Ensure jsign.jar exists and return the absolute path to it. + */ +async function ensureJsignJar() { + const jsignPath = resolve(getRootDir(), 'third_party', 'jsign', JSIGN_FILE_NAME); + if ((await getFileChecksum(jsignPath, 'sha256')) === JSIGN_SHA256_CHECKSUM) { + return jsignPath; + } + + console.debug(`downloading jsign from "${JSIGN_DOWNLOAD_URL}" to "${jsignPath}"`); + await downloadFile(JSIGN_DOWNLOAD_URL, jsignPath, JSIGN_SHA256_CHECKSUM); + + console.debug(`successfully downloaded "${jsignPath}"`); + return jsignPath; +} diff --git a/third_party/outline-ss-server/.gitignore b/third_party/outline-ss-server/.gitignore new file mode 100644 index 000000000..ae3c17260 --- /dev/null +++ b/third_party/outline-ss-server/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/third_party/outline-ss-server/LICENSE b/third_party/outline-ss-server/LICENSE new file mode 100644 index 000000000..f49a4e16e --- /dev/null +++ b/third_party/outline-ss-server/LICENSE @@ -0,0 +1,201 @@ + 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. \ No newline at end of file diff --git a/third_party/outline-ss-server/METADATA b/third_party/outline-ss-server/METADATA new file mode 100644 index 000000000..babb4cace --- /dev/null +++ b/third_party/outline-ss-server/METADATA @@ -0,0 +1,15 @@ +name: "outline-ss-server" +description: "outline-ss-server is an open-source Outline Shadowsocks server" + +third_party { + url { + type: HOMEPAGE + value: "https://getoutline.org/" + } + url { + type: ARCHIVE + value: "https://github.com/Jigsaw-Code/outline-ss-server/releases/tag/v1.4.0" + } + version: "1.4.0" + last_upgrade_date { year: 2022 month: 10 day: 24 } +} diff --git a/third_party/outline-ss-server/Makefile b/third_party/outline-ss-server/Makefile new file mode 100644 index 000000000..c780a577a --- /dev/null +++ b/third_party/outline-ss-server/Makefile @@ -0,0 +1,28 @@ +VERSION=1.4.0 + +.PHONY: all +all: bin/linux-x86_64/outline-ss-server bin/linux-arm64/outline-ss-server bin/macos-x86_64/outline-ss-server bin/macos-arm64/outline-ss-server + +bin/linux-x86_64/outline-ss-server: OS=linux +bin/linux-x86_64/outline-ss-server: SHA256=f51bcb6391cca0ae828620c429e698a3b7c409de2374c52f113ca9a525e021a8 + +bin/linux-arm64/outline-ss-server: OS=linux +bin/linux-arm64/outline-ss-server: SHA256=14ae581414c9aab04253a385ef1854c003d09f545f6f8a3a55aa987f0c6d3859 + +bin/macos-x86_64/outline-ss-server: OS=macos +bin/macos-x86_64/outline-ss-server: SHA256=c85b2e8ae2d48482cbc101e54dcb7eed074a22c14a3a7301993e5f786b34081d + +bin/macos-arm64/outline-ss-server: OS=macos +bin/macos-arm64/outline-ss-server: SHA256=9647712a7c732184f98b1e2e7f74281855afed2245ec922c4a24b54f0eb0ce72 + +TEMPFILE := $(shell mktemp) +bin/%/outline-ss-server: + node ../../src/build/download_file.mjs --url="https://github.com/Jigsaw-Code/outline-ss-server/releases/download/v$(VERSION)/outline-ss-server_$(VERSION)_$(OS)_$(ARCH).tar.gz" --out="$(TEMPFILE)" --sha256=$(SHA256) + mkdir -p "$(dir $@)" + tar -zx -f "$(TEMPFILE)" -C "$(dir $@)" "$(notdir $@)" + chmod +x "$@" + rm -f $(TEMPFILE) + +.PHONY: clean +clean: + rm -rf bin diff --git a/third_party/prometheus/.gitignore b/third_party/prometheus/.gitignore new file mode 100644 index 000000000..ae3c17260 --- /dev/null +++ b/third_party/prometheus/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/third_party/prometheus/LICENSE b/third_party/prometheus/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/third_party/prometheus/LICENSE @@ -0,0 +1,201 @@ + 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/third_party/prometheus/METADATA b/third_party/prometheus/METADATA new file mode 100644 index 000000000..6b603f4f0 --- /dev/null +++ b/third_party/prometheus/METADATA @@ -0,0 +1,22 @@ +name: "prometheus" +description: + "Prometheus is an open-source systems monitoring and alerting toolkit " + "originally built at SoundCloud." + +third_party { + url { + type: HOMEPAGE + value: "https://prometheus.io/" + } + url { + type: ARCHIVE + value: "https://github.com/prometheus/prometheus/releases/download/2.37.1/prometheus-2.37.1.linux-amd64.tar.gz" + } + url { + type: ARCHIVE + value: "https://github.com/prometheus/prometheus/releases/download/2.37.1/prometheus-2.37.1.darwin-amd64.tar.gz" + } + version: "2.37.1" + last_upgrade_date { year: 2022 month: 10 day: 24 } + license_type: PERMISSIVE +} diff --git a/third_party/prometheus/Makefile b/third_party/prometheus/Makefile new file mode 100644 index 000000000..d00624efd --- /dev/null +++ b/third_party/prometheus/Makefile @@ -0,0 +1,34 @@ +VERSION=2.37.1 + +ifeq ($(ARCH),x86_64) + APP_ARCH=amd64 +else + APP_ARCH=arm64 +endif + +.PHONY: all +all: bin/linux-x86_64/prometheus bin/linux-arm64/prometheus bin/macos-x86_64/prometheus bin/macos-arm64/prometheus + +bin/linux-x86_64/prometheus: OS=linux +bin/linux-x86_64/prometheus: SHA256=753f66437597cf52ada98c2f459aa8c03745475c249c9f2b40ac7b3919131ba6 + +bin/linux-arm64/prometheus: OS=linux +bin/linux-arm64/prometheus: SHA256=b59a66fb5c7ec5acf6bf426793528a5789a1478a0dad8c64edc2843caf31b1b8 + +bin/macos-x86_64/prometheus: OS=darwin +bin/macos-x86_64/prometheus: SHA256=e03a43d98955ac3500f57353ea74b5df829074205f195ea6b3b88f55c4575c79 + +bin/macos-arm64/prometheus: OS=darwin +bin/macos-arm64/prometheus: SHA256=eb8a174c82a0fb6c84e81d9a73214318fb4a605115ad61505d7883d02e5a6f52 + +bin/%/prometheus: TEMPFILE := $(shell mktemp) +bin/%/prometheus: + node ../../src/build/download_file.mjs --url="https://github.com/prometheus/prometheus/releases/download/v$(VERSION)/prometheus-$(VERSION).$(OS)-$(APP_ARCH).tar.gz" --out="$(TEMPFILE)" --sha256=$(SHA256) + mkdir -p "$(dir $@)" + tar -zx -f "$(TEMPFILE)" --strip-components=1 -C "$(dir $@)" prometheus-$(VERSION).$(OS)-$(APP_ARCH)/prometheus + chmod +x "$@" + rm -f $(TEMPFILE) + +.PHONY: clean +clean: + rm -rf bin diff --git a/third_party/prometheus/NOTICE b/third_party/prometheus/NOTICE new file mode 100644 index 000000000..30ce2a826 --- /dev/null +++ b/third_party/prometheus/NOTICE @@ -0,0 +1,93 @@ +The Prometheus systems and service monitoring server +Copyright 2012-2015 The Prometheus Authors + +This product includes software developed at +SoundCloud Ltd. (https://soundcloud.com/). + + +The following components are included in this product: + +Bootstrap +https://getbootstrap.com +Copyright 2011-2014 Twitter, Inc. +Licensed under the MIT License + +bootstrap3-typeahead.js +https://github.com/bassjobsen/Bootstrap-3-Typeahead +Original written by @mdo and @fat +Copyright 2014 Bass Jobsen @bassjobsen +Licensed under the Apache License, Version 2.0 + +fuzzy +https://github.com/mattyork/fuzzy +Original written by @mattyork +Copyright 2012 Matt York +Licensed under the MIT License + +bootstrap-datetimepicker.js +https://github.com/Eonasdan/bootstrap-datetimepicker +Copyright 2015 Jonathan Peterson (@Eonasdan) +Licensed under the MIT License + +moment.js +https://github.com/moment/moment/ +Copyright JS Foundation and other contributors +Licensed under the MIT License + +Rickshaw +https://github.com/shutterstock/rickshaw +Copyright 2011-2014 by Shutterstock Images, LLC +See https://github.com/shutterstock/rickshaw/blob/master/LICENSE for license details + +mustache.js +https://github.com/janl/mustache.js +Copyright 2009 Chris Wanstrath (Ruby) +Copyright 2010-2014 Jan Lehnardt (JavaScript) +Copyright 2010-2015 The mustache.js community +Licensed under the MIT License + +jQuery +https://jquery.org +Copyright jQuery Foundation and other contributors +Licensed under the MIT License + +Protocol Buffers for Go with Gadgets +https://github.com/gogo/protobuf/ +Copyright (c) 2013, The GoGo Authors. +See source code for license details. + +Go support for leveled logs, analogous to +https://code.google.com/p/google-glog/ +Copyright 2013 Google Inc. +Licensed under the Apache License, Version 2.0 + +Support for streaming Protocol Buffer messages for the Go language (golang). +https://github.com/matttproud/golang_protobuf_extensions +Copyright 2013 Matt T. Proud +Licensed under the Apache License, Version 2.0 + +DNS library in Go +https://miek.nl/2014/august/16/go-dns-package/ +Copyright 2009 The Go Authors, 2011 Miek Gieben +See https://github.com/miekg/dns/blob/master/LICENSE for license details. + +LevelDB key/value database in Go +https://github.com/syndtr/goleveldb +Copyright 2012 Suryandaru Triandana +See https://github.com/syndtr/goleveldb/blob/master/LICENSE for license details. + +gosnappy - a fork of code.google.com/p/snappy-go +https://github.com/syndtr/gosnappy +Copyright 2011 The Snappy-Go Authors +See https://github.com/syndtr/gosnappy/blob/master/LICENSE for license details. + +go-zookeeper - Native ZooKeeper client for Go +https://github.com/samuel/go-zookeeper +Copyright (c) 2013, Samuel Stauffer +See https://github.com/samuel/go-zookeeper/blob/master/LICENSE for license details. + +We also use code from a large number of npm packages. For details, see: +- https://github.com/prometheus/prometheus/blob/master/web/ui/react-app/package.json +- https://github.com/prometheus/prometheus/blob/master/web/ui/react-app/package-lock.json +- The individual package licenses as copied from the node_modules directory can be found in + the npm_licenses.tar.bz2 archive in release tarballs and Docker images. diff --git a/third_party/shellcheck/README.md b/third_party/shellcheck/README.md new file mode 100644 index 000000000..a70b94944 --- /dev/null +++ b/third_party/shellcheck/README.md @@ -0,0 +1,11 @@ +# Outline Shellcheck Wrapper + +This directory is used to lint our scripts using [Shellcheck](https://www.shellcheck.net/). To ensure consistency across developer systems, the included script + +- Attempts to identify the developer's OS (Linux, macOS, or Windows) +- Downloads a pinned version of Shellcheck into `./download` +- Checks the archive hash +- Extracts the executable +- Runs the executable + +The executable is cached on the developer's system after the first download. To clear the cache, run `rm download` (or `npm run clean` in the repository root). diff --git a/third_party/shellcheck/hashes.sha256 b/third_party/shellcheck/hashes.sha256 new file mode 100644 index 000000000..e36a79cac --- /dev/null +++ b/third_party/shellcheck/hashes.sha256 @@ -0,0 +1,3 @@ +b080c3b659f7286e27004aa33759664d91e15ef2498ac709a452445d47e3ac23 shellcheck-v0.7.1.darwin.x86_64.tar.xz +64f17152d96d7ec261ad3086ed42d18232fcb65148b44571b564d688269d36c8 shellcheck-v0.7.1.linux.x86_64.tar.xz +1763f8f4a639d39e341798c7787d360ed79c3d68a1cdbad0549c9c0767a75e98 shellcheck-v0.7.1.zip diff --git a/third_party/shellcheck/run.sh b/third_party/shellcheck/run.sh new file mode 100755 index 000000000..1fe7dca06 --- /dev/null +++ b/third_party/shellcheck/run.sh @@ -0,0 +1,48 @@ +#!/bin/bash -eu +# +# Copyright 2021 The Outline Authors +# +# 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. + +readonly VERSION='v0.7.1' + +# The relative location of this script. +DOWNLOAD_DIR="$(dirname "$0")/download" +readonly DOWNLOAD_DIR + +declare file="shellcheck-${VERSION}" # Name of the file to download +declare cmd="${DOWNLOAD_DIR}/shellcheck-${VERSION}" # Path to the executable +declare sha256='' # SHA256 checksum +case "$(uname -s)" in + Linux) file+='.linux.x86_64.tar.xz'; cmd+='/shellcheck'; sha256='64f17152d96d7ec261ad3086ed42d18232fcb65148b44571b564d688269d36c8';; + Darwin) file+='.darwin.x86_64.tar.xz'; cmd+='/shellcheck'; sha256='b080c3b659f7286e27004aa33759664d91e15ef2498ac709a452445d47e3ac23' ;; + *) file+='.zip'; cmd+='.exe'; sha256='1763f8f4a639d39e341798c7787d360ed79c3d68a1cdbad0549c9c0767a75e98';; # Presume Windows/Cygwin +esac +readonly file cmd + +if [[ ! -s "${cmd}" ]]; then + mkdir -p "${DOWNLOAD_DIR}" + + node "$(dirname "$0")/../../src/build/download_file.mjs" --url="https://github.com/koalaman/shellcheck/releases/download/${VERSION}/${file}" --out="${DOWNLOAD_DIR}/${file}" --sha256="${sha256}" + + pushd "${DOWNLOAD_DIR}" + if [[ "${file}" == *'.tar.xz' ]]; then + tar xf "${file}" + else + unzip "${file}" + fi + popd > /dev/null + chmod +x "${cmd}" +fi + +"${cmd}" "$@" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..4d251a13e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2015", + "removeComments": false, + "noImplicitAny": true, + "noImplicitThis": true, + "moduleResolution": "Node", + "sourceMap": true, + "experimentalDecorators": true, + "allowJs": true, + "resolveJsonModule": true, + "noUnusedLocals": true, + "skipLibCheck": true + } +}